git-codereview: make git change handle GitHub PRs

Change-Id: Iab0417d14a29dce1a43f30f1f357d3f71dfdb51f
Reviewed-on: https://go-review.googlesource.com/c/review/+/279721
Trust: Russ Cox <rsc@golang.org>
Reviewed-by: Matthew Dempsky <mdempsky@google.com>
diff --git a/git-codereview/change.go b/git-codereview/change.go
index 6f76df1..5320d9d 100644
--- a/git-codereview/change.go
+++ b/git-codereview/change.go
@@ -103,16 +103,24 @@
 }
 
 func checkoutOrCreate(target string) {
-	// If it's a valid Gerrit number, checkout the CL.
+	// If it's a valid Gerrit number CL or CL/PS or GitHub pull request number PR,
+	// checkout the CL or PR.
 	cl, ps, isCL := parseCL(target)
 	if isCL {
-		if !haveGerrit() {
+		what := "CL"
+		if !haveGerrit() && haveGitHub() {
+			what = "PR"
+			if ps != "" {
+				dief("change PR syntax is NNN not NNN.PP")
+			}
+		}
+		if what == "CL" && !haveGerrit() {
 			dief("cannot change to a CL without gerrit")
 		}
 		if HasStagedChanges() || HasUnstagedChanges() {
-			dief("cannot change to a CL with uncommitted work")
+			dief("cannot change to a %s with uncommitted work", what)
 		}
-		checkoutCL(cl, ps)
+		checkoutCL(what, cl, ps)
 		return
 	}
 
@@ -176,8 +184,8 @@
 }
 
 // Checkout the patch set of the given CL. When patch set is empty, use the latest.
-func checkoutCL(cl, ps string) {
-	if ps == "" {
+func checkoutCL(what, cl, ps string) {
+	if what == "CL" && ps == "" {
 		change, err := readGerritChange(cl + "?o=CURRENT_REVISION")
 		if err != nil {
 			dief("cannot change to CL %s: %v", cl, err)
@@ -189,28 +197,36 @@
 		ps = strconv.Itoa(rev.Number)
 	}
 
-	var group string
-	if len(cl) > 1 {
-		group = cl[len(cl)-2:]
+	var ref string
+	if what == "CL" {
+		var group string
+		if len(cl) > 1 {
+			group = cl[len(cl)-2:]
+		} else {
+			group = "0" + cl
+		}
+		cl = fmt.Sprintf("%s/%s", cl, ps)
+		ref = fmt.Sprintf("refs/changes/%s/%s", group, cl)
 	} else {
-		group = "0" + cl
+		ref = fmt.Sprintf("pull/%s/head", 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)
+		dief("cannot change to %v %s: %v", what, cl, err)
 	}
 	err = runErr("git", "checkout", "-q", "FETCH_HEAD")
 	if err != nil {
-		dief("cannot change to CL %s/%s: %v", cl, ps, err)
+		dief("cannot change to %s %s: %v", what, cl, err)
+	}
+	if *noRun {
+		return
 	}
 	subject, err := trimErr(cmdOutputErr("git", "log", "--format=%s", "-1"))
 	if err != nil {
-		printf("changed to CL %s/%s.", cl, ps)
+		printf("changed to %s %s.", what, cl)
 		dief("cannot read change subject from git: %v", err)
 	}
-	printf("changed to CL %s/%s.\n\t%s", cl, ps, subject)
+	printf("changed to %s %s.\n\t%s", what, cl, subject)
 }
 
 var parseCLRE = regexp.MustCompile(`^([0-9]+)(?:/([0-9]+))?$`)
diff --git a/git-codereview/change_test.go b/git-codereview/change_test.go
index d3b37b0..3e3ea30 100644
--- a/git-codereview/change_test.go
+++ b/git-codereview/change_test.go
@@ -160,6 +160,21 @@
 	checkChangeCL("100/1", "refs/changes/00/100/1", hash1)
 	checkChangeCL("100/2", "refs/changes/00/100/2", hash2)
 	checkChangeCL("100", "refs/changes/00/100/3", hash1)
+
+	// turn off gerrit, make it look like we are on GitHub
+	write(t, gt.server+"/codereview.cfg", "nothing: here", 0644)
+	trun(t, gt.server, "git", "add", "codereview.cfg")
+	trun(t, gt.server, "git", "commit", "-m", "new codereview.cfg on main")
+	testMain(t, "change", "main")
+	trun(t, gt.client, "git", "pull", "-r")
+	trun(t, gt.client, "git", "remote", "set-url", "origin", "https://github.com/google/not-a-project")
+
+	testMain(t, "change", "-n", "123")
+	testNoStdout(t)
+	testPrintedStderr(t,
+		"git fetch -q origin pull/123/head",
+		"git checkout -q FETCH_HEAD",
+	)
 }
 
 func TestChangeWithMessage(t *testing.T) {
diff --git a/git-codereview/config.go b/git-codereview/config.go
index d7a802e..4e0a565 100644
--- a/git-codereview/config.go
+++ b/git-codereview/config.go
@@ -75,6 +75,11 @@
 	return strings.HasSuffix(host, ".googlesource.com")
 }
 
+func haveGitHub() bool {
+	origin := trim(cmdOutput("git", "config", "remote.origin.url"))
+	return strings.Contains(origin, "github.com")
+}
+
 func parseConfig(raw string) (map[string]string, error) {
 	cfg := make(map[string]string)
 	for _, line := range nonBlankLines(raw) {
diff --git a/git-codereview/doc.go b/git-codereview/doc.go
index 4232869..a72b6db 100644
--- a/git-codereview/doc.go
+++ b/git-codereview/doc.go
@@ -154,10 +154,13 @@
 option is only useful when creating commits (e.g. if there are unstaged
 changes). If a commit already exists, it is overwritten. If -q is also
 present, -q will be ignored.
-997As a special case, if branchname is a decimal CL number, such as 987, the change
+
+As a special case, if branchname is a decimal CL number, such as 987, the change
 command downloads the latest patch set of that CL from the server and switches to it.
-A specific patch set can be requested by adding a decimal point: 987.2 for patch set 2
-of CL 987.
+A specific patch set P can be requested by adding /P: 987.2 for patch set 2 of CL 987.
+If the origin server is GitHub instead of Gerrit, then the number is
+treated a GitHub pull request number, and the change command downloads the latest
+version of that pull request. In this case, the /P suffix is disallowed.
 
 Gofmt