Skip to content

Commit 32561f5

Browse files
committed
Merge branch 'dt/notes-multiple'
When linked worktree is used, simultaneous "notes merge" instances for the same ref in refs/notes/* are prevented from stomping on each other. * dt/notes-multiple: notes: handle multiple worktrees worktrees: add find_shared_symref
2 parents 5d5be81 + b02e859 commit 32561f5

File tree

4 files changed

+120
-12
lines changed

4 files changed

+120
-12
lines changed

branch.c

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -311,21 +311,23 @@ void remove_branch_state(void)
311311
unlink(git_path_squash_msg());
312312
}
313313

314-
static void check_linked_checkout(const char *branch, const char *id)
314+
static char *find_linked_symref(const char *symref, const char *branch,
315+
const char *id)
315316
{
316317
struct strbuf sb = STRBUF_INIT;
317318
struct strbuf path = STRBUF_INIT;
318319
struct strbuf gitdir = STRBUF_INIT;
320+
char *existing = NULL;
319321

320322
/*
321-
* $GIT_COMMON_DIR/HEAD is practically outside
322-
* $GIT_DIR so resolve_ref_unsafe() won't work (it
323-
* uses git_path). Parse the ref ourselves.
323+
* $GIT_COMMON_DIR/$symref (e.g. HEAD) is practically outside
324+
* $GIT_DIR so resolve_ref_unsafe() won't work (it uses
325+
* git_path). Parse the ref ourselves.
324326
*/
325327
if (id)
326-
strbuf_addf(&path, "%s/worktrees/%s/HEAD", get_git_common_dir(), id);
328+
strbuf_addf(&path, "%s/worktrees/%s/%s", get_git_common_dir(), id, symref);
327329
else
328-
strbuf_addf(&path, "%s/HEAD", get_git_common_dir());
330+
strbuf_addf(&path, "%s/%s", get_git_common_dir(), symref);
329331

330332
if (!strbuf_readlink(&sb, path.buf, 0)) {
331333
if (!starts_with(sb.buf, "refs/") ||
@@ -347,33 +349,53 @@ static void check_linked_checkout(const char *branch, const char *id)
347349
strbuf_rtrim(&gitdir);
348350
} else
349351
strbuf_addstr(&gitdir, get_git_common_dir());
350-
skip_prefix(branch, "refs/heads/", &branch);
351352
strbuf_strip_suffix(&gitdir, ".git");
352-
die(_("'%s' is already checked out at '%s'"), branch, gitdir.buf);
353+
354+
existing = strbuf_detach(&gitdir, NULL);
353355
done:
354356
strbuf_release(&path);
355357
strbuf_release(&sb);
356358
strbuf_release(&gitdir);
359+
360+
return existing;
357361
}
358362

359-
void die_if_checked_out(const char *branch)
363+
char *find_shared_symref(const char *symref, const char *target)
360364
{
361365
struct strbuf path = STRBUF_INIT;
362366
DIR *dir;
363367
struct dirent *d;
368+
char *existing;
364369

365-
check_linked_checkout(branch, NULL);
370+
if ((existing = find_linked_symref(symref, target, NULL)))
371+
return existing;
366372

367373
strbuf_addf(&path, "%s/worktrees", get_git_common_dir());
368374
dir = opendir(path.buf);
369375
strbuf_release(&path);
370376
if (!dir)
371-
return;
377+
return NULL;
372378

373379
while ((d = readdir(dir)) != NULL) {
374380
if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, ".."))
375381
continue;
376-
check_linked_checkout(branch, d->d_name);
382+
existing = find_linked_symref(symref, target, d->d_name);
383+
if (existing)
384+
goto done;
377385
}
386+
done:
378387
closedir(dir);
388+
389+
return existing;
390+
}
391+
392+
void die_if_checked_out(const char *branch)
393+
{
394+
char *existing;
395+
396+
existing = find_shared_symref("HEAD", branch);
397+
if (existing) {
398+
skip_prefix(branch, "refs/heads/", &branch);
399+
die(_("'%s' is already checked out at '%s'"), branch, existing);
400+
}
379401
}

branch.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,12 @@ extern int read_branch_desc(struct strbuf *, const char *branch_name);
5959
*/
6060
extern void die_if_checked_out(const char *branch);
6161

62+
/*
63+
* Check if a per-worktree symref points to a ref in the main worktree
64+
* or any linked worktree, and return the path to the exising worktree
65+
* if it is. Returns NULL if there is no existing ref. The caller is
66+
* responsible for freeing the returned path.
67+
*/
68+
extern char *find_shared_symref(const char *symref, const char *target);
69+
6270
#endif

