git-codereview: fix hooks install when using 'git worktree'

Use 'git rev-parse' to obtain the repo root (toplevel) and the hooks
directory (git-path hooks).

Git knows better how to handle more complex scenarios, e.g. in a
linked worktree (created using "git worktree add"), ".git" is not
a directory but a regular file, that contains the path to the linked
gitdir, that then contains the path to the main gitdir. The hooks
should be there.

For older version git versions without --git-path (and without
worktree), fallback to the previous approach.

Updates golang/go#12182.

Change-Id: I7a90362409fc5000282db95c4ec2ab5052ae59a8
Reviewed-on: https://go-review.googlesource.com/19882
Run-TryBot: Andrew Gerrand <adg@golang.org>
Reviewed-by: Andrew Gerrand <adg@golang.org>
diff --git a/git-codereview/hook.go b/git-codereview/hook.go
index 264c85b..f10c070 100644
--- a/git-codereview/hook.go
+++ b/git-codereview/hook.go
@@ -13,19 +13,18 @@
 	"os"
 	"path/filepath"
 	"regexp"
-	"runtime"
 	"strings"
 )
 
-var hookPath = ".git/hooks/"
 var hookFiles = []string{
 	"commit-msg",
 	"pre-commit",
 }
 
 func installHook() {
+	hooksDir := gitPath("hooks")
 	for _, hookFile := range hookFiles {
-		filename := filepath.Join(repoRoot(), hookPath+hookFile)
+		filename := filepath.Join(hooksDir, hookFile)
 		hookContent := fmt.Sprintf(hookScript, hookFile)
 
 		if data, err := ioutil.ReadFile(filename); err == nil {
@@ -70,23 +69,22 @@
 }
 
 func repoRoot() string {
-	dir, err := os.Getwd()
+	return filepath.Clean(trim(cmdOutput("git", "rev-parse", "--show-toplevel")))
+}
+
+// gitPath resolve the $GIT_DIR/path, taking in consideration
+// all other path relocations, e.g. hooks for linked worktrees
+// are not kept in their gitdir, but shared in the main one.
+func gitPath(path string) string {
+	p, err := trimErr(cmdOutputErr("git", "rev-parse", "--git-path", path))
 	if err != nil {
-		dief("could not get current directory: %v", err)
+		// When --git-path is not available, assume the common case.
+		p = filepath.Join(".git", path)
 	}
-	rootlen := 1
-	if runtime.GOOS == "windows" {
-		rootlen += len(filepath.VolumeName(dir))
+	if !filepath.IsAbs(p) {
+		p = filepath.Join(repoRoot(), p)
 	}
-	for {
-		if _, err := os.Stat(filepath.Join(dir, ".git")); err == nil {
-			return dir
-		}
-		if len(dir) == rootlen && dir[rootlen-1] == filepath.Separator {
-			dief("git root not found. Rerun from within the Git tree.")
-		}
-		dir = filepath.Dir(dir)
-	}
+	return p
 }
 
 var hookScript = `#!/bin/sh
diff --git a/git-codereview/hook_test.go b/git-codereview/hook_test.go
index 247666c..fe083bb 100644
--- a/git-codereview/hook_test.go
+++ b/git-codereview/hook_test.go
@@ -10,6 +10,7 @@
 	"io/ioutil"
 	"os"
 	"path/filepath"
+	"regexp"
 	"strings"
 	"testing"
 )
@@ -411,6 +412,36 @@
 	}
 }
 
+var worktreeRE = regexp.MustCompile(`\sworktree\s`)
+
+func mustHaveWorktree(t *testing.T) {
+	commands := trun(t, "", "git", "help", "-a")
+	if !worktreeRE.MatchString(commands) {
+		t.Skip("git doesn't support worktree")
+	}
+}
+
+func TestHooksInWorktree(t *testing.T) {
+	gt := newGitTest(t)
+	defer gt.done()
+
+	mustHaveWorktree(t)
+
+	trun(t, gt.client, "git", "worktree", "add", "../worktree")
+	chdir(t, filepath.Join("..", "worktree"))
+
+	gt.removeStubHooks()
+	testMain(t, "hooks") // install hooks
+
+	data, err := ioutil.ReadFile(gt.client + "/.git/hooks/commit-msg")
+	if err != nil {
+		t.Fatalf("hooks did not write commit-msg hook: %v", err)
+	}
+	if string(data) != "#!/bin/sh\nexec git-codereview hook-invoke commit-msg \"$@\"\n" {
+		t.Fatalf("invalid commit-msg hook:\n%s", string(data))
+	}
+}
+
 func TestHooksOverwriteOldCommitMsg(t *testing.T) {
 	gt := newGitTest(t)
 	defer gt.done()