Skip to content

Commit 2c608e0

Browse files
committed
Merge branch 'nd/worktree-lock'
"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()
2 parents d0b6966 + 080739b commit 2c608e0

6 files changed

Lines changed: 260 additions & 7 deletions

File tree

Documentation/git-worktree.txt

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ SYNOPSIS
1111
[verse]
1212
'git worktree add' [-f] [--detach] [--checkout] [-b <new-branch>] <path> [<branch>]
1313
'git worktree list' [--porcelain]
14+
'git worktree lock' [--reason <string>] <worktree>
1415
'git worktree prune' [-n] [-v] [--expire <expire>]
16+
'git worktree unlock' <worktree>
1517

1618
DESCRIPTION
1719
-----------
@@ -38,9 +40,8 @@ section "DETAILS" for more information.
3840

3941
If a linked working tree is stored on a portable device or network share
4042
which is not always mounted, you can prevent its administrative files from
41-
being pruned by creating a file named 'locked' alongside the other
42-
administrative files, optionally containing a plain text reason that
43-
pruning should be suppressed. See section "DETAILS" for more information.
43+
being pruned by issuing the `git worktree lock` command, optionally
44+
specifying `--reason` to explain why the working tree is locked.
4445

4546
COMMANDS
4647
--------
@@ -62,10 +63,22 @@ each of the linked worktrees. The output details include if the worktree is
6263
bare, the revision currently checked out, and the branch currently checked out
6364
(or 'detached HEAD' if none).
6465

66+
lock::
67+
68+
If a working tree is on a portable device or network share which
69+
is not always mounted, lock it to prevent its administrative
70+
files from being pruned automatically. This also prevents it from
71+
being moved or deleted. Optionally, specify a reason for the lock
72+
with `--reason`.
73+
6574
prune::
6675

6776
Prune working tree information in $GIT_DIR/worktrees.
6877

78+
unlock::
79+
80+
Unlock a working tree, allowing it to be pruned, moved or deleted.
81+
6982
OPTIONS
7083
-------
7184

@@ -111,6 +124,18 @@ OPTIONS
111124
--expire <time>::
112125
With `prune`, only expire unused working trees older than <time>.
113126

127+
--reason <string>::
128+
With `lock`, an explanation why the working tree is locked.
129+
130+
<worktree>::
131+
Working trees can be identified by path, either relative or
132+
absolute.
133+
+
134+
If the last path components in the working tree's path is unique among
135+
working trees, it can be used to identify worktrees. For example if
136+
you only have to working trees at "/abc/def/ghi" and "/abc/def/ggg",
137+
then "ghi" or "def/ghi" is enough to point to the former working tree.
138+
114139
DETAILS
115140
-------
116141
Each linked working tree has a private sub-directory in the repository's
@@ -151,7 +176,8 @@ instead.
151176

152177
To prevent a $GIT_DIR/worktrees entry from being pruned (which
153178
can be useful in some situations, such as when the
154-
entry's working tree is stored on a portable device), add a file named
179+
entry's working tree is stored on a portable device), use the
180+
`git worktree lock` command, which adds a file named
155181
'locked' to the entry's directory. The file contains the reason in
156182
plain text. For example, if a linked working tree's `.git` file points
157183
to `/path/main/.git/worktrees/test-next` then a file named
@@ -227,8 +253,6 @@ performed manually, such as:
227253
- `remove` to remove a linked working tree and its administrative files (and
228254
warn if the working tree is dirty)
229255
- `mv` to move or rename a working tree and update its administrative files
230-
- `lock` to prevent automatic pruning of administrative files (for instance,
231-
for a working tree on a portable device)
232256

233257
GIT
234258
---

builtin/worktree.c

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
static const char * const worktree_usage[] = {
1515
N_("git worktree add [<options>] <path> [<branch>]"),
1616
N_("git worktree list [<options>]"),
17+
N_("git worktree lock [<options>] <path>"),
1718
N_("git worktree prune [<options>]"),
19+
N_("git worktree unlock <path>"),
1820
NULL
1921
};
2022

@@ -462,6 +464,66 @@ static int list(int ac, const char **av, const char *prefix)
462464
return 0;
463465
}
464466

