Merge branch 'nd/worktree-lock'
authorJunio C Hamano <gitster@pobox.com>
Thu, 28 Jul 2016 17:34:41 +0000 (10:34 -0700)
committerJunio C Hamano <gitster@pobox.com>
Thu, 28 Jul 2016 17:34:42 +0000 (10:34 -0700)
"git worktree prune" protected worktrees that are marked as
"locked" by creating a file in a known location. "git worktree"
command learned a dedicated command pair to create and remove such
a file, so that the users do not have to do this with editor.

* nd/worktree-lock:
worktree.c: find_worktree() search by path suffix
worktree: add "unlock" command
worktree: add "lock" command
worktree.c: add is_worktree_locked()
worktree.c: add is_main_worktree()
worktree.c: add find_worktree()

1  2 
Documentation/git-worktree.txt
builtin/worktree.c
contrib/completion/git-completion.bash
worktree.c
index 7c4cfb0885f44619761708cb2a2d12356fbe011e,7850deedf7fc57f17060dab6e5b89bd49ee74ce6..0aeb020d026f59ad435c72e752d6f9c2f3cec73b
@@@ -11,7 -11,9 +11,9 @@@ SYNOPSI
  [verse]
  'git worktree add' [-f] [--detach] [--checkout] [-b <new-branch>] <path> [<branch>]
  'git worktree list' [--porcelain]
+ 'git worktree lock' [--reason <string>] <worktree>
  'git worktree prune' [-n] [-v] [--expire <expire>]
+ 'git worktree unlock' <worktree>
  
  DESCRIPTION
  -----------
@@@ -38,9 -40,8 +40,8 @@@ section "DETAILS" for more information
  
  If a linked working tree is stored on a portable device or network share
  which is not always mounted, you can prevent its administrative files from
- being pruned by creating a file named 'locked' alongside the other
- administrative files, optionally containing a plain text reason that
- pruning should be suppressed. See section "DETAILS" for more information.
+ being pruned by issuing the `git worktree lock` command, optionally
+ specifying `--reason` to explain why the working tree is locked.
  
  COMMANDS
  --------
@@@ -48,8 -49,7 +49,8 @@@ add <path> [<branch>]:
  
  Create `<path>` and checkout `<branch>` into it. The new working directory
  is linked to the current repository, sharing everything except working
 -directory specific files such as HEAD, index, etc.
 +directory specific files such as HEAD, index, etc. `-` may also be
 +specified as `<branch>`; it is synonymous with `@{-1}`.
  +
  If `<branch>` is omitted and neither `-b` nor `-B` nor `--detached` used,
  then, as a convenience, a new branch based at HEAD is created automatically,
@@@ -62,10 -62,22 +63,22 @@@ each of the linked worktrees.  The outp
  bare, the revision currently checked out, and the branch currently checked out
  (or 'detached HEAD' if none).
  
+ lock::
+ If a working tree is on a portable device or network share which
+ is not always mounted, lock it to prevent its administrative
+ files from being pruned automatically. This also prevents it from
+ being moved or deleted. Optionally, specify a reason for the lock
+ with `--reason`.
  prune::
  
  Prune working tree information in $GIT_DIR/worktrees.
  
+ unlock::
+ Unlock a working tree, allowing it to be pruned, moved or deleted.
  OPTIONS
  -------
  
  --expire <time>::
        With `prune`, only expire unused working trees older than <time>.
  
+ --reason <string>::
+       With `lock`, an explanation why the working tree is locked.
+ <worktree>::
+       Working trees can be identified by path, either relative or
+       absolute.
+ +
+ If the last path components in the working tree's path is unique among
+ working trees, it can be used to identify worktrees. For example if
+ you only have to working trees at "/abc/def/ghi" and "/abc/def/ggg",
+ then "ghi" or "def/ghi" is enough to point to the former working tree.
  DETAILS
  -------
  Each linked working tree has a private sub-directory in the repository's
