cmd/releasebot: remove runDir and extraEnv object state

Change-Id: I361a42676926aaf92314a2681f519bbf21a1d612
Reviewed-on: https://go-review.googlesource.com/119536
Reviewed-by: Andrew Bonventre <andybons@golang.org>
diff --git a/cmd/releasebot/git.go b/cmd/releasebot/git.go
index 59e92b0..e2474fd 100644
--- a/cmd/releasebot/git.go
+++ b/cmd/releasebot/git.go
@@ -18,8 +18,6 @@
 // gitCheckout also creates a clean checkout in
 // $HOME/go-releasebot-work/<release>/gitmirror,
 // to use as an object cache to speed future checkouts.
-// On return, w.runDir has been set to gitwork/src,
-// to allow commands like "./make.bash".
 func (w *Work) gitCheckout() {
 	shortRel := strings.ToLower(w.Milestone.Title)
 	shortRel = shortRel[:strings.LastIndex(shortRel, ".")]
@@ -33,28 +31,26 @@
 
 	// Check out a local mirror to work-mirror, to speed future checkouts for this point release.
 	mirror := filepath.Join(w.Dir, "gitmirror")
+	r := w.runner(mirror)
 	if _, err := os.Stat(mirror); err != nil {
-		w.run("git", "clone", "https://go.googlesource.com/go", mirror)
-		w.runDir = mirror
-		w.run("git", "config", "gc.auto", "0") // don't throw away refs we fetch
+		w.runner(w.Dir).run("git", "clone", "https://go.googlesource.com/go", mirror)
+		r.run("git", "config", "gc.auto", "0") // don't throw away refs we fetch
 	} else {
-		w.runDir = mirror
-		w.run("git", "fetch", "origin", "master")
+		r.run("git", "fetch", "origin", "master")
 	}
-	w.run("git", "fetch", "origin", w.ReleaseBranch)
+	r.run("git", "fetch", "origin", w.ReleaseBranch)
 
 	// Clone real Gerrit, but using local mirror for most objects.
 	gitDir := filepath.Join(w.Dir, "gitwork")
 	if err := os.RemoveAll(gitDir); err != nil {
 		w.log.Panic(err)
 	}
-	w.run("git", "clone", "--reference", mirror, "-b", w.ReleaseBranch, "https://go.googlesource.com/go", gitDir)
-	w.runDir = gitDir
-	w.run("git", "codereview", "change", "relwork")
-	w.run("git", "config", "gc.auto", "0") // don't throw away refs we fetch
-	w.runDir = filepath.Join(gitDir, "src")
+	w.runner(w.Dir).run("git", "clone", "--reference", mirror, "-b", w.ReleaseBranch, "https://go.googlesource.com/go", gitDir)
+	r = w.runner(gitDir)
+	r.run("git", "codereview", "change", "relwork")
+	r.run("git", "config", "gc.auto", "0") // don't throw away refs we fetch
 
-	_, err := w.runErr("git", "rev-parse", w.Version)
+	_, err := r.runErr("git", "rev-parse", w.Version)
 	if err == nil {
 		w.logError("%s tag already exists in Go repository!", w.Version)
 		w.log.Panic("already released")
@@ -63,10 +59,10 @@
 
 // gitTagVersion tags the release candidate or release in Git.
 func (w *Work) gitTagVersion() {
-	w.runDir = filepath.Join(w.Dir, "gitwork")
-	out := w.runOut("git", "rev-parse", "HEAD")
+	r := w.runner(filepath.Join(w.Dir, "gitwork"))
+	out := r.runOut("git", "rev-parse", "HEAD")
 	w.VersionCommit = strings.TrimSpace(string(out))
-	out = w.runOut("git", "show", w.VersionCommit)
+	out = r.runOut("git", "show", w.VersionCommit)
 	fmt.Printf("About to tag the following commit as %s:\n\n%s\n\nOk? (y/n) ", w.Version, out)
 	if dryRun {
 		return
@@ -79,10 +75,10 @@
 	if response != "y" {
 		w.log.Panic("stopped")
 	}
-	out, err = w.runErr("git", "tag", w.Version, w.VersionCommit)
+	out, err = r.runErr("git", "tag", w.Version, w.VersionCommit)
 	if err != nil {
 		w.logError("git tag failed: %s\n%s", err, out)
 		return
 	}
-	w.run("git", "push", "origin", w.Version)
+	r.run("git", "push", "origin", w.Version)
 }
diff --git a/cmd/releasebot/main.go b/cmd/releasebot/main.go
index dd13ea6..df08340 100644
--- a/cmd/releasebot/main.go
+++ b/cmd/releasebot/main.go
@@ -171,10 +171,8 @@
 // is managing multiple releases, although the current releasebot command line
 // only accepts a single release.
 type Work struct {
-	logBuf   *bytes.Buffer
-	log      *log.Logger
-	runDir   string
-	extraEnv []string
+	logBuf *bytes.Buffer
+	log    *log.Logger
 
 	Prepare bool // create the release commit and submit it for review
 
@@ -182,7 +180,7 @@
 	NextMilestone *maintner.GitHubMilestone // Next minor milestone
 	ReleaseIssue  int                       // Release status issue number
 	ReleaseBranch string
-	Dir           string // work directory
+	Dir           string // work directory ($HOME/go-releasebot-work/<release>)
 	Errors        []string
 	ReleaseBinary string
 	Version       string
@@ -224,14 +222,28 @@
 	w.postSummary()
 }
 
+type runner struct {
+	w        *Work
+	dir      string
+	extraEnv []string
+}
+
+func (w *Work) runner(dir string, env ...string) *runner {
+	return &runner{
+		w:        w,
+		dir:      dir,
+		extraEnv: env,
+	}
+}
+
 // run runs the command and requires that it succeeds.
 // If not, it logs the failure and aborts the work.
 // It logs the command line.
-func (w *Work) run(args ...string) {
-	out, err := w.runErr(args...)
+func (r *runner) run(args ...string) {
+	out, err := r.runErr(args...)
 	if err != nil {
-		w.log.Printf("command failed: %s\n%s", err, out)
-		panic("cmd")
+		r.w.log.Printf("command failed: %s\n%s", err, out)
+		panic("command failed")
 	}
 }
 
@@ -240,47 +252,28 @@
 // It does not log the command line except in case of failure.
 // Not logging these commands avoids filling the log with
 // runs of side-effect-free commands like "git cat-file commit HEAD".
-func (w *Work) runOut(args ...string) []byte {
+func (r *runner) runOut(args ...string) []byte {
 	cmd := exec.Command(args[0], args[1:]...)
-	// Make Git editor a no-op so that git codereview submit -i does not pop up
-	// an editor.
-	cmd.Env = []string{"EDITOR=true"}
-	cmd.Dir = w.runDir
+	cmd.Dir = r.dir
 	out, err := cmd.CombinedOutput()
 	if err != nil {
-		w.log.Printf("$ %s\n", strings.Join(args, " "))
-		w.log.Printf("command failed: %s\n%s", err, out)
-		panic("cmd")
+		r.w.log.Printf("$ %s\n", strings.Join(args, " "))
+		r.w.log.Printf("command failed: %s\n%s", err, out)
+		panic("command failed")
 	}
 	return out
 }
 
 // runErr runs the given command and returns the output and status (error).
 // It logs the command line.
-// It retries certain known-flaky commands automatically.
-func (w *Work) runErr(args ...string) ([]byte, error) {
-	maxTry := 1
-	try := 0
-	// Gerrit sometimes returns 502 errors from git fetch
-	if len(args) >= 2 && args[0] == "git" && args[1] == "fetch" {
-		maxTry = 3
-	}
-Again:
-	try++
-	w.log.Printf("$ %s\n", strings.Join(args, " "))
+func (r *runner) runErr(args ...string) ([]byte, error) {
+	r.w.log.Printf("$ %s\n", strings.Join(args, " "))
 	cmd := exec.Command(args[0], args[1:]...)
-	// Make Git editor a no-op so that git codereview submit -i does not pop up
-	// an editor.
-	cmd.Env = append(os.Environ(), "EDITOR=true")
-	cmd.Dir = w.runDir
-	if len(w.extraEnv) > 0 {
-		cmd.Env = append(cmd.Env, w.extraEnv...)
+	cmd.Dir = r.dir
+	if len(r.extraEnv) > 0 {
+		cmd.Env = append(os.Environ(), r.extraEnv...)
 	}
-	out, err := cmd.CombinedOutput()
-	if err != nil && try < maxTry {
-		goto Again
-	}
-	return out, err
+	return cmd.CombinedOutput()
 }
 
 func (w *Work) doRelease() {
@@ -428,7 +421,7 @@
 func (w *Work) checkDocs() {
 	// Check that we've documented the release.
 	version := strings.ToLower(w.Milestone.Title)
-	data, err := ioutil.ReadFile(filepath.Join(w.runDir, "../doc/devel/release.html"))
+	data, err := ioutil.ReadFile(filepath.Join(w.Dir, "gitwork", "doc/devel/release.html"))
 	if err != nil {
 		w.log.Panic(err)
 	}
@@ -442,7 +435,7 @@
 
 	version := strings.ToLower(w.Milestone.Title)
 
-	err := ioutil.WriteFile(filepath.Join(w.runDir, "../VERSION"), []byte(version), 0666)
+	err := ioutil.WriteFile(filepath.Join(w.Dir, "gitwork", "VERSION"), []byte(version), 0666)
 	if err != nil {
 		w.log.Panic(err)
 	}
@@ -450,18 +443,19 @@
 	desc := version + "\n\n"
 	desc += "Change-Id: " + changeID + "\n"
 
-	w.run("git", "commit", "-m", desc, "../VERSION")
+	r := w.runner(filepath.Join(w.Dir, "gitwork"))
+	r.run("git", "commit", "-m", desc, "VERSION")
 	if dryRun {
-		fmt.Printf("\n### VERSION commit\n\n%s\n", w.runOut("git", "show", "HEAD"))
+		fmt.Printf("\n### VERSION commit\n\n%s\n", r.runOut("git", "show", "HEAD"))
 	} else {
-		w.run("git", "codereview", "mail", "-trybot", "HEAD")
+		r.run("git", "codereview", "mail", "-trybot", "HEAD")
 	}
 	return
 }
 
 // checkVersion makes sure that the version commit has been submitted.
 func (w *Work) checkVersion() {
-	ver, err := ioutil.ReadFile(filepath.Join(w.runDir, "../VERSION"))
+	ver, err := ioutil.ReadFile(filepath.Join(w.Dir, "gitwork", "VERSION"))
 	if err != nil {
 		w.log.Panic(err)
 	}
@@ -478,8 +472,8 @@
 	if err := os.MkdirAll(gopath, 0777); err != nil {
 		w.log.Panic(err)
 	}
-	w.extraEnv = append(w.extraEnv, "GOPATH="+gopath, "GOBIN="+filepath.Join(gopath, "bin"))
-	w.run("go", "get", "golang.org/x/build/cmd/release")
+	r := w.runner(w.Dir, "GOPATH="+gopath, "GOBIN="+filepath.Join(gopath, "bin"))
+	r.run("go", "get", "golang.org/x/build/cmd/release")
 	w.ReleaseBinary = filepath.Join(gopath, "bin/release")
 }
 
@@ -488,7 +482,6 @@
 	if err := os.MkdirAll(filepath.Join(w.Dir, "release"), 0777); err != nil {
 		w.log.Panic(err)
 	}
-	w.runDir = filepath.Join(w.Dir, "release")
 	w.ReleaseInfo = make(map[string]*ReleaseInfo)
 
 	var wg sync.WaitGroup
@@ -564,7 +557,7 @@
 			Suffix: strings.TrimPrefix(file, prefix),
 		}
 		outs = append(outs, out)
-		_, err := os.Stat(filepath.Join(w.runDir, file))
+		_, err := os.Stat(filepath.Join(w.Dir, "release", file))
 		if err != nil {
 			haveFiles = false
 		}
@@ -576,14 +569,17 @@
 	if haveFiles {
 		w.log.Printf("release %s: already have %v; not rebuilding files", target, files)
 	} else {
-		for failures := 0; ; {
-			out, err := w.runErr(w.ReleaseBinary, "-target", target, "-user", gomoteUser, "-version", w.Version, "-rev", w.VersionCommit, "-tools", w.ReleaseBranch, "-net", w.ReleaseBranch)
+		failures := 0
+		for {
+			out, err := w.runner(filepath.Join(w.Dir, "release"), "GOPATH="+filepath.Join(w.Dir, "gopath")).runErr(
+				w.ReleaseBinary, "-target", target, "-user", gomoteUser, "-version", w.Version, "-rev", w.VersionCommit,
+				"-tools", w.ReleaseBranch, "-net", w.ReleaseBranch)
 			// Exit code from release binary is apparently unreliable.
 			// Look to see if the files we expected were created instead.
 			failed := false
 			w.releaseMu.Lock()
 			for _, out := range outs {
-				if _, err := os.Stat(filepath.Join(w.runDir, out.File)); err != nil {
+				if _, err := os.Stat(filepath.Join(w.Dir, "release", out.File)); err != nil {
 					failed = true
 				}
 			}
@@ -630,7 +626,7 @@
 		return errors.New("attempted write operation in dry-run mode")
 	}
 
-	src := filepath.Join(w.runDir, out.File)
+	src := filepath.Join(w.Dir, "release", out.File)
 	h := sha256.New()
 	f, err := os.Open(src)
 	if err != nil {