Created
June 14, 2017 19:16
-
-
Save apparentlymart/90b90b82c672777847be57b25db3ff8d to your computer and use it in GitHub Desktop.
Terraform Provider PR migration helper
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// This is a helper program to do some of the more tedious steps of migrating | |
// Terraform provider commits from an older PR in the main Terraform repo to | |
// a new PR in the new provider repo. | |
// | |
// Specifically it: | |
// - Retrieves the PR commits from the Terraform repo | |
// - Computes the diff for each commit that the PR introduced | |
// - Does basic rewriting of paths in the diff for conventions in the new repos | |
// - Tries to apply the rewritten diff, creating new commits in the current repo | |
// | |
// It is intended to be run within the provider repo, on a branch where the | |
// new commits should be added: | |
// | |
// $ cd $GOPATH/src/terraform-providers/terraform-provider-aws | |
// $ git checkout master | |
// $ git pull --rebase | |
// $ git checkout -b pr-branch | |
// $ go run terraform-pr-migrate.go <terraform-pr-number> | |
// | |
// In the unlikely event that it succeeds completely without intervention, | |
// each of the original PR commits will be applied. There will probably be | |
// merge conflicts along the way, in which case this tool exits in a state | |
// where a merge is in progress, so usual conflict resolution techniques can | |
// be used in conjunction with the git am workflow commands: | |
// git am --continue | |
// git am --abort | |
// | |
package main | |
import ( | |
"bufio" | |
"bytes" | |
"fmt" | |
"os" | |
"os/exec" | |
"regexp" | |
"strings" | |
) | |
const terraformRepo = "https://github.com/hashicorp/terraform.git" | |
const baseRemoteRef = "refs/heads/before-provider-split" | |
var rewritePatterns = []struct { | |
pattern *regexp.Regexp | |
replacement string | |
}{ | |
{regexp.MustCompile("builtin/providers/"), ""}, | |
{regexp.MustCompile("vendor/"), "vendor/"}, // no change | |
{regexp.MustCompile("website/source/docs/providers/[^/]+/"), "website/docs/"}, | |
{regexp.MustCompile("website/source/layouts/([^.]+)\\.erb"), "website/docs/$1.erb"}, | |
} | |
func realmain(args []string) error { | |
if len(args) != 2 || args[1] == "--help" || args[1] == "-?" { | |
return fmt.Errorf("usage: go run terraform-pr-migrate.go <terraform-pr-number>\n") | |
} | |
fmt.Printf("Attempting to apply Terraform PR %s to the current repository as a new set of commits.\n", args[1]) | |
prRemoteRef := fmt.Sprintf("refs/pull/%s/head", args[1]) | |
fmt.Printf("\n### Fetching commits from 'terraform' repo...\n") | |
err := git("fetch", terraformRepo, baseRemoteRef) | |
if err != nil { | |
return err | |
} | |
// Capture the base remote commit as a ref so we don't need to | |
// re-download the entire history when we fetch the PR, and | |
// future runs of this program will re-use the already-downloaded | |
// history. | |
baseCommit, err := gitPlumb("rev-parse", "FETCH_HEAD") | |
if err != nil { | |
return fmt.Errorf("failed to determine base commit: %s", err) | |
} | |
git("update-ref", "-m", "Terraform provider split point", "refs/terraform-provider-split", baseCommit) | |
fmt.Printf("\n### Fetching PR commits...\n") | |
err = git("fetch", terraformRepo, prRemoteRef) | |
if err != nil { | |
return err | |
} | |
prCommit, err := gitPlumb("rev-parse", "FETCH_HEAD") | |
if err != nil { | |
return fmt.Errorf("failed to determine PR commit: %s", err) | |
} | |
fmt.Printf("\n### Computing diff...\n") | |
patchRange := fmt.Sprintf("%s..%s", baseCommit, prCommit) | |
origPatch, err := gitPlumb("format-patch", "--stdout", patchRange) | |
if err != nil { | |
return err | |
} | |
r := strings.NewReader(origPatch) | |
patchBuf := &bytes.Buffer{} | |
sc := bufio.NewScanner(r) | |
for sc.Scan() { | |
line := sc.Bytes() | |
if bytes.HasPrefix(line, []byte{'-', '-', '-', ' ', 'a'}) || | |
bytes.HasPrefix(line, []byte{'+', '+', '+', ' ', 'b'}) || | |
bytes.HasPrefix(line, []byte{'d', 'i', 'f', 'f', ' ', '-', '-', 'g', 'i', 't', ' '}) { | |
// Looks like a diff header | |
matched := false | |
for _, rule := range rewritePatterns { | |
if rule.pattern.Match(line) { | |
matched = true | |
} | |
line = rule.pattern.ReplaceAll(line, []byte(rule.replacement)) | |
} | |
if !matched { | |
fmt.Fprintf(os.Stderr, "WARNING: no rewrite rule for line %s", line) | |
} | |
} | |
patchBuf.Write(line) | |
patchBuf.WriteByte('\n') | |
} | |
fmt.Printf("\n### Applying rewritten diff...\n") | |
cmd := exec.Command("git", "am", "-3") | |
cmd.Stdin = patchBuf | |
cmd.Stdout = os.Stdout | |
cmd.Stderr = os.Stderr | |
err = cmd.Run() | |
if err != nil { | |
fmt.Fprintf(os.Stderr, ` | |
It seems that the patch did not apply successfully. | |
A three-way merge should now be in progress, allowing you to resolve | |
conflicts in the usual way. Use git commands directly to complete the merge. | |
Start with "git status" to see what state things are in. | |
`) | |
} | |
return nil | |
} | |
func git(args ...string) error { | |
cmd := exec.Command("git", args...) | |
cmd.Stdin = os.Stdin | |
cmd.Stdout = os.Stdout | |
cmd.Stderr = os.Stderr | |
return cmd.Run() | |
} | |
func gitPlumb(args ...string) (string, error) { | |
buf := &bytes.Buffer{} | |
cmd := exec.Command("git", args...) | |
cmd.Stdin = os.Stdin | |
cmd.Stdout = buf | |
cmd.Stderr = os.Stderr | |
err := cmd.Run() | |
if err != nil { | |
return "", err | |
} | |
return strings.TrimSpace(buf.String()), nil | |
} | |
func main() { | |
err := realmain(os.Args) | |
if err != nil { | |
fmt.Fprintf(os.Stderr, "%s\n", err) | |
os.Exit(1) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment