refs: update worktree HEADs when renaming branches Whenever we rename a branch, we update the repository's symbolic HEAD reference if it currently points to the branch that is to be renamed. But with the introduction of worktrees, we also have to iterate over all HEADs of linked worktrees to adjust them. Do so.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
diff --git a/src/refs.c b/src/refs.c
index a53d094..31410b7 100644
--- a/src/refs.c
+++ b/src/refs.c
@@ -614,20 +614,53 @@ int git_reference_symbolic_set_target(
out, ref->db->repo, ref->name, target, 1, ref->target.symbolic, log_message);
}
+typedef struct {
+ const char *old_name;
+ git_refname_t new_name;
+} rename_cb_data;
+
+static int update_wt_heads(git_repository *repo, const char *path, void *payload)
+{
+ rename_cb_data *data = (rename_cb_data *) payload;
+ git_reference *head;
+ char *gitdir = NULL;
+ int error = 0;
+
+ if (git_reference__read_head(&head, repo, path) < 0 ||
+ git_reference_type(head) != GIT_REF_SYMBOLIC ||
+ git__strcmp(head->target.symbolic, data->old_name) != 0 ||
+ (gitdir = git_path_dirname(path)) == NULL)
+ goto out;
+
+ /* Update HEAD it was pointing to the reference being renamed */
+ if ((error = git_repository_create_head(gitdir, data->new_name)) < 0) {
+ giterr_set(GITERR_REFERENCE, "failed to update HEAD after renaming reference");
+ goto out;
+ }
+
+out:
+ git_reference_free(head);
+ git__free(gitdir);
+
+ return error;
+}
+
static int reference__rename(git_reference **out, git_reference *ref, const char *new_name, int force,
const git_signature *signature, const char *message)
{
+ git_repository *repo;
git_refname_t normalized;
bool should_head_be_updated = false;
int error = 0;
assert(ref && new_name && signature);
+ repo = git_reference_owner(ref);
+
if ((error = reference_normalize_for_repo(
- normalized, git_reference_owner(ref), new_name, true)) < 0)
+ normalized, repo, new_name, true)) < 0)
return error;
-
/* Check if we have to update HEAD. */
if ((error = git_branch_is_head(ref)) < 0)
return error;
@@ -637,14 +670,18 @@ static int reference__rename(git_reference **out, git_reference *ref, const char
if ((error = git_refdb_rename(out, ref->db, ref->name, normalized, force, signature, message)) < 0)
return error;
- /* Update HEAD it was pointing to the reference being renamed */
- if (should_head_be_updated &&
- (error = git_repository_set_head(ref->db->repo, normalized)) < 0) {
- giterr_set(GITERR_REFERENCE, "failed to update HEAD after renaming reference");
- return error;
+ /* Update HEAD if it was pointing to the reference being renamed */
+ if (should_head_be_updated) {
+ error = git_repository_set_head(ref->db->repo, normalized);
+ } else {
+ rename_cb_data payload;
+ payload.old_name = ref->name;
+ memcpy(&payload.new_name, &normalized, sizeof(normalized));
+
+ error = git_repository_foreach_head(repo, update_wt_heads, &payload);
}
- return 0;
+ return error;
}
diff --git a/tests/worktree/refs.c b/tests/worktree/refs.c
index cb6bfb5..95b173e 100644
--- a/tests/worktree/refs.c
+++ b/tests/worktree/refs.c
@@ -131,6 +131,20 @@ void test_worktree_refs__delete_succeeds_after_pruning_worktree(void)
git_reference_free(branch);
}
+void test_worktree_refs__renaming_reference_updates_worktree_heads(void)
+{
+ git_reference *head, *branch, *renamed;
+
+ cl_git_pass(git_branch_lookup(&branch, fixture.repo,
+ "testrepo-worktree", GIT_BRANCH_LOCAL));
+ cl_git_pass(git_reference_rename(&renamed, branch, "refs/heads/renamed", 0, NULL));
+ cl_git_pass(git_repository_head(&head, fixture.worktree));
+
+ git_reference_free(head);
+ git_reference_free(branch);
+ git_reference_free(renamed);
+}
+
void test_worktree_refs__creating_refs_uses_commondir(void)
{
git_reference *head, *branch, *lookup;
diff --git a/tests/worktree/worktree.c b/tests/worktree/worktree.c
index 6e90e6a..73991bf 100644
--- a/tests/worktree/worktree.c
+++ b/tests/worktree/worktree.c
@@ -486,3 +486,46 @@ void test_worktree_worktree__prune_both(void)
git_worktree_free(wt);
}
+
+static int read_head_ref(git_repository *repo, const char *path, void *payload)
+{
+ git_vector *refs = (git_vector *) payload;
+ git_reference *head;
+
+ GIT_UNUSED(repo);
+
+ cl_git_pass(git_reference__read_head(&head, repo, path));
+
+ git_vector_insert(refs, head);
+
+ return 0;
+}
+
+void test_worktree_worktree__foreach_head_gives_same_results_in_wt_and_repo(void)
+{
+ git_vector repo_refs = GIT_VECTOR_INIT, worktree_refs = GIT_VECTOR_INIT;
+ git_reference *heads[2];
+ size_t i;
+
+ cl_git_pass(git_reference_lookup(&heads[0], fixture.repo, GIT_HEAD_FILE));
+ cl_git_pass(git_reference_lookup(&heads[1], fixture.worktree, GIT_HEAD_FILE));
+
+ cl_git_pass(git_repository_foreach_head(fixture.repo, read_head_ref, &repo_refs));
+ cl_git_pass(git_repository_foreach_head(fixture.worktree, read_head_ref, &worktree_refs));
+
+ cl_assert_equal_i(repo_refs.length, ARRAY_SIZE(heads));
+ cl_assert_equal_i(worktree_refs.length, ARRAY_SIZE(heads));
+
+ for (i = 0; i < ARRAY_SIZE(heads); i++) {
+ cl_assert_equal_s(heads[i]->name, ((git_reference *) repo_refs.contents[i])->name);
+ cl_assert_equal_s(heads[i]->name, ((git_reference *) repo_refs.contents[i])->name);
+ cl_assert_equal_s(heads[i]->name, ((git_reference *) worktree_refs.contents[i])->name);
+
+ git_reference_free(heads[i]);
+ git_reference_free(repo_refs.contents[i]);
+ git_reference_free(worktree_refs.contents[i]);
+ }
+
+ git_vector_free(&repo_refs);
+ git_vector_free(&worktree_refs);
+}