Merge branch 'es/worktree-add-post-checkout-hook' into next
authorJunio C Hamano <gitster@pobox.com>
Wed, 21 Feb 2018 23:41:18 +0000 (15:41 -0800)
committerJunio C Hamano <gitster@pobox.com>
Wed, 21 Feb 2018 23:41:18 +0000 (15:41 -0800)
"git worktree add" learned to run the post-checkout hook, just like
"git clone" runs it upon the initial checkout.

* es/worktree-add-post-checkout-hook:
worktree: add: fix 'post-checkout' not knowing new worktree location

1  2 
builtin/worktree.c
t/t2025-worktree-add.sh
diff --combined builtin/worktree.c
index 9efdc224661b02192d06c6877e49c5585ddbcdca,81e40413f84a6e4ab1603c7bab8630fc99a4ce0e..4e7c98758fcfb93c19e3dbf58fc3167829e998b8
@@@ -1,5 -1,4 +1,5 @@@
  #include "cache.h"
 +#include "checkout.h"
  #include "config.h"
  #include "builtin.h"
  #include "dir.h"
@@@ -14,7 -13,7 +14,7 @@@
  #include "worktree.h"
  
  static const char * const worktree_usage[] = {
 -      N_("git worktree add [<options>] <path> [<branch>]"),
 +      N_("git worktree add [<options>] <path> [<commit-ish>]"),
        N_("git worktree list [<options>]"),
        N_("git worktree lock [<options>] <path>"),
        N_("git worktree prune [<options>]"),
@@@ -33,19 -32,8 +33,19 @@@ struct add_opts 
  
  static int show_only;
  static int verbose;
 +static int guess_remote;
  static timestamp_t expire;
  
 +static int git_worktree_config(const char *var, const char *value, void *cb)
 +{
 +      if (!strcmp(var, "worktree.guessremote")) {
 +              guess_remote = git_config_bool(var, value);
 +              return 0;
 +      }
 +
 +      return git_default_config(var, value, cb);
 +}
 +
  static int prune_worktree(const char *id, struct strbuf *reason)
  {
        struct stat st;
@@@ -345,9 -333,23 +345,23 @@@ done
         * Hook failure does not warrant worktree deletion, so run hook after
         * is_junk is cleared, but do return appropriate code when hook fails.
         */
-       if (!ret && opts->checkout)
-               ret = run_hook_le(NULL, "post-checkout", oid_to_hex(&null_oid),
-                                 oid_to_hex(&commit->object.oid), "1", NULL);
+       if (!ret && opts->checkout) {
+               const char *hook = find_hook("post-checkout");
+               if (hook) {
+                       const char *env[] = { "GIT_DIR", "GIT_WORK_TREE", NULL };
+                       cp.git_cmd = 0;
+                       cp.no_stdin = 1;
+                       cp.stdout_to_stderr = 1;
+                       cp.dir = path;
+                       cp.env = env;
+                       cp.argv = NULL;
+                       argv_array_pushl(&cp.args, absolute_path(hook),
+                                        oid_to_hex(&null_oid),
+                                        oid_to_hex(&commit->object.oid),
+                                        "1", NULL);
+                       ret = run_command(&cp);
+               }
+       }
  
        argv_array_clear(&child_env);
        strbuf_release(&sb);
@@@ -363,7 -365,6 +377,7 @@@ static int add(int ac, const char **av
        const char *new_branch_force = NULL;
        char *path;
        const char *branch;
 +      const char *opt_track = NULL;
        struct option options[] = {
                OPT__FORCE(&opts.force, N_("checkout <branch> even if already checked out in other worktree")),
                OPT_STRING('b', NULL, &opts.new_branch, N_("branch"),
                OPT_BOOL(0, "detach", &opts.detach, N_("detach HEAD at named commit")),
                OPT_BOOL(0, "checkout", &opts.checkout, N_("populate the new working tree")),
                OPT_BOOL(0, "lock", &opts.keep_locked, N_("keep the new working tree locked")),
 +              OPT_PASSTHRU(0, "track", &opt_track, NULL,
 +                           N_("set up tracking mode (see git-branch(1))"),
 +                           PARSE_OPT_NOARG | PARSE_OPT_OPTARG),
 +              OPT_BOOL(0, "guess-remote", &guess_remote,
 +                       N_("try to match the new branch name with a remote-tracking branch")),
                OPT_END()
        };
  
                int n;
                const char *s = worktree_basename(path, &n);
                opts.new_branch = xstrndup(s, n);
 +              if (guess_remote) {
 +                      struct object_id oid;
 +                      const char *remote =
 +                              unique_tracking_name(opts.new_branch, &oid);
 +                      if (remote)
 +                              branch = remote;
 +              }
 +      }
 +
 +      if (ac == 2 && !opts.new_branch && !opts.detach) {
 +              struct object_id oid;
 +              struct commit *commit;
 +              const char *remote;
 +
 +              commit = lookup_commit_reference_by_name(branch);
 +              if (!commit) {
 +                      remote = unique_tracking_name(branch, &oid);
 +                      if (remote) {
 +                              opts.new_branch = branch;
 +                              branch = remote;
 +                      }
 +              }
        }
  
        if (opts.new_branch) {
                        argv_array_push(&cp.args, "--force");
                argv_array_push(&cp.args, opts.new_branch);
                argv_array_push(&cp.args, branch);
 +              if (opt_track)
 +                      argv_array_push(&cp.args, opt_track);
                if (run_command(&cp))
                        return -1;
                branch = opts.new_branch;
 +      } else if (opt_track) {
 +              die(_("--[no-]track can only be used if a new branch is created"));
        }
  
        UNLEAK(path);
@@@ -611,7 -581,7 +625,7 @@@ int cmd_worktree(int ac, const char **a
                OPT_END()
        };
  
 -      git_config(git_default_config, NULL);
 +      git_config(git_worktree_config, NULL);
  
        if (ac < 2)
                usage_with_options(worktree_usage, options);
diff --combined t/t2025-worktree-add.sh
index 2b959449730e14dd4e650ce9115a463d71eedca4,6ea4f5a2f7d8c0c2780aaf348aaeb08332987d92..d0d2e4f7ec3310ec51da7144fa87151129f393c0
@@@ -245,12 -245,6 +245,12 @@@ test_expect_success 'local clone from l
        ( cd here-clone && git fsck )
  '
  
 +test_expect_success 'local clone --shared from linked checkout' '
 +      git -C bare worktree add --detach ../baretree &&
 +      git clone --local --shared baretree bare-clone &&
 +      grep /bare/ bare-clone/.git/objects/info/alternates
 +'
 +
  test_expect_success '"add" worktree with --no-checkout' '
        git worktree add --no-checkout -b swamp swamp &&
        ! test -e swamp/init.t &&
@@@ -319,164 -313,70 +319,200 @@@ test_expect_success 'checkout a branch 
  test_expect_success 'rename a branch under bisect not allowed' '
        test_must_fail git branch -M under-bisect bisect-with-new-name
  '
 +# Is branch "refs/heads/$1" set to pull from "$2/$3"?
 +test_branch_upstream () {
 +      printf "%s\n" "$2" "refs/heads/$3" >expect.upstream &&
 +      {
 +              git config "branch.$1.remote" &&
 +              git config "branch.$1.merge"
 +      } >actual.upstream &&
 +      test_cmp expect.upstream actual.upstream
 +}
 +
 +test_expect_success '--track sets up tracking' '
 +      test_when_finished rm -rf track &&
 +      git worktree add --track -b track track master &&
 +      test_branch_upstream track . master
 +'
 +
 +# setup remote repository $1 and repository $2 with $1 set up as
 +# remote.  The remote has two branches, master and foo.
 +setup_remote_repo () {
 +      git init $1 &&
 +      (
 +              cd $1 &&
 +              test_commit $1_master &&
 +              git checkout -b foo &&
 +              test_commit upstream_foo
 +      ) &&
 +      git init $2 &&
 +      (
 +              cd $2 &&
 +              test_commit $2_master &&
 +              git remote add $1 ../$1 &&
 +              git config remote.$1.fetch \
 +                      "refs/heads/*:refs/remotes/$1/*" &&
 +              git fetch --all
 +      )
 +}
 +
 +test_expect_success '--no-track avoids setting up tracking' '
 +      test_when_finished rm -rf repo_upstream repo_local foo &&
 +      setup_remote_repo repo_upstream repo_local &&
 +      (
 +              cd repo_local &&
 +              git worktree add --no-track -b foo ../foo repo_upstream/foo
 +      ) &&
 +      (
 +              cd foo &&
 +              test_must_fail git config "branch.foo.remote" &&
 +              test_must_fail git config "branch.foo.merge" &&
 +              test_cmp_rev refs/remotes/repo_upstream/foo refs/heads/foo
 +      )
 +'
 +
 +test_expect_success '"add" <path> <non-existent-branch> fails' '
 +      test_must_fail git worktree add foo non-existent
 +'
 +
 +test_expect_success '"add" <path> <branch> dwims' '
 +      test_when_finished rm -rf repo_upstream repo_dwim foo &&
 +      setup_remote_repo repo_upstream repo_dwim &&
 +      git init repo_dwim &&
 +      (
 +              cd repo_dwim &&
 +              git worktree add ../foo foo
 +      ) &&
 +      (
 +              cd foo &&
 +              test_branch_upstream foo repo_upstream foo &&
 +              test_cmp_rev refs/remotes/repo_upstream/foo refs/heads/foo
 +      )
 +'
 +
 +test_expect_success 'git worktree add does not match remote' '
 +      test_when_finished rm -rf repo_a repo_b foo &&
 +      setup_remote_repo repo_a repo_b &&
 +      (
 +              cd repo_b &&
 +              git worktree add ../foo
 +      ) &&
 +      (
 +              cd foo &&
 +              test_must_fail git config "branch.foo.remote" &&
 +              test_must_fail git config "branch.foo.merge" &&
 +              ! test_cmp_rev refs/remotes/repo_a/foo refs/heads/foo
 +      )
 +'
 +
 +test_expect_success 'git worktree add --guess-remote sets up tracking' '
 +      test_when_finished rm -rf repo_a repo_b foo &&
 +      setup_remote_repo repo_a repo_b &&
 +      (
 +              cd repo_b &&
 +              git worktree add --guess-remote ../foo
 +      ) &&
 +      (
 +              cd foo &&
 +              test_branch_upstream foo repo_a foo &&
 +              test_cmp_rev refs/remotes/repo_a/foo refs/heads/foo
 +      )
 +'
 +
 +test_expect_success 'git worktree add with worktree.guessRemote sets up tracking' '
 +      test_when_finished rm -rf repo_a repo_b foo &&
 +      setup_remote_repo repo_a repo_b &&
 +      (
 +              cd repo_b &&
 +              git config worktree.guessRemote true &&
 +              git worktree add ../foo
 +      ) &&
 +      (
 +              cd foo &&
 +              test_branch_upstream foo repo_a foo &&
 +              test_cmp_rev refs/remotes/repo_a/foo refs/heads/foo
 +      )
 +'
 +
 +test_expect_success 'git worktree --no-guess-remote option overrides config' '
 +      test_when_finished rm -rf repo_a repo_b foo &&
 +      setup_remote_repo repo_a repo_b &&
 +      (
 +              cd repo_b &&
 +              git config worktree.guessRemote true &&
 +              git worktree add --no-guess-remote ../foo
 +      ) &&
 +      (
 +              cd foo &&
 +              test_must_fail git config "branch.foo.remote" &&
 +              test_must_fail git config "branch.foo.merge" &&
 +              ! test_cmp_rev refs/remotes/repo_a/foo refs/heads/foo
 +      )
 +'
  
  post_checkout_hook () {
-       test_when_finished "rm -f .git/hooks/post-checkout" &&
-       mkdir -p .git/hooks &&
-       write_script .git/hooks/post-checkout <<-\EOF
-       echo $* >hook.actual
+       gitdir=${1:-.git}
+       test_when_finished "rm -f $gitdir/hooks/post-checkout" &&
+       mkdir -p $gitdir/hooks &&
+       write_script $gitdir/hooks/post-checkout <<-\EOF
+       {
+               echo $*
+               git rev-parse --git-dir --show-toplevel
+       } >hook.actual
        EOF
  }
  
  test_expect_success '"add" invokes post-checkout hook (branch)' '
        post_checkout_hook &&
-       printf "%s %s 1\n" $_z40 $(git rev-parse HEAD) >hook.expect &&
+       {
+               echo $_z40 $(git rev-parse HEAD) 1 &&
+               echo $(pwd)/.git/worktrees/gumby &&
+               echo $(pwd)/gumby
+       } >hook.expect &&
        git worktree add gumby &&
-       test_cmp hook.expect hook.actual
+       test_cmp hook.expect gumby/hook.actual
  '
  
  test_expect_success '"add" invokes post-checkout hook (detached)' '
        post_checkout_hook &&
-       printf "%s %s 1\n" $_z40 $(git rev-parse HEAD) >hook.expect &&
+       {
+               echo $_z40 $(git rev-parse HEAD) 1 &&
+               echo $(pwd)/.git/worktrees/grumpy &&
+               echo $(pwd)/grumpy
+       } >hook.expect &&
        git worktree add --detach grumpy &&
-       test_cmp hook.expect hook.actual
+       test_cmp hook.expect grumpy/hook.actual
  '
  
  test_expect_success '"add --no-checkout" suppresses post-checkout hook' '
        post_checkout_hook &&
        rm -f hook.actual &&
        git worktree add --no-checkout gloopy &&
-       test_path_is_missing hook.actual
+       test_path_is_missing gloopy/hook.actual
+ '
+ test_expect_success '"add" in other worktree invokes post-checkout hook' '
+       post_checkout_hook &&
+       {
+               echo $_z40 $(git rev-parse HEAD) 1 &&
+               echo $(pwd)/.git/worktrees/guppy &&
+               echo $(pwd)/guppy
+       } >hook.expect &&
+       git -C gloopy worktree add --detach ../guppy &&
+       test_cmp hook.expect guppy/hook.actual
+ '
+ test_expect_success '"add" in bare repo invokes post-checkout hook' '
+       rm -rf bare &&
+       git clone --bare . bare &&
+       {
+               echo $_z40 $(git --git-dir=bare rev-parse HEAD) 1 &&
+               echo $(pwd)/bare/worktrees/goozy &&
+               echo $(pwd)/goozy
+       } >hook.expect &&
+       post_checkout_hook bare &&
+       git -C bare worktree add --detach ../goozy &&
+       test_cmp hook.expect goozy/hook.actual
  '
  
  test_done