@@@ -151,7 -175,8 +176,8 @@@ instead
  
  To prevent a $GIT_DIR/worktrees entry from being pruned (which
  can be useful in some situations, such as when the
- entry's working tree is stored on a portable device), add a file named
+ entry's working tree is stored on a portable device), use the
+ `git worktree lock` command, which adds a file named
  'locked' to the entry's directory. The file contains the reason in
  plain text. For example, if a linked working tree's `.git` file points
  to `/path/main/.git/worktrees/test-next` then a file named
@@@ -227,8 -252,6 +253,6 @@@ performed manually, such as
  - `remove` to remove a linked working tree and its administrative files (and
    warn if the working tree is dirty)
  - `mv` to move or rename a working tree and update its administrative files
- - `lock` to prevent automatic pruning of administrative files (for instance,
-   for a working tree on a portable device)
  
  GIT
  ---
diff --combined builtin/worktree.c
index cce555cbbc8a58b41789990402d756d7d4aa337a,48774211189b939591428e2d28d9e3d59ed3c1a5..5a41788edb7501ba2457b4db50f1b2472eeccc1e
@@@ -14,7 -14,9 +14,9 @@@
  static const char * const worktree_usage[] = {
        N_("git worktree add [<options>] <path> [<branch>]"),
        N_("git worktree list [<options>]"),
+       N_("git worktree lock [<options>] <path>"),
        N_("git worktree prune [<options>]"),
+       N_("git worktree unlock <path>"),
        NULL
  };
  
@@@ -110,7 -112,7 +112,7 @@@ static void prune_worktrees(void
                if (ret < 0 && errno == ENOTDIR)
                        ret = unlink(path.buf);
                if (ret)
 -                      error(_("failed to remove: %s"), strerror(errno));
 +                      error_errno(_("failed to remove '%s'"), path.buf);
        }
        closedir(dir);
        if (!show_only)
@@@ -262,7 -264,7 +264,7 @@@ static int add_worktree(const char *pat
         */
        strbuf_reset(&sb);
        strbuf_addf(&sb, "%s/HEAD", sb_repo.buf);
 -      write_file(sb.buf, sha1_to_hex(null_sha1));
 +      write_file(sb.buf, "%s", sha1_to_hex(null_sha1));
        strbuf_reset(&sb);
        strbuf_addf(&sb, "%s/commondir", sb_repo.buf);
        write_file(sb.buf, "../..");
@@@ -340,9 -342,6 +342,9 @@@ static int add(int ac, const char **av
        path = prefix_filename(prefix, strlen(prefix), av[0]);
        branch = ac < 2 ? "HEAD" : av[1];
  
 +      if (!strcmp(branch, "-"))
 +              branch = "@{-1}";
 +
        opts.force_new_branch = !!new_branch_force;
        if (opts.force_new_branch) {
                struct strbuf symref = STRBUF_INIT;
@@@ -462,6 -461,66 +464,66 @@@ static int list(int ac, const char **av
        return 0;
  }
  
+ static int lock_worktree(int ac, const char **av, const char *prefix)
+ {
+       const char *reason = "", *old_reason;
+       struct option options[] = {
+               OPT_STRING(0, "reason", &reason, N_("string"),
+                          N_("reason for locking")),
+               OPT_END()
+       };
+       struct worktree **worktrees, *wt;
+       ac = parse_options(ac, av, prefix, options, worktree_usage, 0);
+       if (ac != 1)
+               usage_with_options(worktree_usage, options);
+       worktrees = get_worktrees();
+       wt = find_worktree(worktrees, prefix, av[0]);
+       if (!wt)
+               die(_("'%s' is not a working tree"), av[0]);
+       if (is_main_worktree(wt))
+               die(_("The main working tree cannot be locked or unlocked"));
+       old_reason = is_worktree_locked(wt);
+       if (old_reason) {
+               if (*old_reason)
+                       die(_("'%s' is already locked, reason: %s"),
+                           av[0], old_reason);
+               die(_("'%s' is already locked"), av[0]);
+       }
+       write_file(git_common_path("worktrees/%s/locked", wt->id),
+                  "%s", reason);
+       free_worktrees(worktrees);
+       return 0;
+ }
+ static int unlock_worktree(int ac, const char **av, const char *prefix)
+ {
+       struct option options[] = {
+               OPT_END()
+       };
+       struct worktree **worktrees, *wt;
+       int ret;
+       ac = parse_options(ac, av, prefix, options, worktree_usage, 0);
+       if (ac != 1)
+               usage_with_options(worktree_usage, options);
+       worktrees = get_worktrees();
+       wt = find_worktree(worktrees, prefix, av[0]);
+       if (!wt)
+               die(_("'%s' is not a working tree"), av[0]);
+       if (is_main_worktree(wt))
+               die(_("The main working tree cannot be locked or unlocked"));
+       if (!is_worktree_locked(wt))
+               die(_("'%s' is not locked"), av[0]);
+       ret = unlink_or_warn(git_common_path("worktrees/%s/locked", wt->id));
+       free_worktrees(worktrees);
+       return ret;
+ }
  int cmd_worktree(int ac, const char **av, const char *prefix)
  {
        struct option options[] = {
                return prune(ac - 1, av + 1, prefix);
        if (!strcmp(av[1], "list"))
                return list(ac - 1, av + 1, prefix);
+       if (!strcmp(av[1], "lock"))
+               return lock_worktree(ac - 1, av + 1, prefix);
+       if (!strcmp(av[1], "unlock"))
+               return unlock_worktree(ac - 1, av + 1, prefix);
        usage_with_options(worktree_usage, options);
  }
index 37888f4e570e7f268bdc3e735c63f7a136774aa3,0e3841d83547e93de386979b1dac471e82fa5e9f..10f6d52254335edeb2253df5b9e63bb4d5608150
@@@ -803,50 -803,6 +803,50 @@@ __git_find_on_cmdline (
        done
  }
  
 +# Echo the value of an option set on the command line or config
 +#
 +# $1: short option name
 +# $2: long option name including =
 +# $3: list of possible values
 +# $4: config string (optional)
 +#
 +# example:
 +# result="$(__git_get_option_value "-d" "--do-something=" \
 +#     "yes no" "core.doSomething")"
 +#
 +# result is then either empty (no option set) or "yes" or "no"
 +#
 +# __git_get_option_value requires 3 arguments
 +__git_get_option_value ()
 +{
 +      local c short_opt long_opt val
 +      local result= values config_key word
 +
 +      short_opt="$1"
 +      long_opt="$2"
 +      values="$3"
 +      config_key="$4"
 +
 +      ((c = $cword - 1))
 +      while [ $c -ge 0 ]; do
 +              word="${words[c]}"
 +              for val in $values; do
 +                      if [ "$short_opt$val" = "$word" ] ||
 +                         [ "$long_opt$val"  = "$word" ]; then
 +                              result="$val"
 +                              break 2
 +                      fi
 +              done
 +              ((c--))
 +      done
 +
 +      if [ -n "$config_key" ] && [ -z "$result" ]; then
 +              result="$(git --git-dir="$(__gitdir)" config "$config_key")"
 +      fi
 +
 +      echo "$result"
 +}
 +
  __git_has_doubledash ()
  {
        local c=1
@@@ -1142,8 -1098,6 +1142,8 @@@ _git_clone (
        esac
  }
  
 +__git_untracked_file_modes="all no normal"
 +
  _git_commit ()
  {
        case "$prev" in
                return
                ;;
        --untracked-files=*)
 -              __gitcomp "all no normal" "" "${cur##--untracked-files=}"
 +              __gitcomp "$__git_untracked_file_modes" "" "${cur##--untracked-files=}"
                return
                ;;
        --*)
@@@ -1826,56 -1780,6 +1826,56 @@@ _git_stage (
        _git_add
  }
  
 +_git_status ()
 +{
 +      local complete_opt
 +      local untracked_state
 +
 +      case "$cur" in
 +      --ignore-submodules=*)
 +              __gitcomp "none untracked dirty all" "" "${cur##--ignore-submodules=}"
 +              return
 +              ;;
 +      --untracked-files=*)
 +              __gitcomp "$__git_untracked_file_modes" "" "${cur##--untracked-files=}"
 +              return
 +              ;;
 +      --column=*)
 +              __gitcomp "
 +                      always never auto column row plain dense nodense
 +                      " "" "${cur##--column=}"
 +              return
 +              ;;
 +      --*)
 +              __gitcomp "
 +                      --short --branch --porcelain --long --verbose
 +                      --untracked-files= --ignore-submodules= --ignored
 +                      --column= --no-column
 +                      "
 +              return
 +              ;;
 +      esac
 +
 +      untracked_state="$(__git_get_option_value "-u" "--untracked-files=" \
 +              "$__git_untracked_file_modes" "status.showUntrackedFiles")"
 +
 +      case "$untracked_state" in
 +      no)
 +              # --ignored option does not matter
 +              complete_opt=
 +              ;;
 +      all|normal|*)
 +              complete_opt="--cached --directory --no-empty-directory --others"
 +
 +              if [ -n "$(__git_find_on_cmdline "--ignored")" ]; then
 +                      complete_opt="$complete_opt --ignored --exclude=*"
 +              fi
 +              ;;
 +      esac
 +
 +      __git_complete_index_file "$complete_opt"
 +}
 +
  __git_config_get_set_variables ()
  {
        local prevword word config_file= c=$cword
@@@ -2693,7 -2597,7 +2693,7 @@@ _git_whatchanged (
  
  _git_worktree ()
  {
-       local subcommands="add list prune"
+       local subcommands="add list lock prune unlock"
        local subcommand="$(__git_find_on_cmdline "$subcommands")"
        if [ -z "$subcommand" ]; then
                __gitcomp "$subcommands"
                list,--*)
                        __gitcomp "--porcelain"
                        ;;
+               lock,--*)
+                       __gitcomp "--reason"
+                       ;;
                prune,--*)
                        __gitcomp "--dry-run --expire --verbose"
                        ;;