467+
static int lock_worktree(int ac, const char **av, const char *prefix)
468+
{
469+
const char *reason = "", *old_reason;
470+
struct option options[] = {
471+
OPT_STRING(0, "reason", &reason, N_("string"),
472+
N_("reason for locking")),
473+
OPT_END()
474+
};
475+
struct worktree **worktrees, *wt;
476+
477+
ac = parse_options(ac, av, prefix, options, worktree_usage, 0);
478+
if (ac != 1)
479+
usage_with_options(worktree_usage, options);
480+
481+
worktrees = get_worktrees();
482+
wt = find_worktree(worktrees, prefix, av[0]);
483+
if (!wt)
484+
die(_("'%s' is not a working tree"), av[0]);
485+
if (is_main_worktree(wt))
486+
die(_("The main working tree cannot be locked or unlocked"));
487+
488+
old_reason = is_worktree_locked(wt);
489+
if (old_reason) {
490+
if (*old_reason)
491+
die(_("'%s' is already locked, reason: %s"),
492+
av[0], old_reason);
493+
die(_("'%s' is already locked"), av[0]);
494+
}
495+
496+
write_file(git_common_path("worktrees/%s/locked", wt->id),
497+
"%s", reason);
498+
free_worktrees(worktrees);
499+
return 0;
500+
}
501+
502+
static int unlock_worktree(int ac, const char **av, const char *prefix)
503+
{
504+
struct option options[] = {
505+
OPT_END()
506+
};
507+
struct worktree **worktrees, *wt;
508+
int ret;
509+
510+
ac = parse_options(ac, av, prefix, options, worktree_usage, 0);
511+
if (ac != 1)
512+
usage_with_options(worktree_usage, options);
513+
514+
worktrees = get_worktrees();
515+
wt = find_worktree(worktrees, prefix, av[0]);
516+
if (!wt)
517+
die(_("'%s' is not a working tree"), av[0]);
518+
if (is_main_worktree(wt))
519+
die(_("The main working tree cannot be locked or unlocked"));
520+
if (!is_worktree_locked(wt))
521+
die(_("'%s' is not locked"), av[0]);
522+
ret = unlink_or_warn(git_common_path("worktrees/%s/locked", wt->id));
523+
free_worktrees(worktrees);
524+
return ret;
525+
}
526+
465527
int cmd_worktree(int ac, const char **av, const char *prefix)
466528
{
467529
struct option options[] = {
@@ -478,5 +540,9 @@ int cmd_worktree(int ac, const char **av, const char *prefix)
478540
return prune(ac - 1, av + 1, prefix);
479541
if (!strcmp(av[1], "list"))
480542
return list(ac - 1, av + 1, prefix);
543+
if (!strcmp(av[1], "lock"))
544+
return lock_worktree(ac - 1, av + 1, prefix);
545+
if (!strcmp(av[1], "unlock"))
546+
return unlock_worktree(ac - 1, av + 1, prefix);
481547
usage_with_options(worktree_usage, options);
482548
}

contrib/completion/git-completion.bash

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2693,7 +2693,7 @@ _git_whatchanged ()
26932693

26942694
_git_worktree ()
26952695
{
2696-
local subcommands="add list prune"
2696+
local subcommands="add list lock prune unlock"
26972697
local subcommand="$(__git_find_on_cmdline "$subcommands")"
26982698
if [ -z "$subcommand" ]; then
26992699
__gitcomp "$subcommands"
@@ -2705,6 +2705,9 @@ _git_worktree ()
27052705
list,--*)
27062706
__gitcomp "--porcelain"
27072707
;;
2708+
lock,--*)
2709+
__gitcomp "--reason"
2710+
;;
27082711
prune,--*)
27092712
__gitcomp "--dry-run --expire --verbose"
27102713
;;

t/t2028-worktree-move.sh

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#!/bin/sh
2+
3+
test_description='test git worktree move, remove, lock and unlock'
4+
5+
. ./test-lib.sh
6+
7+
test_expect_success 'setup' '
8+
test_commit init &&
9+
git worktree add source &&
10+
git worktree list --porcelain | grep "^worktree" >actual &&
11+
cat <<-EOF >expected &&
12+
worktree $(pwd)
13+
worktree $(pwd)/source
14+
EOF
15+
test_cmp expected actual
16+
'
17+
18+
test_expect_success 'lock main worktree' '
19+
test_must_fail git worktree lock .
20+
'
21+
22+
test_expect_success 'lock linked worktree' '
23+
git worktree lock --reason hahaha source &&
24+
echo hahaha >expected &&
25+
test_cmp expected .git/worktrees/source/locked
26+
'
27+
28+
test_expect_success 'lock linked worktree from another worktree' '
29+
rm .git/worktrees/source/locked &&
30+
git worktree add elsewhere &&
31+
git -C elsewhere worktree lock --reason hahaha ../source &&
32+
echo hahaha >expected &&
33+
test_cmp expected .git/worktrees/source/locked
34+
'
35+
36+
test_expect_success 'lock worktree twice' '
37+
test_must_fail git worktree lock source &&
38+
echo hahaha >expected &&
39+
test_cmp expected .git/worktrees/source/locked
40+
'
41+
42+
test_expect_success 'lock worktree twice (from the locked worktree)' '
43+
test_must_fail git -C source worktree lock . &&
44+
echo hahaha >expected &&
45+
test_cmp expected .git/worktrees/source/locked
46+
'
47+
48+
test_expect_success 'unlock main worktree' '
49+
test_must_fail git worktree unlock .
50+
'
51+
52+
test_expect_success 'unlock linked worktree' '
53+
git worktree unlock source &&
54+
test_path_is_missing .git/worktrees/source/locked
55+
'
56+
57+
test_expect_success 'unlock worktree twice' '
58+
test_must_fail git worktree unlock source &&
59+
test_path_is_missing .git/worktrees/source/locked
60+
'
61+
62+
test_done

