Skip to content

Instantly share code, notes, and snippets.

@apparentlymart
Created June 14, 2017 19:16
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save apparentlymart/90b90b82c672777847be57b25db3ff8d to your computer and use it in GitHub Desktop.
Save apparentlymart/90b90b82c672777847be57b25db3ff8d to your computer and use it in GitHub Desktop.
Terraform Provider PR migration helper
//
// 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