Subject: [PATCH v2 0/5] worktree lock/unlock

From: Nguyễn Thái Ngọc Duy <[email-masked]>
Date: Sun, 22 May 2016 17:43:36 +0700

This should address all of Eric's comments (thanks!). An extra change
I made is free_worktrees() at the end of {,un}lock_worktree() to avoid
leaking. This series depends on nd/worktree-cleanup-post-head-protection.

Interdiff
-- 8< --
diff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt
index 9ac1129..e0f2605 100644
--- a/Documentation/git-worktree.txt
+++ b/Documentation/git-worktree.txt
@@ -40,9 +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
 --------
@@ -65,9 +64,11 @@ bare, the revision currently checked out, and the branch currently checked out
 
 lock::
 
-When a worktree is locked, it cannot be pruned, moved or deleted. For
-example, if the worktree is on portable device that is not available
-when "git worktree <command>" is executed.
+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::
 
@@ -123,7 +124,7 @@ OPTIONS
 	With `prune`, only expire unused working trees older than <time>.
 
 --reason <string>:
-	An explanation why the worktree is locked.
+	With `lock`, an explanation why the worktree is locked.
 
 DETAILS
 -------
@@ -165,7 +166,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` comamnd, 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
diff --git a/builtin/worktree.c b/builtin/worktree.c
index 53e5f5a..da9f771 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -482,6 +482,7 @@ static int lock_worktree(int ac, const char **av, const char *prefix)
 
 	worktrees = get_worktrees();
 	wt = find_worktree_by_path(worktrees, dst.buf);
+	strbuf_release(&dst);
 	if (!wt)
 		die(_("'%s' is not a working directory"), av[0]);
 	if (is_main_worktree(wt))
@@ -491,11 +492,12 @@ static int lock_worktree(int ac, const char **av, const char *prefix)
 	if (old_reason) {
 		if (*old_reason)
 			die(_("already locked, reason: %s"), old_reason);
-		die(_("already locked, no reason"));
+		die(_("already locked"));
 	}
 
 	write_file(git_common_path("worktrees/%s/locked", wt->id),
 		   "%s", reason);
+	free_worktrees(worktrees);
 	return 0;
 }
 
@@ -506,6 +508,7 @@ static int unlock_worktree(int ac, const char **av, const char *prefix)
 	};
 	struct worktree **worktrees, *wt;
 	struct strbuf dst = STRBUF_INIT;
+	int ret;
 
 	ac = parse_options(ac, av, prefix, options, worktree_usage, 0);
 	if (ac != 1)
@@ -517,14 +520,16 @@ static int unlock_worktree(int ac, const char **av, const char *prefix)
 
 	worktrees = get_worktrees();
 	wt = find_worktree_by_path(worktrees, dst.buf);
+	strbuf_release(&dst);
 	if (!wt)
 		die(_("'%s' is not a working directory"), av[0]);
 	if (is_main_worktree(wt))
 		die(_("'%s' is a main working directory"), av[0]);
 	if (!is_worktree_locked(wt))
 		die(_("not locked"));
-
-	return unlink_or_warn(git_common_path("worktrees/%s/locked", wt->id));
+	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)
diff --git a/t/t2028-worktree-move.sh b/t/t2028-worktree-move.sh
index f4b2816..1927537 100755
--- a/t/t2028-worktree-move.sh
+++ b/t/t2028-worktree-move.sh
@@ -25,12 +25,32 @@ test_expect_success 'lock linked worktree' '
 	test_cmp expected .git/worktrees/source/locked
 '
 
+test_expect_success 'lock linked worktree from another worktree' '
+	rm .git/worktrees/source/locked &&
+	git worktree add elsewhere &&
+	(
+		cd elsewhere &&
+		git worktree lock --reason hahaha ../source
+	) &&
+	echo hahaha >expected &&
+	test_cmp expected .git/worktrees/source/locked
+'
+
 test_expect_success 'lock worktree twice' '
 	test_must_fail git worktree lock source &&
 	echo hahaha >expected &&
 	test_cmp expected .git/worktrees/source/locked
 '
 
+test_expect_success 'lock worktree twice (from the locked worktree)' '
+	(
+		cd source &&
+		test_must_fail git worktree lock .
+	) &&
+	echo hahaha >expected &&
+	test_cmp expected .git/worktrees/source/locked
+'
+
 test_expect_success 'unlock main worktree' '
 	test_must_fail git worktree unlock .
 '
diff --git a/worktree.c b/worktree.c
index a9cfbb3..cb30af3 100644
--- a/worktree.c
+++ b/worktree.c
@@ -218,38 +218,37 @@ struct worktree *find_worktree_by_path(struct worktree **list,
 				       const char *path_)
 {
 	char *path = xstrdup(real_path(path_));
-	struct worktree *wt = NULL;
 
-	while (*list) {
-		wt = *list++;
-		if (!fspathcmp(path, real_path(wt->path)))
+	for (; *list; list++)
+		if (!fspathcmp(path, real_path((*list)->path)))
 			break;
-		wt = NULL;
-	}
 	free(path);
-	return wt;
+	return *list;
 }
 
 int is_main_worktree(const struct worktree *wt)
 {
-	return wt && !wt->id;
+	return !wt->id;
 }
 
 const char *is_worktree_locked(const struct worktree *wt)
 {
 	static struct strbuf sb = STRBUF_INIT;
+	struct strbuf path = STRBUF_INIT;
+
+	strbuf_git_common_path(&path, "worktrees/%s/locked", wt->id);
 
-	if (!file_exists(git_common_path("worktrees/%s/locked", wt->id)))
+	if (!file_exists(path.buf)) {
+		strbuf_release(&path);
 		return NULL;
+	}
 
 	strbuf_reset(&sb);
-	if (strbuf_read_file(&sb,
-			     git_common_path("worktrees/%s/locked", wt->id),
-			     0) < 0)
-		die_errno(_("failed to read '%s'"),
-			  git_common_path("worktrees/%s/locked", wt->id));
+	if (strbuf_read_file(&sb, path.buf, 0) < 0)
+		die_errno(_("failed to read '%s'"), path.buf);
+	strbuf_release(&path);
 
-	strbuf_rtrim(&sb);
+	strbuf_trim(&sb);
 	return sb.buf;
 }
 
diff --git a/worktree.h b/worktree.h
index 19a6790..3a780c2 100644
--- a/worktree.h
+++ b/worktree.h
@@ -42,7 +42,7 @@ extern int is_main_worktree(const struct worktree *wt);
 
 /*
  * Return the reason string if the given worktree is locked. Return
- * NULL otherwise.
+ * NULL otherwise. Return value is only valid until the next invocation.
  */
 extern const char *is_worktree_locked(const struct worktree *wt);
-- 8< --

Nguyễn Thái Ngọc Duy (5):
  worktree.c: add find_worktree_by_path()
  worktree.c: add is_main_worktree()
  worktree.c: add is_worktree_locked()
  worktree: add "lock" command
  worktree: add "unlock" command

 Documentation/git-worktree.txt         | 27 +++++++++---
 builtin/worktree.c                     | 77 ++++++++++++++++++++++++++++++++++
 contrib/completion/git-completion.bash |  5 ++-
 t/t2028-worktree-move.sh (new +x)      | 68 ++++++++++++++++++++++++++++++
 worktree.c                             | 38 +++++++++++++++++
 worktree.h                             | 17 ++++++++
 6 files changed, 225 insertions(+), 7 deletions(-)
 create mode 100755 t/t2028-worktree-move.sh

-- 
2.8.2.524.g6ff3d78