builtin/notes.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include "string-list.h"
2020
#include "notes-merge.h"
2121
#include "notes-utils.h"
22+
#include "branch.h"
2223

2324
static const char * const git_notes_usage[] = {
2425
N_("git notes [--ref <notes-ref>] [list [<object>]]"),
@@ -825,10 +826,15 @@ static int merge(int argc, const char **argv, const char *prefix)
825826
update_ref(msg.buf, default_notes_ref(), result_sha1, NULL,
826827
0, UPDATE_REFS_DIE_ON_ERR);
827828
else { /* Merge has unresolved conflicts */
829+
char *existing;
828830
/* Update .git/NOTES_MERGE_PARTIAL with partial merge result */
829831
update_ref(msg.buf, "NOTES_MERGE_PARTIAL", result_sha1, NULL,
830832
0, UPDATE_REFS_DIE_ON_ERR);
831833
/* Store ref-to-be-updated into .git/NOTES_MERGE_REF */
834+
existing = find_shared_symref("NOTES_MERGE_REF", default_notes_ref());
835+
if (existing)
836+
die(_("A notes merge into %s is already in-progress at %s"),
837+
default_notes_ref(), existing);
832838
if (create_symref("NOTES_MERGE_REF", default_notes_ref(), NULL))
833839
die("Failed to store link to current notes ref (%s)",
834840
default_notes_ref());

t/t3320-notes-merge-worktrees.sh

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#!/bin/sh
2+
#
3+
# Copyright (c) 2015 Twitter, Inc
4+
#
5+
6+
test_description='Test merging of notes trees in multiple worktrees'
7+
8+
. ./test-lib.sh
9+
10+
test_expect_success 'setup commit' '
11+
test_commit tantrum
12+
'
13+
14+
commit_tantrum=$(git rev-parse tantrum^{commit})
15+
16+
test_expect_success 'setup notes ref (x)' '
17+
git config core.notesRef refs/notes/x &&
18+
git notes add -m "x notes on tantrum" tantrum
19+
'
20+
21+
test_expect_success 'setup local branch (y)' '
22+
git update-ref refs/notes/y refs/notes/x &&
23+
git config core.notesRef refs/notes/y &&
24+
git notes remove tantrum
25+
'
26+
27+
test_expect_success 'setup remote branch (z)' '
28+
git update-ref refs/notes/z refs/notes/x &&
29+
git config core.notesRef refs/notes/z &&
30+
git notes add -f -m "conflicting notes on tantrum" tantrum
31+
'
32+
33+
test_expect_success 'modify notes ref ourselves (x)' '
34+
git config core.notesRef refs/notes/x &&
35+
git notes add -f -m "more conflicting notes on tantrum" tantrum
36+
'
37+
38+
test_expect_success 'create some new worktrees' '
39+
git worktree add -b newbranch worktree master &&
40+
git worktree add -b newbranch2 worktree2 master
41+
'
42+
43+
test_expect_success 'merge z into y fails and sets NOTES_MERGE_REF' '
44+
git config core.notesRef refs/notes/y &&
45+
test_must_fail git notes merge z &&
46+
echo "ref: refs/notes/y" >expect &&
47+
test_cmp .git/NOTES_MERGE_REF expect
48+
'
49+
50+
test_expect_success 'merge z into y while mid-merge in another workdir fails' '
51+
(
52+
cd worktree &&
53+
git config core.notesRef refs/notes/y &&
54+
test_must_fail git notes merge z 2>err &&
55+
grep "A notes merge into refs/notes/y is already in-progress at" err
56+
) &&
57+
test_path_is_missing .git/worktrees/worktree/NOTES_MERGE_REF
58+
'
59+
60+
test_expect_success 'merge z into x while mid-merge on y succeeds' '
61+
(
62+
cd worktree2 &&
63+
git config core.notesRef refs/notes/x &&
64+
test_must_fail git notes merge z 2>&1 >out &&
65+
grep "Automatic notes merge failed" out &&
66+
grep -v "A notes merge into refs/notes/x is already in-progress in" out
67+
) &&
68+
echo "ref: refs/notes/x" >expect &&
69+
test_cmp .git/worktrees/worktree2/NOTES_MERGE_REF expect
70+
'
71+
72+
test_done

0 commit comments

Comments
 (0)