diff --combined worktree.c
index b819baf0cda97c9b596e83d8146f1b42891efb01,2107c0625daa892299c7b2695a9c9b9acef16e4b..5acfe4cd64967d86462f3958433f3d2f844ead51
@@@ -13,6 -13,7 +13,7 @@@ void free_worktrees(struct worktree **w
                free(worktrees[i]->path);
                free(worktrees[i]->id);
                free(worktrees[i]->head_ref);
+               free(worktrees[i]->lock_reason);
                free(worktrees[i]);
        }
        free (worktrees);
@@@ -20,7 -21,7 +21,7 @@@
  
  /*
   * read 'path_to_ref' into 'ref'.  Also if is_detached is not NULL,
 - * set is_detached to 1 (0) if the ref is detatched (is not detached).
 + * set is_detached to 1 (0) if the ref is detached (is not detached).
   *
   * $GIT_COMMON_DIR/$symref (e.g. HEAD) is practically outside $GIT_DIR so
   * for linked worktrees, `resolve_ref_unsafe()` won't work (it uses
@@@ -80,7 -81,7 +81,7 @@@ static struct worktree *get_main_worktr
        int is_bare = 0;
        int is_detached = 0;
  
 -      strbuf_addstr(&worktree_path, absolute_path(get_git_common_dir()));
 +      strbuf_add_absolute_path(&worktree_path, get_git_common_dir());
        is_bare = !strbuf_strip_suffix(&worktree_path, "/.git");
        if (is_bare)
                strbuf_strip_suffix(&worktree_path, "/.");
@@@ -98,6 -99,8 +99,8 @@@
        worktree->is_detached = is_detached;
        worktree->is_current = 0;
        add_head_info(&head_ref, worktree);
+       worktree->lock_reason = NULL;
+       worktree->lock_reason_valid = 0;
  
  done:
        strbuf_release(&path);
@@@ -125,7 -128,7 +128,7 @@@ static struct worktree *get_linked_work
        strbuf_rtrim(&worktree_path);
        if (!strbuf_strip_suffix(&worktree_path, "/.git")) {
                strbuf_reset(&worktree_path);
 -              strbuf_addstr(&worktree_path, absolute_path("."));
 +              strbuf_add_absolute_path(&worktree_path, ".");
                strbuf_strip_suffix(&worktree_path, "/.");
        }
  
        worktree->is_detached = is_detached;
        worktree->is_current = 0;
        add_head_info(&head_ref, worktree);
+       worktree->lock_reason = NULL;
+       worktree->lock_reason_valid = 0;
  
  done:
        strbuf_release(&path);
@@@ -214,6 -219,78 +219,78 @@@ const char *get_worktree_git_dir(const 
                return git_common_path("worktrees/%s", wt->id);
  }
  
+ static struct worktree *find_worktree_by_suffix(struct worktree **list,
+                                               const char *suffix)
+ {
+       struct worktree *found = NULL;
+       int nr_found = 0, suffixlen;
+       suffixlen = strlen(suffix);
+       if (!suffixlen)
+               return NULL;
+       for (; *list && nr_found < 2; list++) {
+               const char      *path    = (*list)->path;
+               int              pathlen = strlen(path);
+               int              start   = pathlen - suffixlen;
+               /* suffix must start at directory boundary */
+               if ((!start || (start > 0 && is_dir_sep(path[start - 1]))) &&
+                   !fspathcmp(suffix, path + start)) {
+                       found = *list;
+                       nr_found++;
+               }
+       }
+       return nr_found == 1 ? found : NULL;
+ }
+ struct worktree *find_worktree(struct worktree **list,
+                              const char *prefix,
+                              const char *arg)
+ {
+       struct worktree *wt;
+       char *path;
+       if ((wt = find_worktree_by_suffix(list, arg)))
+               return wt;
+       arg = prefix_filename(prefix, strlen(prefix), arg);
+       path = xstrdup(real_path(arg));
+       for (; *list; list++)
+               if (!fspathcmp(path, real_path((*list)->path)))
+                       break;
+       free(path);
+       return *list;
+ }
+ int is_main_worktree(const struct worktree *wt)
+ {
+       return !wt->id;
+ }
+ const char *is_worktree_locked(struct worktree *wt)
+ {
+       assert(!is_main_worktree(wt));
+       if (!wt->lock_reason_valid) {
+               struct strbuf path = STRBUF_INIT;
+               strbuf_addstr(&path, worktree_git_path(wt, "locked"));
+               if (file_exists(path.buf)) {
+                       struct strbuf lock_reason = STRBUF_INIT;
+                       if (strbuf_read_file(&lock_reason, path.buf, 0) < 0)
+                               die_errno(_("failed to read '%s'"), path.buf);
+                       strbuf_trim(&lock_reason);
+                       wt->lock_reason = strbuf_detach(&lock_reason, NULL);
+               } else
+                       wt->lock_reason = NULL;
+               wt->lock_reason_valid = 1;
+               strbuf_release(&path);
+       }
+       return wt->lock_reason;
+ }
  int is_worktree_being_rebased(const struct worktree *wt,
                              const char *target)
  {