blob: 790d4e6cc4ec18ce90281439a38f8b3dbc6269c0 [file] [log] [blame]
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"fmt"
"os"
"regexp"
"strconv"
"strings"
)
var commitMsg string
var changeAuto bool
var changeQuick bool
func cmdChange(args []string) {
flags.StringVar(&commitMsg, "m", "", "specify a commit message")
flags.BoolVar(&changeAuto, "a", false, "add changes to any tracked files")
flags.BoolVar(&changeQuick, "q", false, "do not edit pending commit msg")
flags.Parse(args)
if len(flags.Args()) > 1 {
fmt.Fprintf(stderr(), "Usage: %s change %s [branch]\n", os.Args[0], globalFlags)
os.Exit(2)
}
// Checkout or create branch, if specified.
target := flags.Arg(0)
if target != "" {
checkoutOrCreate(target)
b := CurrentBranch()
if HasStagedChanges() && b.IsLocalOnly() && !b.HasPendingCommit() {
commitChanges(false)
}
b.check()
return
}
// Create or amend change commit.
b := CurrentBranch()
if !b.IsLocalOnly() {
dief("can't commit to %s branch (use '%s change branchname').", b.Name, os.Args[0])
}
amend := b.HasPendingCommit()
if amend {
// Dies if there is not exactly one commit.
b.DefaultCommit("amend change", "")
}
commitChanges(amend)
b.loadedPending = false // force reload after commitChanges
b.check()
}
func (b *Branch) check() {
// TODO(rsc): Test
staged, unstaged, _ := LocalChanges()
if len(staged) == 0 && len(unstaged) == 0 {
// No staged changes, no unstaged changes.
// If the branch is behind upstream, now is a good time to point that out.
// This applies to both local work branches and tracking branches.
// TODO(rsc): Test.
b.loadPending()
if n := b.CommitsBehind(); n > 0 {
printf("warning: %d commit%s behind %s; run 'git codereview sync' to update.", n, suffix(n, "s"), b.OriginBranch())
}
}
// TODO(rsc): Test
if text := b.errors(); text != "" {
printf("error: %s\n", text)
}
}
var testCommitMsg string
func commitChanges(amend bool) {
// git commit will run the gofmt hook.
// Run it now to give a better error (won't show a git commit command failing).
hookGofmt()
if HasUnstagedChanges() && !HasStagedChanges() && !changeAuto {
printf("warning: unstaged changes and no staged changes; use 'git add' or 'git change -a'")
}
commit := func(amend bool) {
args := []string{"commit", "-q", "--allow-empty"}
if amend {
args = append(args, "--amend")
if changeQuick {
args = append(args, "--no-edit")
}
}
if commitMsg != "" {
args = append(args, "-m", commitMsg)
} else if testCommitMsg != "" {
args = append(args, "-m", testCommitMsg)
}
if changeAuto {
args = append(args, "-a")
}
run("git", args...)
}
commit(amend)
for !commitMessageOK() {
fmt.Print("re-edit commit message (y/n)? ")
if !scanYes() {
break
}
commit(true)
}
printf("change updated.")
}
func checkoutOrCreate(target string) {
// If it's a valid Gerrit number, checkout the CL.
cl, ps, isCL := parseCL(target)
if isCL {
if !haveGerrit() {
dief("cannot change to a CL without gerrit")
}
if HasStagedChanges() || HasUnstagedChanges() {
dief("cannot change to a CL with uncommitted work")
}
checkoutCL(cl, ps)
return
}
if strings.ToUpper(target) == "HEAD" {
// Git gets very upset and confused if you 'git change head'
// on systems with case-insensitive file names: the branch
// head conflicts with the usual HEAD.
dief("invalid branch name %q: ref name HEAD is reserved for git.", target)
}
// If local branch exists, check it out.
for _, b := range LocalBranches() {
if b.Name == target {
run("git", "checkout", "-q", target)
printf("changed to branch %v.", target)
return
}
}
// If origin branch exists, create local branch tracking it.
for _, name := range OriginBranches() {
if name == "origin/"+target {
run("git", "checkout", "-q", "-t", "-b", target, name)
printf("created branch %v tracking %s.", target, name)
return
}
}
// Otherwise, this is a request to create a local work branch.
// Check for reserved names. We take everything with a dot.
if strings.Contains(target, ".") {
dief("invalid branch name %v: branch names with dots are reserved for git-codereview.", target)
}
// If the current branch has a pending commit, building
// on top of it will not help. Don't allow that.
// Otherwise, inherit HEAD and upstream from the current branch.
b := CurrentBranch()
if b.HasPendingCommit() {
if !b.IsLocalOnly() {
dief("bad repo state: branch %s is ahead of origin/%s", b.Name, b.Name)
}
dief("cannot branch from work branch; change back to %v first.", strings.TrimPrefix(b.OriginBranch(), "origin/"))
}
origin := b.OriginBranch()
// NOTE: This is different from git checkout -q -t -b branch. It does not move HEAD.
run("git", "checkout", "-q", "-b", target)
run("git", "branch", "-q", "--set-upstream-to", origin)
printf("created branch %v tracking %s.", target, origin)
}
// Checkout the patch set of the given CL. When patch set is empty, use the latest.
func checkoutCL(cl, ps string) {
if ps == "" {
change, err := readGerritChange(cl + "?o=CURRENT_REVISION")
if err != nil {
dief("cannot change to CL %s: %v", cl, err)
}
rev, ok := change.Revisions[change.CurrentRevision]
if !ok {
dief("cannot change to CL %s: invalid current revision from gerrit", cl)
}
ps = strconv.Itoa(rev.Number)
}
var group string
if len(cl) > 1 {
group = cl[len(cl)-2:]
} else {
group = "0" + cl
}
ref := fmt.Sprintf("refs/changes/%s/%s/%s", group, cl, ps)
err := runErr("git", "fetch", "-q", "origin", ref)
if err != nil {
dief("cannot change to CL %s/%s: %v", cl, ps, err)
}
err = runErr("git", "checkout", "-q", "FETCH_HEAD")
if err != nil {
dief("cannot change to CL %s/%s: %v", cl, ps, err)
}
subject, err := trimErr(cmdOutputErr("git", "log", "--format=%s", "-1"))
if err != nil {
printf("changed to CL %s/%s.", cl, ps)
dief("cannot read change subject from git: %v", err)
}
printf("changed to CL %s/%s.\n\t%s", cl, ps, subject)
}
var parseCLRE = regexp.MustCompile(`^([0-9]+)(?:/([0-9]+))?$`)
// parseCL validates and splits the CL number and patch set (if present).
func parseCL(arg string) (cl, patchset string, ok bool) {
m := parseCLRE.FindStringSubmatch(arg)
if len(m) == 0 {
return "", "", false
}
return m[1], m[2], true
}
var messageRE = regexp.MustCompile(`^(\[[a-zA-Z0-9.-]+\] )?[a-zA-Z0-9-/,. ]+: `)
func commitMessageOK() bool {
body := cmdOutput("git", "log", "--format=format:%B", "-n", "1")
ok := true
if !messageRE.MatchString(body) {
fmt.Print(commitMessageWarning)
ok = false
}
return ok
}
const commitMessageWarning = `
Your CL description appears not to use the standard form.
The first line of your change description is conventionally a one-line summary
of the change, prefixed by the primary affected package, and is used as the
subject for code review mail; the rest of the description elaborates.
Examples:
encoding/rot13: new package
math: add IsInf, IsNaN
net: fix cname in LookupHost
unicode: update to Unicode 5.0.2
`
func scanYes() bool {
var s string
fmt.Scan(&s)
return strings.HasPrefix(strings.ToLower(s), "y")
}