worktree.c

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ void free_worktrees(struct worktree **worktrees)
1313
free(worktrees[i]->path);
1414
free(worktrees[i]->id);
1515
free(worktrees[i]->head_ref);
16+
free(worktrees[i]->lock_reason);
1617
free(worktrees[i]);
1718
}
1819
free (worktrees);
@@ -98,6 +99,8 @@ static struct worktree *get_main_worktree(void)
9899
worktree->is_detached = is_detached;
99100
worktree->is_current = 0;
100101
add_head_info(&head_ref, worktree);
102+
worktree->lock_reason = NULL;
103+
worktree->lock_reason_valid = 0;
101104

102105
done:
103106
strbuf_release(&path);
@@ -143,6 +146,8 @@ static struct worktree *get_linked_worktree(const char *id)
143146
worktree->is_detached = is_detached;
144147
worktree->is_current = 0;
145148
add_head_info(&head_ref, worktree);
149+
worktree->lock_reason = NULL;
150+
worktree->lock_reason_valid = 0;
146151

147152
done:
148153
strbuf_release(&path);
@@ -214,6 +219,78 @@ const char *get_worktree_git_dir(const struct worktree *wt)
214219
return git_common_path("worktrees/%s", wt->id);
215220
}
216221

222+
static struct worktree *find_worktree_by_suffix(struct worktree **list,
223+
const char *suffix)
224+
{
225+
struct worktree *found = NULL;
226+
int nr_found = 0, suffixlen;
227+
228+
suffixlen = strlen(suffix);
229+
if (!suffixlen)
230+
return NULL;
231+
232+
for (; *list && nr_found < 2; list++) {
233+
const char *path = (*list)->path;
234+
int pathlen = strlen(path);
235+
int start = pathlen - suffixlen;
236+
237+
/* suffix must start at directory boundary */
238+
if ((!start || (start > 0 && is_dir_sep(path[start - 1]))) &&
239+
!fspathcmp(suffix, path + start)) {
240+
found = *list;
241+
nr_found++;
242+
}
243+
}
244+
return nr_found == 1 ? found : NULL;
245+
}
246+
247+
struct worktree *find_worktree(struct worktree **list,
248+
const char *prefix,
249+
const char *arg)
250+
{
251+
struct worktree *wt;
252+
char *path;
253+
254+
if ((wt = find_worktree_by_suffix(list, arg)))
255+
return wt;
256+
257+
arg = prefix_filename(prefix, strlen(prefix), arg);
258+
path = xstrdup(real_path(arg));
259+
for (; *list; list++)
260+
if (!fspathcmp(path, real_path((*list)->path)))
261+
break;
262+
free(path);
263+
return *list;
264+
}
265+
266+
int is_main_worktree(const struct worktree *wt)
267+
{
268+
return !wt->id;
269+
}
270+
271+
const char *is_worktree_locked(struct worktree *wt)
272+
{
273+
assert(!is_main_worktree(wt));
274+
275+
if (!wt->lock_reason_valid) {
276+
struct strbuf path = STRBUF_INIT;
277+
278+
strbuf_addstr(&path, worktree_git_path(wt, "locked"));
279+
if (file_exists(path.buf)) {
280+
struct strbuf lock_reason = STRBUF_INIT;
281+
if (strbuf_read_file(&lock_reason, path.buf, 0) < 0)
282+
die_errno(_("failed to read '%s'"), path.buf);
283+
strbuf_trim(&lock_reason);
284+
wt->lock_reason = strbuf_detach(&lock_reason, NULL);
285+
} else
286+
wt->lock_reason = NULL;
287+
wt->lock_reason_valid = 1;
288+
strbuf_release(&path);
289+
}
290+
291+
return wt->lock_reason;
292+
}
293+
217294
int is_worktree_being_rebased(const struct worktree *wt,
218295
const char *target)
219296
{

worktree.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ struct worktree {
55
char *path;
66
char *id;
77
char *head_ref;
8+
char *lock_reason; /* internal use */
89
unsigned char head_sha1[20];
910
int is_detached;
1011
int is_bare;
1112
int is_current;
13+
int lock_reason_valid;
1214
};
1315

1416
/* Functions for acting on the information about worktrees. */
@@ -29,6 +31,25 @@ extern struct worktree **get_worktrees(void);
2931
*/
3032
extern const char *get_worktree_git_dir(const struct worktree *wt);
3133

34+
/*
35+
* Search a worktree that can be unambiguously identified by
36+
* "arg". "prefix" must not be NULL.
37+
*/
38+
extern struct worktree *find_worktree(struct worktree **list,
39+
const char *prefix,
40+
const char *arg);
41+
42+
/*
43+
* Return true if the given worktree is the main one.
44+
*/
45+
extern int is_main_worktree(const struct worktree *wt);
46+
47+
/*
48+
* Return the reason string if the given worktree is locked or NULL
49+
* otherwise.
50+
*/
51+
extern const char *is_worktree_locked(struct worktree *wt);
52+
3253
/*
3354
* Free up the memory for worktree(s)
3455
*/

0 commit comments

Comments
 (0)