Merge branch 'es/worktree-checkout-hook'
authorJunio C Hamano <gitster@pobox.com>
Wed, 27 Dec 2017 19:16:21 +0000 (11:16 -0800)
committerJunio C Hamano <gitster@pobox.com>
Wed, 27 Dec 2017 19:16:21 +0000 (11:16 -0800)
"git worktree add" learned to run the post-checkout hook, just like
"git checkout" does, after the initial checkout.

* es/worktree-checkout-hook:
  worktree: invoke post-checkout hook (unless --no-checkout)

1  2 
Documentation/githooks.txt
builtin/worktree.c
t/t2025-worktree-add.sh

@@@ -170,7 -170,8 +170,8 @@@ This hook cannot affect the outcome of 
  
  It is also run after 'git clone', unless the --no-checkout (-n) option is
  used. The first parameter given to the hook is the null-ref, the second the
- ref of the new HEAD and the flag is always 1.
+ ref of the new HEAD and the flag is always 1. Likewise for 'git worktree add'
+ unless --no-checkout is used.
  
  This hook can be used to perform repository validity checks, auto-display
  differences from the previous HEAD if different, or set working dir metadata
@@@ -223,8 -224,8 +224,8 @@@ to the user by writing to standard erro
  pre-receive
  ~~~~~~~~~~~
  
 -This hook is invoked by 'git-receive-pack' on the remote repository,
 -which happens when a 'git push' is done on a local repository.
 +This hook is invoked by 'git-receive-pack' when it reacts to
 +'git push' and updates reference(s) in its repository.
  Just before starting to update refs on the remote repository, the
  pre-receive hook is invoked.  Its exit status determines the success
  or failure of the update.
@@@ -264,8 -265,8 +265,8 @@@ linkgit:git-receive-pack[1] for some ca
  update
  ~~~~~~
  
 -This hook is invoked by 'git-receive-pack' on the remote repository,
 -which happens when a 'git push' is done on a local repository.
 +This hook is invoked by 'git-receive-pack' when it reacts to
 +'git push' and updates reference(s) in its repository.
  Just before updating the ref on the remote repository, the update hook
  is invoked.  Its exit status determines the success or failure of
  the ref update.
@@@ -309,8 -310,8 +310,8 @@@ unannotated tags to be pushed
  post-receive
  ~~~~~~~~~~~~
  
 -This hook is invoked by 'git-receive-pack' on the remote repository,
 -which happens when a 'git push' is done on a local repository.
 +This hook is invoked by 'git-receive-pack' when it reacts to
 +'git push' and updates reference(s) in its repository.
  It executes on the remote repository once after all the refs have
  been updated.
  
@@@ -348,8 -349,8 +349,8 @@@ will be set to zero, `GIT_PUSH_OPTION_C
  post-update
  ~~~~~~~~~~~
  
 -This hook is invoked by 'git-receive-pack' on the remote repository,
 -which happens when a 'git push' is done on a local repository.
 +This hook is invoked by 'git-receive-pack' when it reacts to
 +'git push' and updates reference(s) in its repository.
  It executes on the remote repository once after all the refs have
  been updated.
  
@@@ -379,8 -380,8 +380,8 @@@ for the user
  push-to-checkout
  ~~~~~~~~~~~~~~~~
  
 -This hook is invoked by 'git-receive-pack' on the remote repository,
 -which happens when a 'git push' is done on a local repository, when
 +This hook is invoked by 'git-receive-pack' when it reacts to
 +'git push' and updates reference(s) in its repository, and when
  the push tries to update the branch that is currently checked out
  and the `receive.denyCurrentBranch` configuration variable is set to
  `updateInstead`.  Such a push by default is refused if the working
diff --combined builtin/worktree.c
@@@ -1,5 -1,4 +1,5 @@@
  #include "cache.h"
 +#include "checkout.h"
  #include "config.h"
  #include "builtin.h"
  #include "dir.h"
@@@ -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;
@@@ -230,20 -218,21 +230,21 @@@ static int add_worktree(const char *pat
        int counter = 0, len, ret;
        struct strbuf symref = STRBUF_INIT;
        struct commit *commit = NULL;
+       int is_branch = 0;
  
        if (file_exists(path) && !is_empty_dir(path))
                die(_("'%s' already exists"), path);
  
        /* is 'refname' a branch or commit? */
        if (!opts->detach && !strbuf_check_branch_ref(&symref, refname) &&
-                ref_exists(symref.buf)) { /* it's a branch */
+           ref_exists(symref.buf)) {
+               is_branch = 1;
                if (!opts->force)
                        die_if_checked_out(symref.buf, 0);
-       } else { /* must be a commit */
-               commit = lookup_commit_reference_by_name(refname);
-               if (!commit)
-                       die(_("invalid reference: %s"), refname);
        }
+       commit = lookup_commit_reference_by_name(refname);
+       if (!commit)
+               die(_("invalid reference: %s"), refname);
  
        name = worktree_basename(path, &len);
        git_path_buf(&sb_repo, "worktrees/%.*s", (int)(path + len - name), name);
        argv_array_pushf(&child_env, "%s=%s", GIT_WORK_TREE_ENVIRONMENT, path);
        cp.git_cmd = 1;
  
-       if (commit)
+       if (!is_branch)
                argv_array_pushl(&cp.args, "update-ref", "HEAD",
                                 oid_to_hex(&commit->object.oid), NULL);
        else
@@@ -339,6 -328,15 +340,15 @@@ done
                strbuf_addf(&sb, "%s/locked", sb_repo.buf);
                unlink_or_warn(sb.buf);
        }
+       /*
+        * 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);
        argv_array_clear(&child_env);
        strbuf_release(&sb);
        strbuf_release(&symref);
@@@ -353,7 -351,6 +363,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);
@@@ -601,7 -567,7 +611,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
@@@ -313,135 -313,34 +313,164 @@@ 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
+       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 &&
+       git worktree add gumby &&
+       test_cmp hook.expect 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 &&
+       git worktree add --detach grumpy &&
+       test_cmp hook.expect 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_done