Create branch_checked_out() helper
cover Create branch_checked_out() helper
This is a replacement for some patches from v2 of my 'git rebase --update-refs' topic [1]. After some feedback from Philip, I've decided to pull that topic while I rework how I track the refs to rewrite [2]. This series moves forward with the branch_checked_out() helper that was a bit more complicated than expected at first glance. This series is a culmination of the discussion started by Junio at [3].
[1]
https://lore.kernel.org/git/[email protected]
[2]
https://lore.kernel.org/git/[email protected]/
[3] https://lore.kernel.org/git/[email protected]/
Here is the patch outline:
1. Add a basic form of branch_checked_out() that only looks at the HEAD of each worktree. Since this doesn't look at BISECT_HEAD or REBASE_HEAD, the helper isn't linked to any consumer in this patch. A test script is added that verifies the current behavior checks that are implemented, even if they are not connected yet.
2. Make branch_checked_out() actually look at BISECT_HEAD and REBASE_HEAD. Add tests for those cases, which was previously untested in the Git test suite. Use branch_checked_out() in 'git branch -f' to demonstrate this helper works as expected.
3. Use branch_checked_out() in 'git fetch' when checking refs that would be updated by the refspec. Add tests for that scenario, too.
4. Use branch_checked_out() in 'git branch -D' to prevent deleting refs from under an existing checkout. The commit message here describes some barriers to removing other uses of find_shared_symref() that could be good investigations for later.
Thanks, -Stolee
Derrick Stolee (4):
branch: add branch_checked_out() helper
branch: check for bisects and rebases
fetch: use new branch_checked_out() and add tests
branch: use branch_checked_out() when deleting refs
branch.c | 77 +++++++++++++++++++++++++++++++++---- branch.h | 8 ++++ builtin/branch.c | 8 ++-- builtin/fetch.c | 25 ++++++------ t/t2407-worktree-heads.sh | 81 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 173 insertions(+), 26 deletions(-) create mode 100755 t/t2407-worktree-heads.sh
base-commit: 2668e3608e47494f2f10ef2b6e69f08a84816bcb
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1254%2Fderrickstolee%2Fbranch-checked-out-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1254/derrickstolee/branch-checked-out-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/1254
Signature · 1 line
gitgitgadget
PATCH 1/4 branch: add branch_checked_out() helper
From: Derrick Stolee <[email protected]>
The validate_new_branchname() method contains a check to see if a branch is checked out in any non-bare worktree. This is intended to prevent a force push that will mess up an existing checkout. This helper is not suitable to performing just that check, because the method will die() when the branch is checked out instead of returning an error code.
Create a new branch_checked_out() helper that performs the most basic form of this check. To ensure we can call branch_checked_out() in a loop with good performance, do a single preparation step that iterates over all worktrees and stores their current HEAD branches in a strmap. The branch_checked_out() helper can then discover these branches using a hash lookup.
This helper is currently missing some key functionality. Namely: it doesn't look for active rebases or bisects which mean that the branch is "checked out" even though HEAD doesn't point to that ref. This functionality will be added in a coming change.
We could use branch_checked_out() in validate_new_branchname(), but this missing functionality would be a regression. However, we have no tests that cover this case!
Add a new test script that will be expanded with these cross-worktree ref updates. The current tests would still pass if we refactored validate_new_branchname() to use this version of branch_checked_out(). The next change will fix that functionality and add the proper test coverage.
--- branch.c | 42 +++++++++++++++++++++++++++++++++++++++ branch.h | 8 ++++++++ t/t2407-worktree-heads.sh | 24 ++++++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100755 t/t2407-worktree-heads.sh
branch.c +42 -0
@@ -10,6 +10,7 @@#include "worktree.h" #include "submodule-config.h" #include "run-command.h" +#include "strmap.h" struct tracking {struct refspec_item spec;
@@ -369,6 +370,47 @@ int validate_branchname(const char *name, struct strbuf *ref)return ref_exists (ref->buf); } +static int initialized_checked_out_branches; +static struct strmap current_checked_out_branches =STRMAP_INIT ; + +static void prepare_checked_out_branches (void ) +{ +int i =0 ; +struct worktree **worktrees; + +if (initialized_checked_out_branches) +return ; + initialized_checked_out_branches =1 ; + + worktrees =get_worktrees (); + +while (worktrees[i]) { +struct worktree *wt = worktrees[i++]; + +if (wt->is_bare) +continue ; + +if (wt->head_ref) +strmap_put (¤t_checked_out_branches, + wt->head_ref, +xstrdup (wt->path)); + } + +free_worktrees (worktrees); +} + +int branch_checked_out (const char *refname,char **path) +{ +const char *path_in_set; +prepare_checked_out_branches (); + + path_in_set =strmap_get (¤t_checked_out_branches, refname); +if (path_in_set && path) + *path =xstrdup (path_in_set); + +return !!path_in_set; +} +/* * Check if a branch 'name' can be created as a new branch; die otherwise. * 'force' can be used when it is OK for the named branch already exists.
branch.h +8 -0
@@ -101,6 +101,14 @@ void create_branches_recursively(struct repository *r, const char *name, const char *tracking_name, int force, int reflog, int quiet, enum branch_track track, int dry_run); + +/* +* Returns true if the branch at 'refname' is checked out at any +* non-bare worktree. The path of the worktree is stored in the +* given 'path', if provided. +*/ +int branch_checked_out (const char *refname,char **path); +/* * Check if 'name' can be a valid name for a branch; die otherwise. * Return 1 if the named branch already exists; return 0 otherwise.
t/t2407-worktree-heads.sh new file +24 -0
@@ -0,0 +1,24 @@ +#!/bin/sh + +test_description='test operations trying to overwrite refs at worktree HEAD' + +. ./test-lib.sh + +test_expect_success 'setup' ' + for i in 1 2 3 4 + do + test_commit $i && + git branch wt-$i && + git worktree add wt-$i wt-$i || return 1 + done +' + +test_expect_success 'refuse to overwrite: checked out in worktree' ' + for i in 1 2 3 4 + do + test_must_fail git branch -f wt-$i HEAD 2>err + grep "cannot force update the branch" err || return 1 + done +' + +test_done
Signature · 1 line
gitgitgadget
PATCH 2/4 branch: check for bisects and rebases
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
The branch_checked_out() helper was added by the previous change, but it used an over-simplified view to check if a branch is checked out. It only focused on the HEAD symref, but ignored whether a bisect or rebase was happening.
Teach branch_checked_out() to check for these things, and also add tests to ensure that we do not lose this functionality in the future.
Now that this test coverage exists, we can safely refactor validate_new_branchname() to use branch_checked_out().
Note that we need to prepend "refs/heads/" to the 'state.branch' after calling wt_status_check_*(). We also need to duplicate wt->path so the value is not freed at the end of the call.
--- branch.c | 35 +++++++++++++++++++++++++++-------- t/t2407-worktree-heads.sh | 29 +++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 8 deletions(-)
branch.c +27 -8
@@ -385,6 +385,7 @@ static void prepare_checked_out_branches(void) worktrees =get_worktrees ();while (worktrees[i]) { +struct wt_status_state state = {0 };struct worktree *wt = worktrees[i++];if (wt->is_bare)
@@ -394,6 +395,29 @@ static void prepare_checked_out_branches(void)strmap_put (¤t_checked_out_branches, wt->head_ref,xstrdup (wt->path)); + +if (wt_status_check_rebase (wt, &state) && + (state.rebase_in_progress || state.rebase_interactive_in_progress) && + state.branch) { +struct strbuf ref =STRBUF_INIT ; +strbuf_addf (&ref,"refs/heads/%s" , state.branch); +strmap_put (¤t_checked_out_branches, + ref.buf, +xstrdup (wt->path)); +strbuf_release (&ref); + } +wt_status_state_free_buffers (&state); + +if (wt_status_check_bisect (wt, &state) && + state.branch) { +struct strbuf ref =STRBUF_INIT ; +strbuf_addf (&ref,"refs/heads/%s" , state.branch); +strmap_put (¤t_checked_out_branches, + ref.buf, +xstrdup (wt->path)); +strbuf_release (&ref); + } +wt_status_state_free_buffers (&state); }free_worktrees (worktrees);
@@ -419,9 +443,7 @@ int branch_checked_out(const char *refname, char **path)*/ int validate_new_branchname (const char *name,struct strbuf *ref,int force) { -struct worktree **worktrees; -const struct worktree *wt; - +char *path;if (!validate_branchname (name, ref))return 0 ;
@@ -429,13 +451,10 @@ int validate_new_branchname(const char *name, struct strbuf *ref, int force)die (_ ("a branch named '%s' already exists" ), ref->buf +strlen ("refs/heads/" )); - worktrees =get_worktrees (); - wt =find_shared_symref (worktrees,"HEAD" , ref->buf); -if (wt && !wt->is_bare) +if (branch_checked_out (ref->buf, &path))die (_ ("cannot force update the branch '%s' " "checked out at '%s'" ), - ref->buf +strlen ("refs/heads/" ), wt->path); -free_worktrees (worktrees); + ref->buf +strlen ("refs/heads/" ), path);return 1 ; }
t/t2407-worktree-heads.sh +29 -0
@@ -21,4 +21,33 @@ test_expect_success 'refuse to overwrite: checked out in worktree' ' done ' +test_expect_success 'refuse to overwrite: worktree in bisect' ' + test_when_finished test_might_fail git -C wt-4 bisect reset && + + ( + git -C wt-4 bisect start && + git -C wt-4 bisect bad HEAD && + git -C wt-4 bisect good HEAD~3 + ) && + + test_must_fail git branch -f wt-4 HEAD 2>err && + grep "cannot force update the branch '\''wt-4'\'' checked out at" err +' + +. "$TEST_DIRECTORY"/lib-rebase.sh + +test_expect_success 'refuse to overwrite: worktree in rebase' ' + test_when_finished test_might_fail git -C wt-4 rebase --abort && + + ( + set_fake_editor && + FAKE_LINES="edit 1 2 3" \ + git -C wt-4 rebase -i HEAD~3 >rebase && + git -C wt-4 status + ) && + + test_must_fail git branch -f wt-4 HEAD 2>err && + grep "cannot force update the branch '\''wt-4'\'' checked out at" err +' + test_done
Signature · 1 line
gitgitgadget
PATCH 3/4 fetch: use new branch_checked_out() and add tests
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
When fetching refs from a remote, it is possible that the refspec will cause use to overwrite a ref that is checked out in a worktree. The existing logic in builtin/fetch.c uses a possibly-slow mechanism. Update those sections to use the new, more efficient branch_checked_out() helper.
These uses were not previously tested, so add a test case that can be used for these kinds of collisions. There is only one test now, but more tests will be added as other consumers of branch_checked_out() are added.
Note that there are two uses in builtin/fetch.c, but only one of the messages is tested. This is because the tested check is run before completing the fetch, and the untested check is not reachable without concurrent updates to the filesystem. Thus, it is beneficial to keep that extra check for the sake of defense-in-depth. However, we should not attempt to test the check, as the effort required is too complicated to be worth the effort.
--- builtin/fetch.c | 25 +++++++++++-------------- t/t2407-worktree-heads.sh | 29 +++++++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 16 deletions(-)
builtin/fetch.c +11 -14
@@ -885,7 +885,7 @@ static int update_local_ref(struct ref *ref, struct worktree **worktrees) {struct commit *current =NULL , *updated; -const struct worktree *wt; +char *path =NULL ;const char *pretty_ref =prettify_refname (ref->name);int fast_forward =0 ;
@@ -900,17 +900,17 @@ static int update_local_ref(struct ref *ref, }if (!update_head_ok && - (wt =find_shared_symref (worktrees,"HEAD" , ref->name)) && - !wt->is_bare && !is_null_oid (&ref->old_oid)) { + !is_null_oid (&ref->old_oid) && +branch_checked_out (ref->name, &path)) {/* * If this is the head, and it's not okay to update * the head, and the old value of the head isn't empty... */ format_display (display,'!' ,_ ("[rejected]" ), - wt->is_current ? -_ ("can't fetch in current branch" ) : -_ ("checked out in another worktree" ), + path ?_ ("can't fetch in current branch" ) : +_ ("checked out in another worktree" ), remote, pretty_ref, summary_width); +free (path);return 1 ; }
@@ -1434,19 +1434,16 @@ cleanup:return result; } -static void check_not_current_branch (struct ref *ref_map, -struct worktree **worktrees) +static void check_not_current_branch (struct ref *ref_map) { -const struct worktree *wt; +char *path;for (; ref_map; ref_map = ref_map->next)if (ref_map->peer_ref &&starts_with (ref_map->peer_ref->name,"refs/heads/" ) && - (wt =find_shared_symref (worktrees,"HEAD" , - ref_map->peer_ref->name)) && - !wt->is_bare) +branch_checked_out (ref_map->peer_ref->name, &path))die (_ ("refusing to fetch into branch '%s' " "checked out at '%s'" ), - ref_map->peer_ref->name, wt->path); + ref_map->peer_ref->name, path); }static int truncate_fetch_head (void )
@@ -1650,7 +1647,7 @@ static int do_fetch(struct transport *transport, ref_map =get_ref_map (transport->remote, remote_refs, rs, tags, &autotags);if (!update_head_ok) -check_not_current_branch (ref_map, worktrees); +check_not_current_branch (ref_map); retcode =open_fetch_head (&fetch_head);if (retcode)
t/t2407-worktree-heads.sh +27 -2
@@ -10,6 +10,15 @@ test_expect_success 'setup' ' test_commit $i && git branch wt-$i && git worktree add wt-$i wt-$i || return 1 + done && + + # Create a server that updates each branch by one commit + git clone . server && + git remote add server ./server && + for i in 1 2 3 4 + do + git -C server checkout wt-$i && + test_commit -C server A-$i || return 1 done '
@@ -21,6 +30,16 @@ test_expect_success 'refuse to overwrite: checked out in worktree' ' done ' +test_expect_success 'refuse to overwrite during fetch' ' + test_must_fail git fetch server +refs/heads/wt-3:refs/heads/wt-3 2>err && + grep "refusing to fetch into branch '\''refs/heads/wt-3'\''" err && + + # General fetch into refs/heads/ will fail on first ref, + # so use a generic error message check. + test_must_fail git fetch server +refs/heads/*:refs/heads/* 2>err && + grep "refusing to fetch into branch" err +' + test_expect_success 'refuse to overwrite: worktree in bisect' ' test_when_finished test_might_fail git -C wt-4 bisect reset &&
@@ -31,7 +50,10 @@ test_expect_success 'refuse to overwrite: worktree in bisect' ' ) && test_must_fail git branch -f wt-4 HEAD 2>err && - grep "cannot force update the branch '\''wt-4'\'' checked out at" err + grep "cannot force update the branch '\''wt-4'\'' checked out at" err && + + test_must_fail git fetch server +refs/heads/wt-4:refs/heads/wt-4 2>err && + grep "refusing to fetch into branch '\''refs/heads/wt-4'\''" err ' . "$TEST_DIRECTORY"/lib-rebase.sh
@@ -47,7 +69,10 @@ test_expect_success 'refuse to overwrite: worktree in rebase' ' ) && test_must_fail git branch -f wt-4 HEAD 2>err && - grep "cannot force update the branch '\''wt-4'\'' checked out at" err + grep "cannot force update the branch '\''wt-4'\'' checked out at" err && + + test_must_fail git fetch server +refs/heads/wt-4:refs/heads/wt-4 2>err && + grep "refusing to fetch into branch '\''refs/heads/wt-4'\''" err ' test_done
Signature · 1 line
gitgitgadget
PATCH 4/4 branch: use branch_checked_out() when deleting refs
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
This is the last current use of find_shared_symref() that can easily be replaced by branch_checked_out(). The benefit of this switch is that the code is a bit simpler, but also it is faster on repeated calls.
The remaining uses of find_shared_symref() are non-trivial to remove, so we probably should not continue in that direction:
* builtin/notes.c uses find_shared_symref() with "NOTES_MERGE_REF" instead of "HEAD", so it doesn't have an immediate analogue with branch_checked_out(). Perhaps we should consider extending it to include that symref in addition to HEAD, BISECT_HEAD, and REBASE_HEAD.
* receive-pack.c checks to see if a worktree has a checkout for the ref that is being updated. The tricky part is that it can actually decide to update the worktree directly instead of just skipping the update. This all depends on the receive.denyCurrentBranch config option. The implementation currenty cares about receiving the worktree in the result, so the current branch_checked_out() prototype is insufficient currently. This is something to investigate later, though, since a large number of refs could be updated at the same time and using the strmap implementation of branch_checked_out() could be beneficial.
--- builtin/branch.c | 8 ++++---- t/t2407-worktree-heads.sh | 5 ++++- 2 files changed, 8 insertions(+), 5 deletions(-)
builtin/branch.c +4 -4
@@ -253,12 +253,12 @@ static int delete_branches(int argc, const char **argv, int force, int kinds, name =mkpathdup (fmt, bname.buf);if (kinds ==FILTER_REFS_BRANCHES ) { -const struct worktree *wt = -find_shared_symref (worktrees,"HEAD" , name); -if (wt) { +char *path; +if (branch_checked_out (name, &path)) {error (_ ("Cannot delete branch '%s' " "checked out at '%s'" ), - bname.buf, wt->path); + bname.buf, path); +free (path); ret =1 ;continue ; }
t/t2407-worktree-heads.sh +4 -1
@@ -26,7 +26,10 @@ test_expect_success 'refuse to overwrite: checked out in worktree' ' for i in 1 2 3 4 do test_must_fail git branch -f wt-$i HEAD 2>err - grep "cannot force update the branch" err || return 1 + grep "cannot force update the branch" err && + + test_must_fail git branch -D wt-$i 2>err + grep "Cannot delete branch" err || return 1 done '
Signature · 1 line
gitgitgadget
Re: [PATCH 2/4] branch: check for bisects and rebases
In reply to Derrick Stolee via GitGitGadgetQuoted 28 lines · Derrick Stolee via GitGitGadget
t/t2407-worktree-heads.sh +20
-0@@ -21,4 +21,33 @@ test_expect_success 'refuse to overwrite: checked out in worktree' ' done ' +test_expect_success 'refuse to overwrite: worktree in bisect' ' + test_when_finished test_might_fail git -C wt-4 bisect reset && + + ( + git -C wt-4 bisect start && + git -C wt-4 bisect bad HEAD && + git -C wt-4 bisect good HEAD~3 + ) && + + test_must_fail git branch -f wt-4 HEAD 2>err && + grep "cannot force update the branch '\''wt-4'\'' checked out at" err +' + +. "$TEST_DIRECTORY"/lib-rebase.sh + +test_expect_success 'refuse to overwrite: worktree in rebase' ' + test_when_finished test_might_fail git -C wt-4 rebase --abort && + + ( + set_fake_editor &&
t/t2407-worktree-heads.sh test_expect_success 'refuse to overwrite: checked out in worktree' ' · line 53
Quoted 10 lines
t/t2407-worktree-heads.sh +9
-0+ FAKE_LINES="edit 1 2 3" \ + git -C wt-4 rebase -i HEAD~3 >rebase && + git -C wt-4 status + ) && + + test_must_fail git branch -f wt-4 HEAD 2>err && + grep "cannot force update the branch '\''wt-4'\'' checked out at" err +' + test_done
Nice to see that each additional feature corresponds to each additional test.
Thanks.
Re: [PATCH 1/4] branch: add branch_checked_out() helper
In reply to Derrick Stolee via GitGitGadgetQuoted 12 lines · Derrick Stolee via GitGitGadget
branch.c +12
-0+static void prepare_checked_out_branches (void ) +{ +int i =0 ; +struct worktree **worktrees; + +if (initialized_checked_out_branches) +return ; + initialized_checked_out_branches =1 ; + + worktrees =get_worktrees (); + +while (worktrees[i]) {
branch.c int validate_branchname(const char *name, struct strbuf *ref) · line 397
Quoted 10 lines
branch.c +10
-0+struct worktree *wt = worktrees[i++]; + +if (wt->is_bare) +continue ; + +if (wt->head_ref) +strmap_put (¤t_checked_out_branches, + wt->head_ref, +xstrdup (wt->path)); + }
If two worktrees have by accident or by bug checked out the same branch, this strmap_put() will leak, as it overwrites the path to the worktree we found earlier with the branch checked out with the path to the worktree we just discovered with the same branch checked out. We could easily work it around by immediately freeing what comes back from strmap_put(), presumably. The resulting code
if (wt->head_ref) free(strmap_put(...));
may look strange, though, especially because we will see more instances of strmap_put() in this loop in later steps.
PATCH 5/5 branch: fix branch_checked_out() leaks
In reply to Derrick Stolee via GitGitGadgetQuoted 7 lines · Derrick Stolee via GitGitGadget
This is a replacement for some patches from v2 of my 'git rebase --update-refs' topic [1]. After some feedback from Philip, I've decided to pull that topic while I rework how I track the refs to rewrite [2]. This series moves forward with the
branch_checked_out()helper that was a bit more complicated than expected at first glance. This series is a culmination of the discussion started by Junio at [3].
Junio pointed out that patch 1 introduced a memory leak when a ref is checked out in multiple places. Here is a patch to fix that scenario. It applies cleanly on top of patch 4, so I include it as a new "patch 5". I will include it in any v2 of the full series, if needed.
Thanks,
-Stolee
---- >8 ----
From c3842b36ebb4053ac49b0306154b840431f9bf6f Mon Sep 17 00:00:00 2001
From: Derrick Stolee <[email protected]>
Date: Mon, 13 Jun 2022 10:33:20 -0400
Subject: [PATCH 5/5] branch: fix branch_checked_out() leaks
The branch_checked_out() method populates a strmap linking a refname to a worktree that has that branch checked out. While unlikely, it is possible that a bug or filesystem manipulation could create a scenario where the same ref is checked out in multiple places. Further, there are some states in an interactive rebase where HEAD and REBASE_HEAD point to the same ref, leading to multiple insertions into the strmap. In either case, the strmap_put() method returns the old value which is leaked.
Update branch_checked_out() to consume that pointer and free it.
Add a test in t2407 that checks this erroneous case. The test "checks itself" by first confirming that the filesystem manipulations it makes trigger the branch_checked_out() logic, and then sets up similar manipulations to make it look like there are multiple worktrees pointing to the same ref.
While TEST_PASSES_SANITIZE_LEAK would be helpful to demonstrate the leakage and prevent it in the future, t2407 uses helpers such as 'git clone' that cause the test to fail under that mode.
--- branch.c | 25 +++++++++++++++---------- t/t2407-worktree-heads.sh | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 10 deletions(-)
branch.c +15 -10
@@ -385,25 +385,29 @@ static void prepare_checked_out_branches(void) worktrees =get_worktrees ();while (worktrees[i]) { +char *old;struct wt_status_state state = {0 };struct worktree *wt = worktrees[i++];if (wt->is_bare)continue ; -if (wt->head_ref) -strmap_put (¤t_checked_out_branches, - wt->head_ref, -xstrdup (wt->path)); +if (wt->head_ref) { + old =strmap_put (¤t_checked_out_branches, + wt->head_ref, +xstrdup (wt->path)); +free (old); + }if (wt_status_check_rebase (wt, &state) && (state.rebase_in_progress || state.rebase_interactive_in_progress) && state.branch) {struct strbuf ref =STRBUF_INIT ;strbuf_addf (&ref,"refs/heads/%s" , state.branch); -strmap_put (¤t_checked_out_branches, - ref.buf, -xstrdup (wt->path)); + old =strmap_put (¤t_checked_out_branches, + ref.buf, +xstrdup (wt->path)); +free (old);strbuf_release (&ref); }wt_status_state_free_buffers (&state);
@@ -412,9 +416,10 @@ static void prepare_checked_out_branches(void) state.branch) {struct strbuf ref =STRBUF_INIT ;strbuf_addf (&ref,"refs/heads/%s" , state.branch); -strmap_put (¤t_checked_out_branches, - ref.buf, -xstrdup (wt->path)); + old =strmap_put (¤t_checked_out_branches, + ref.buf, +xstrdup (wt->path)); +free (old);strbuf_release (&ref); }wt_status_state_free_buffers (&state);
t/t2407-worktree-heads.sh +39 -0
@@ -78,4 +78,43 @@ test_expect_success 'refuse to overwrite: worktree in rebase' ' grep "refusing to fetch into branch '\''refs/heads/wt-4'\''" err ' +test_expect_success 'refuse to overwrite when in error states' ' + test_when_finished rm -rf .git/worktrees/wt-*/rebase-merge && + test_when_finished rm -rf .git/worktrees/wt-*/BISECT_* && + + git branch -f fake1 && + mkdir -p .git/worktrees/wt-3/rebase-merge && + touch .git/worktrees/wt-3/rebase-merge/interactive && + echo refs/heads/fake1 >.git/worktrees/wt-3/rebase-merge/head-name && + echo refs/heads/fake2 >.git/worktrees/wt-3/rebase-merge/onto && + + git branch -f fake2 && + touch .git/worktrees/wt-4/BISECT_LOG && + echo refs/heads/fake2 >.git/worktrees/wt-4/BISECT_START && + + # First, ensure we prevent writing when only one reason to fail. + for i in 1 2 + do + test_must_fail git branch -f fake$i HEAD 2>err && + grep "cannot force update the branch '\''fake$i'\'' checked out at" err || + return 1 + done && + + # Second, set up duplicate values. + mkdir -p .git/worktrees/wt-4/rebase-merge && + touch .git/worktrees/wt-4/rebase-merge/interactive && + echo refs/heads/fake2 >.git/worktrees/wt-4/rebase-merge/head-name && + echo refs/heads/fake1 >.git/worktrees/wt-4/rebase-merge/onto && + + touch .git/worktrees/wt-1/BISECT_LOG && + echo refs/heads/fake1 >.git/worktrees/wt-1/BISECT_START && + + for i in 1 2 + do + test_must_fail git branch -f fake$i HEAD 2>err && + grep "cannot force update the branch '\''fake$i'\'' checked out at" err || + return 1 + done +' + test_done
Signature · 1 line
2.36.1.220.g1fae7daf425
Re: [PATCH 5/5] branch: fix branch_checked_out() leaks
Quoted 8 lines · Derrick Stolee
branch.c +1
-0while (worktrees[i]) { +char *old;struct wt_status_state state = {0 };struct worktree *wt = worktrees[i++];if (wt->is_bare)continue ;
branch.c static void prepare_checked_out_branches(void) · line 400
Quoted 10 lines
branch.c +6
-4-if (wt->head_ref) -strmap_put (¤t_checked_out_branches, - wt->head_ref, -xstrdup (wt->path)); +if (wt->head_ref) { + old =strmap_put (¤t_checked_out_branches, + wt->head_ref, +xstrdup (wt->path)); +free (old); + }
While it is equivalent to
free(strmap_put(¤t_checked_out_branches,
wt->head_ref,
xstrdup(wt->path)));writing the "put the new one" and "discard the old one (if exists)" as separate steps like the above does make it easier to follow, at least to me.
Thanks. Will queue.
Re: [PATCH 1/4] branch: add branch_checked_out() helper
In reply to Derrick Stolee via GitGitGadgetbranch.c int validate_branchname(const char *name, struct strbuf *ref) · line 409
Quoted 5 lines · Derrick Stolee via GitGitGadget
From: Derrick Stolee <[email protected]>
[...]branch.c +3
-0+ path_in_set =strmap_get (¤t_checked_out_branches, refname); +if (path_in_set && path) + *path =xstrdup (path_in_set);
Looking at the end state of this series there is nothing that passes a null "path" to this function, i.e. it's all &some_variable.
So just dropping that && seems better from a code understanding & analysis perspective.
More generally, can't we just expose an API that gives us the strmap itself, so that callers don't need to keep any special-behavior in mind? I.e. just "make me a strmap of checked out branches".
Then you can just use strmap_get(), or xstrdup(strmap_get()), and if the pattern of "get me the item but duped" is found elsewhere maybe we should eventually have a strmap_get_dup() or whatever.
I.e. just making it: xstrdup(strmap_get(checked_out_branches_strmap(), refname)).
Re: [PATCH 3/4] fetch: use new branch_checked_out() and add tests
In reply to Derrick Stolee via GitGitGadgetQuoted 1 line · Derrick Stolee via GitGitGadget
From: Derrick Stolee <[email protected]>
re a comment on 1/4...
builtin/fetch.c static int update_local_ref(struct ref *ref, · line 888
Quoted 3 lines
builtin/fetch.c +1
-1struct commit *current =NULL , *updated; -const struct worktree *wt; +char *path =NULL ;
Init'd to NULL here...
Quoted 14 lines
builtin/fetch.c +2
-2const char *pretty_ref =prettify_refname (ref->name);int fast_forward =0 ;@@ -900,17 +900,17 @@ static int update_local_ref(struct ref *ref, }if (!update_head_ok && - (wt =find_shared_symref (worktrees,"HEAD" , ref->name)) && - !wt->is_bare && !is_null_oid (&ref->old_oid)) { + !is_null_oid (&ref->old_oid) && +branch_checked_out (ref->name, &path)) {/* * If this is the head, and it's not okay to update * the head, and the old value of the head isn't empty...
builtin/fetch.c static int update_local_ref(struct ref *ref, · line 914
Quoted 10 lines
builtin/fetch.c +3
-3*/ format_display (display,'!' ,_ ("[rejected]" ), - wt->is_current ? -_ ("can't fetch in current branch" ) : -_ ("checked out in another worktree" ), + path ?_ ("can't fetch in current branch" ) : +_ ("checked out in another worktree" ), remote, pretty_ref, summary_width); +free (path);return 1 ;
..but only used if branch_checkoed_out() returns non-zero, but (and I've only eyeballed this, not run it) isn't it impossible for that function not to return successfully and have "path" be non-NULL?
This seems to be a defense against the underlying strmap_get() starting to return nonsensical results, but if we instead just trust it...
Also re the "can't we just expose the strmap?" comment on 1/4, here we strmap_get() it, and xstrdup() and free() it, but we never needed our own copy, or even cared about the string at all, we're just checking "was it in the set?".
Which is what strmap_contains() would give us, if we just exposed it at that level. So:
struct strmap *map = make_my_branchs_map_thing();
[...]
if ([...] && strmap_contains(map, ref->name)) {
...Seems more straightforward, no?
Looking at the other callers it seems none of them need the xstrdup() at all, if looking at the end state. This one is a strmp_contains(), then an error() which could use strmap_get() without the xstrdup(), as we format it immediately.
The remaining two are calling die() right afterwards, which ditto can use a plain strmap_get() without xstrdup().
It's not that I care about the extra xstrdup(), which is obviously trivial, it's just harder to read & reason about APIs which e.g. "&& path", and strdup(), only to find that apparently no user in-tree cared about those...
Maybe they'll be needed, but let's just add them then, and in the meantime aim for simpler? :)
Re: [PATCH 5/5] branch: fix branch_checked_out() leaks
In reply to Derrick StoleeQuoted 41 lines · Derrick Stolee
Quoted 7 lines · Derrick Stolee via GitGitGadget
This is a replacement for some patches from v2 of my 'git rebase --update-refs' topic [1]. After some feedback from Philip, I've decided to pull that topic while I rework how I track the refs to rewrite [2]. This series moves forward with the
branch_checked_out()helper that was a bit more complicated than expected at first glance. This series is a culmination of the discussion started by Junio at [3].Junio pointed out that patch 1 introduced a memory leak when a ref is checked out in multiple places. Here is a patch to fix that scenario. It applies cleanly on top of patch 4, so I include it as a new "patch 5". I will include it in any v2 of the full series, if needed.
Thanks,
-Stolee---- >8 ----
From
c3842b36ebb4053ac49b0306154b840431f9bf6fMon Sep 17 00:00:00 2001
From: Derrick Stolee <[email protected]>
Date: Mon, 13 Jun 2022 10:33:20 -0400
Subject: [PATCH 5/5] branch: fixbranch_checked_out()leaksThe
branch_checked_out()method populates a strmap linking a refname to a worktree that has that branch checked out. While unlikely, it is possible that a bug or filesystem manipulation could create a scenario where the same ref is checked out in multiple places. Further, there are some states in an interactive rebase where HEAD andREBASE_HEADpoint to the same ref, leading to multiple insertions into the strmap. In either case, thestrmap_put()method returns the old value which is leaked.Update
branch_checked_out()to consume that pointer and free it.Add a test in t2407 that checks this erroneous case. The test "checks itself" by first confirming that the filesystem manipulations it makes trigger the
branch_checked_out()logic, and then sets up similar manipulations to make it look like there are multiple worktrees pointing to the same ref.
Quoted 3 lines
While
TEST_PASSES_SANITIZE_LEAKwould be helpful to demonstrate the leakage and prevent it in the future, t2407 uses helpers such as 'git clone' that cause the test to fail under that mode.
If you apply this:
diff --git a/t/t2407-worktree-heads.sh b/t/t2407-worktree-heads.sh index 0760595337b..d41171acb83 100755 --- a/t/t2407-worktree-heads.sh +++ b/t/t2407-worktree-heads.sh @@ -10,16 +10,8 @@ test_expect_success 'setup' ' test_commit $i && git branch wt-$i && git worktree add wt-$i wt-$i || return 1 - done && - - # Create a server that updates each branch by one commit - git clone . server && - git remote add server ./server && - for i in 1 2 3 4 - do - git -C server checkout wt-$i && - test_commit -C server A-$i || return 1 done + ' test_expect_success 'refuse to overwrite: checked out in worktree' '
And compile with SANITIZE=leak then this will pass as:
./t2407-worktree-heads.sh --run=1,6
I.e. you only needed the earlier part of the setup, and not "clone.
Given that I think it makes sense to just create a t2408-worktree-heads-leak.sh or something for this new test, then you can use TEST_PASSES_SANITIZE_LEAK.
Normally I'd just say "let's leave it for later", but in this case the entire point of the commit and the relatively lengthy test is to deal with a memory leak, so just copy/pasting the few lines of setup you actually need to a new test & testing with SANITIZE=leak seems worth the effort in this case.
Re: [PATCH 1/4] branch: add branch_checked_out() helper
In reply to Derrick Stolee via GitGitGadgetHi Stolee
Quoted 51 lines · Derrick Stolee via GitGitGadget
From: Derrick Stolee <[email protected]>
The
validate_new_branchname()method contains a check to see if a branch is checked out in any non-bare worktree. This is intended to prevent a force push that will mess up an existing checkout. This helper is not suitable to performing just that check, because the method willdie()when the branch is checked out instead of returning an error code.Create a new
branch_checked_out()helper that performs the most basic form of this check. To ensure we can callbranch_checked_out()in a loop with good performance, do a single preparation step that iterates over all worktrees and stores their current HEAD branches in a strmap. Thebranch_checked_out()helper can then discover these branches using a hash lookup.This helper is currently missing some key functionality. Namely: it doesn't look for active rebases or bisects which mean that the branch is "checked out" even though HEAD doesn't point to that ref. This functionality will be added in a coming change.
We could use
branch_checked_out()invalidate_new_branchname(), but this missing functionality would be a regression. However, we have no tests that cover this case!Add a new test script that will be expanded with these cross-worktree ref updates. The current tests would still pass if we refactored
validate_new_branchname()to use this version ofbranch_checked_out(). The next change will fix that functionality and add the proper test coverage.Signed-off-by Derrick Stolee <[email protected]>--- branch.c | 42 +++++++++++++++++++++++++++++++++++++++ branch.h | 8 ++++++++ t/t2407-worktree-heads.sh | 24 ++++++++++++++++++++++ 3 files changed, 74 insertions(+)create mode 100755
t/t2407-worktree-heads.shbranch.c +1
-0@@ -10,6 +10,7 @@#include "worktree.h" #include "submodule-config.h" #include "run-command.h" +#include "strmap.h" struct tracking {struct refspec_item spec;
branch.c int validate_branchname(const char *name, struct strbuf *ref) · line 374
Quoted 5 lines
branch.c +2
-0@@ -369,6 +370,47 @@ int validate_branchname(const char *name, struct strbuf *ref)return ref_exists (ref->buf); } +static int initialized_checked_out_branches; +static struct strmap current_checked_out_branches =STRMAP_INIT ;
I looks like this map is never freed which I think makes sense but makes me wonder about the relevance of patch 5. I think it would probably be worth marking the map with UNLEAK() in prepare_checked_out_branches().
Quoted 11 lines
branch.c +11
-0+static void prepare_checked_out_branches (void ) +{ +int i =0 ; +struct worktree **worktrees; + +if (initialized_checked_out_branches) +return ; + initialized_checked_out_branches =1 ; + + worktrees =get_worktrees (); +
branch.c int validate_branchname(const char *name, struct strbuf *ref) · line 396
Quoted 10 lines
branch.c +10
-0+while (worktrees[i]) { +struct worktree *wt = worktrees[i++]; + +if (wt->is_bare) +continue ; + +if (wt->head_ref) +strmap_put (¤t_checked_out_branches, + wt->head_ref, +xstrdup (wt->path));
STRMAP_INIT sets .strdup_strings = 1, so the xstrdup() is unnecessary.
Quoted 6 lines
branch.c +6
-0+ } + +free_worktrees (worktrees); +} + +int branch_checked_out (const char *refname,char **path)
branch.c int validate_branchname(const char *name, struct strbuf *ref) · line 412
Quoted 10 lines
branch.c +10
-0+{ +const char *path_in_set; +prepare_checked_out_branches (); + + path_in_set =strmap_get (¤t_checked_out_branches, refname); +if (path_in_set && path) + *path =xstrdup (path_in_set); + +return !!path_in_set; +}
I like the idea of having a specific function to see if a branch is checkout out rather than Ævar's suggestion of forcing all callers to do
strmap_get(get_worktree_refs_strmap(), ref)
which will quickly get tiresome. I do wonder though if we'd be better off with a thin wrapper around strmap_get() such as
const char* branch_checked_out(const char *refname)
{
prepare_checked_out_branches();
return strmap_get(¤t_checked_out_branches, refname);
}so that the user can choose whether to copy the path or not.
Best Wishes
Phillip
Quoted 52 lines
branch.c +0
-0/* * Check if a branch 'name' can be created as a new branch; die otherwise. * 'force' can be used when it is OK for the named branch already exists. branch.h +8
-0@@ -101,6 +101,14 @@ void create_branches_recursively(struct repository *r, const char *name, const char *tracking_name, int force, int reflog, int quiet, enum branch_track track, int dry_run); + +/* +* Returns true if the branch at 'refname' is checked out at any +* non-bare worktree. The path of the worktree is stored in the +* given 'path', if provided. +*/ +int branch_checked_out (const char *refname,char **path); +/* * Check if 'name' can be a valid name for a branch; die otherwise. * Return 1 if the named branch already exists; return 0 otherwise. t/t2407-worktree-heads.sh new file +24
-0@@ -0,0 +1,24 @@ +#!/bin/sh + +test_description='test operations trying to overwrite refs at worktree HEAD' + +. ./test-lib.sh + +test_expect_success 'setup' ' + for i in 1 2 3 4 + do + test_commit $i && + git branch wt-$i && + git worktree add wt-$i wt-$i || return 1 + done +' + +test_expect_success 'refuse to overwrite: checked out in worktree' ' + for i in 1 2 3 4 + do + test_must_fail git branch -f wt-$i HEAD 2>err + grep "cannot force update the branch" err || return 1 + done +' + +test_done
Re: [PATCH 3/4] fetch: use new branch_checked_out() and add tests
In reply to Derrick Stolee via GitGitGadgetHi Stolee
Quoted 24 lines · Derrick Stolee via GitGitGadget
From: Derrick Stolee <[email protected]>
[...]builtin/fetch.c +3
-3@@ -885,7 +885,7 @@ static int update_local_ref(struct ref *ref, struct worktree **worktrees) {struct commit *current =NULL , *updated; -const struct worktree *wt; +char *path =NULL ;const char *pretty_ref =prettify_refname (ref->name);int fast_forward =0 ;@@ -900,17 +900,17 @@ static int update_local_ref(struct ref *ref, }if (!update_head_ok && - (wt =find_shared_symref (worktrees,"HEAD" , ref->name)) && - !wt->is_bare && !is_null_oid (&ref->old_oid)) { + !is_null_oid (&ref->old_oid) && +branch_checked_out (ref->name, &path)) {
builtin/fetch.c static int update_local_ref(struct ref *ref, · line 911
Quoted 10 lines
builtin/fetch.c +2
-3/* * If this is the head, and it's not okay to update * the head, and the old value of the head isn't empty... */ format_display(display, '!', _("[rejected]"), - wt->is_current ? - _("can't fetch in current branch") : - _("checked out in another worktree"), + path ? _("can't fetch in current branch") : + _("checked out in another worktree"),
I'm confused by this, isn't path always non-null?
Best Wishes
Phillip
Quoted 99 lines
builtin/fetch.c +6
-8remote, pretty_ref, summary_width); +free (path);return 1 ; }@@ -1434,19 +1434,16 @@ cleanup:return result; } -static void check_not_current_branch (struct ref *ref_map, -struct worktree **worktrees) +static void check_not_current_branch (struct ref *ref_map) { -const struct worktree *wt; +char *path;for (; ref_map; ref_map = ref_map->next)if (ref_map->peer_ref &&starts_with (ref_map->peer_ref->name,"refs/heads/" ) && - (wt =find_shared_symref (worktrees,"HEAD" , - ref_map->peer_ref->name)) && - !wt->is_bare) +branch_checked_out (ref_map->peer_ref->name, &path))die (_ ("refusing to fetch into branch '%s' " "checked out at '%s'" ), - ref_map->peer_ref->name, wt->path); + ref_map->peer_ref->name, path); }static int truncate_fetch_head (void )@@ -1650,7 +1647,7 @@ static int do_fetch(struct transport *transport, ref_map =get_ref_map (transport->remote, remote_refs, rs, tags, &autotags);if (!update_head_ok) -check_not_current_branch (ref_map, worktrees); +check_not_current_branch (ref_map); retcode =open_fetch_head (&fetch_head);if (retcode)t/t2407-worktree-heads.sh +27
-2@@ -10,6 +10,15 @@ test_expect_success 'setup' ' test_commit $i && git branch wt-$i && git worktree add wt-$i wt-$i || return 1 + done && + + # Create a server that updates each branch by one commit + git clone . server && + git remote add server ./server && + for i in 1 2 3 4 + do + git -C server checkout wt-$i && + test_commit -C server A-$i || return 1 done '@@ -21,6 +30,16 @@ test_expect_success 'refuse to overwrite: checked out in worktree' ' done ' +test_expect_success 'refuse to overwrite during fetch' ' + test_must_fail git fetch server +refs/heads/wt-3:refs/heads/wt-3 2>err && + grep "refusing to fetch into branch '\''refs/heads/wt-3'\''" err && + + # General fetch into refs/heads/ will fail on first ref, + # so use a generic error message check. + test_must_fail git fetch server +refs/heads/*:refs/heads/* 2>err && + grep "refusing to fetch into branch" err +' + test_expect_success 'refuse to overwrite: worktree in bisect' ' test_when_finished test_might_fail git -C wt-4 bisect reset &&@@ -31,7 +50,10 @@ test_expect_success 'refuse to overwrite: worktree in bisect' ' ) && test_must_fail git branch -f wt-4 HEAD 2>err && - grep "cannot force update the branch '\''wt-4'\'' checked out at" err + grep "cannot force update the branch '\''wt-4'\'' checked out at" err && + + test_must_fail git fetch server +refs/heads/wt-4:refs/heads/wt-4 2>err && + grep "refusing to fetch into branch '\''refs/heads/wt-4'\''" err ' . "$TEST_DIRECTORY"/lib-rebase.sh@@ -47,7 +69,10 @@ test_expect_success 'refuse to overwrite: worktree in rebase' ' ) && test_must_fail git branch -f wt-4 HEAD 2>err && - grep "cannot force update the branch '\''wt-4'\'' checked out at" err + grep "cannot force update the branch '\''wt-4'\'' checked out at" err && + + test_must_fail git fetch server +refs/heads/wt-4:refs/heads/wt-4 2>err && + grep "refusing to fetch into branch '\''refs/heads/wt-4'\''" err ' test_done
Re: [PATCH 1/4] branch: add branch_checked_out() helper
In reply to Phillip WoodQuoted 5 lines · Phillip Wood
[..]
branch.c int validate_branchname(const char *name, struct strbuf *ref) · line 374
Quoted 2 lines
branch.c +2
-0+static int initialized_checked_out_branches; +static struct strmap current_checked_out_branches =STRMAP_INIT ;I looks like this map is never freed which I think makes sense but makes me wonder about the relevance of patch 5. I think it would probably be worth marking the map with
UNLEAK()inprepare_checked_out_branches().
Actually I think UNLEAK() is for marking stack variables, not globals so ignore that. I think patch 5 makes sense in that in keeps the leak bounded.
Quoted 6 lines
Quoted 4 lines
+ if (
wt->head_ref)
+ strmap_put(¤t_checked_out_branches, +wt->head_ref,
+xstrdup(wt->path));
STRMAP_INITsets .strdup_strings = 1, so thexstrdup()is unnecessary.
Sorry that's nonsense - it is wt->head_ref that is copied by strmap_put(), not wt->path, so the xstrdup() call is correct
Thanks for working on this and sorry for the confusion
Phillip
Re: [PATCH 1/4] branch: add branch_checked_out() helper
In reply to Ævar Arnfjörð BjarmasonQuoted 12 lines · Ævar Arnfjörð Bjarmason
branch.c int validate_branchname(const char *name, struct strbuf *ref) · line 409
Quoted 5 lines · Derrick Stolee via GitGitGadget
From: Derrick Stolee <[email protected]>
[...]branch.c +3
-0+ path_in_set =strmap_get (¤t_checked_out_branches, refname); +if (path_in_set && path) + *path =xstrdup (path_in_set);Looking at the end state of this series there is nothing that passes a null "path" to this function, i.e. it's all
&some_variable.
Quoted 2 lines
So just dropping that && seems better from a code understanding & analysis perspective.
I don't see the value in making this method more prone to error in the future, or less flexible to a possible caller that doesn't want to give a 'path'.
Quoted 9 lines
More generally, can't we just expose an API that gives us the strmap itself, so that callers don't need to keep any special-behavior in mind? I.e. just "make me a strmap of checked out branches".
Then you can just use
strmap_get(), or xstrdup(strmap_get()), and if the pattern of "get me the item but duped" is found elsewhere maybe we should eventually have astrmap_get_dup()or whatever.I.e. just making it:
xstrdup(strmap_get(checked_out_branches_strmap(), refname)).
This seems unnecessarily complicated. It also risks someone inserting key-value pairs in an improper place instead of being contained to prepare_checked_out_branches().
If your concern is about creating the static current_checked_out_branches, keep in mind that the callers that call branch_checked_out() in a loop would need to keep track of that strmap across several other methods. That additional complexity is much worse than asking for a simple answer through the black box of branch_checked_out().
Thanks,
-Stolee
Re: [PATCH 1/4] branch: add branch_checked_out() helper
In reply to Phillip WoodQuoted 11 lines · Phillip Wood
Hi Stolee
Quoted 6 lines · Derrick Stolee via GitGitGadget
From: Derrick Stolee <[email protected]>
+ if (
wt->head_ref)
+ strmap_put(¤t_checked_out_branches, +wt->head_ref,
+xstrdup(wt->path));
STRMAP_INITsets .strdup_strings = 1, so thexstrdup()is unnecessary.
That .strdup_strings is only for the key, not the value (since the value could be anything, not necessarily a string).
Quoted 23 lines
Quoted 6 lines
+ }
branch.c +5
-0+ + free_worktrees(worktrees); +} + +int branch_checked_out (const char *refname,char **path)branch.c int validate_branchname(const char *name, struct strbuf *ref) · line 412
Quoted 10 lines
branch.c +10
-0+{ + const char *path_in_set; + prepare_checked_out_branches(); + + path_in_set = strmap_get(¤t_checked_out_branches, refname); + if (path_in_set && path) + *path = xstrdup(path_in_set); + + return !!path_in_set; +}I like the idea of having a specific function to see if a branch is checkout out rather than Ævar's suggestion of forcing all callers to do
strmap_get(get_worktree_refs_strmap(), ref)
which will quickly get tiresome. I do wonder though if we'd be better off with a thin wrapper aroundstrmap_get()such as
Quoted 7 lines
const char*
branch_checked_out(const char *refname){
prepare_checked_out_branches();
returnstrmap_get(¤t_checked_out_branches, refname); }so that the user can choose whether to copy the path or not.
This is an interesting suggestion. It changes callers a bit, but not too much. The pattern currently is:
if (branch_checked_out(name, &path)) {
/* do something */
free(path);
}but would become
if ((path = branch_checked_out(name))) {
/* do something */
}Sounds good to me.
Thanks,
-Stolee
Re: [PATCH 3/4] fetch: use new branch_checked_out() and add tests
In reply to Phillip WoodQuoted 11 lines · Phillip Wood
Hi Stolee
Quoted 6 lines · Derrick Stolee via GitGitGadget
format_display(display, '!',
_("[rejected]"), -wt->is_current?
-_("can't fetch in current branch"):
-_("checked out in another worktree"), + path ?_("can't fetch in current branch"):
+_("checked out in another worktree"),I'm confused by this, isn't path always non-null?
Yes, at this point it is. This message needs to change since we no longer have access to the worktree. As noted in the commit message, this case requires concurrent changes to the filesystem to activate this logic, so having a simpler "branch is checked out _somewhere_" message should work here.
Thanks,
-Stolee
Re: [PATCH 1/4] branch: add branch_checked_out() helper
In reply to Derrick StoleeQuoted 13 lines · Derrick Stolee
Quoted 12 lines · Ævar Arnfjörð Bjarmason
Review
Quoted 5 lines · Derrick Stolee via GitGitGadget
From: Derrick Stolee <[email protected]>
[...](quoted diff) +3
-0+ path_in_set = strmap_get(¤t_checked_out_branches, refname); + if (path_in_set && path) + *path = xstrdup(path_in_set);Looking at the end state of this series there is nothing that passes a null "path" to this function, i.e. it's all
&some_variable.
Quoted 6 lines
Quoted 2 lines
So just dropping that && seems better from a code understanding & analysis perspective.
I don't see the value in making this method more prone to error in the future, or less flexible to a possible caller that doesn't want to give a 'path'.
I think creating wrappers for exsiting APIs when they're not really needed is what's more error prone. I.e. you need to learn to use the new thing, whereas if it returns an existing type like "struct strmap", "struct strbuf" etc. there's no learning curve.
It doesn't matter *that much* in this case, but I think it really adds up.
Quoted 8 lines
Quoted 8 lines
More generally, can't we just expose an API that gives us the strmap itself, so that callers don't need to keep any special-behavior in mind? I.e. just "make me a strmap of checked out branches".
Then you can just use
strmap_get(), or xstrdup(strmap_get()), and if the pattern of "get me the item but duped" is found elsewhere maybe we should eventually have astrmap_get_dup()or whatever.
Quoted 5 lines
Quoted 1 line
I.e. just making it:
xstrdup(strmap_get(checked_out_branches_strmap(), refname)).This seems unnecessarily complicated. It also risks someone inserting key-value pairs in an improper place instead of being contained to
prepare_checked_out_branches().
I really don't think the internal APIs in git need to be this paranoid, just document it as "don't touch this".
I think in practice we have (and can expect) pretty mmuch zero bugs due to misuse of APIs that are clearly meant to be global singletons, whereas as this series & the fix-up shows it's more likely that we'll need to deal with subtle memory leaks etc. due to over-wrapping in new APIS :)
Or, if it does need paranoia simply structure the API to have each caller get its own copy, here all the callers are one-shot built-ins, so isn't this a premature optimization?
And speaking of risking things, probably overly fragile as an underlying "general" API, if that's what you're aiming for.
I.e. you're making the hard assumption that the set of checked out branches aren't changing during the lifetime of the process. So e.g. builtin/checkout.c couldn't call this at the end of its run if something in it had needed to get the map earlier (before we switched branches)...
Quoted 5 lines
If your concern is about creating the static
current_checked_out_branches, keep in mind that the callers that callbranch_checked_out()in a loop would need to keep track of that strmap across several other methods. That additional complexity is much worse than asking for a simple answer through the black box ofbranch_checked_out().
...that being said I was not suggesting to do away with the static pattern, but to keep it, and to just return the map.
As far as hypothetical hot loops are concerned that would be marginally cheaper, as you could move the "get map" outside a loop in such a caller.
I tried this fix-up on top of "seen", which passes your test, and as noted allocates less memory:
branch.c +7 -15
@@ -373,14 +373,13 @@ int validate_branchname(const char *name, struct strbuf *ref)static int initialized_checked_out_branches;static struct strmap current_checked_out_branches =STRMAP_INIT ; -static void prepare_checked_out_branches (void ) +struct strmap *checked_out_branches_map (void ) {int i =0 ;struct worktree **worktrees;if (initialized_checked_out_branches) -return ; - initialized_checked_out_branches =1 ; + goto done; worktrees =get_worktrees ();
@@ -426,18 +425,10 @@ static void prepare_checked_out_branches(void) }free_worktrees (worktrees); -} -int branch_checked_out (const char *refname,char **path) -{ -const char *path_in_set; -prepare_checked_out_branches (); - - path_in_set =strmap_get (¤t_checked_out_branches, refname); -if (path_in_set && path) - *path =xstrdup (path_in_set); - -return !!path_in_set; +done: + initialized_checked_out_branches =1 ; +return ¤t_checked_out_branches; }/*
@@ -456,7 +447,8 @@ int validate_new_branchname(const char *name, struct strbuf *ref, int force)die (_ ("a branch named '%s' already exists" ), ref->buf +strlen ("refs/heads/" )); -if (branch_checked_out (ref->buf, &path)) + path =strmap_get (checked_out_branches_map () , ref->buf); +if (path)die (_ ("cannot force update the branch '%s' " "checked out at '%s'" ), ref->buf +strlen ("refs/heads/" ), path);
branch.h +5 -4
@@ -103,11 +103,12 @@ void create_branches_recursively(struct repository *r, const char *name, int dry_run);/* -* Returns true if the branch at 'refname' is checked out at any -* non-bare worktree. The path of the worktree is stored in the -* given 'path', if provided. +* Lazily returns a "struct strmap" of checked out branches. The +* returned value is a global that'll be populated on the first +* call. You should only call read-only strmap API functions on this, +* such as strmap_get(), strmap_contains() etc. */ -int branch_checked_out (const char *refname,char **path); +struct strmap *checked_out_branches_map (void );/* * Check if 'name' can be a valid name for a branch; die otherwise.
builtin/branch.c +4 -3
@@ -24,6 +24,7 @@#include "worktree.h" #include "help.h" #include "commit-reach.h" +#include "strmap.h" static const char *const builtin_branch_usage[] = {N_ ("git branch [<options>] [-r | -a] [--merged] [--no-merged]" ),
@@ -253,12 +254,12 @@ static int delete_branches(int argc, const char **argv, int force, int kinds, name =mkpathdup (fmt, bname.buf);if (kinds ==FILTER_REFS_BRANCHES ) { -char *path; -if (branch_checked_out (name, &path)) { +const char *path =strmap_get (checked_out_branches_map (), + name); +if (path) {error (_ ("Cannot delete branch '%s' " "checked out at '%s'" ), bname.buf, path); -free (path); ret =1 ;continue ; }
builtin/fetch.c +7 -6
@@ -30,6 +30,7 @@#include "shallow.h" #include "worktree.h" #include "bundle-uri.h" +#include "strmap.h" #define FORCED_UPDATES_DELAY_WARNING_IN_MS (10 * 1000)
@@ -890,7 +891,6 @@ static int update_local_ref(struct ref *ref, struct worktree **worktrees) {struct commit *current =NULL , *updated; -char *path =NULL ;const char *pretty_ref =prettify_refname (ref->name);int fast_forward =0 ;
@@ -905,17 +905,17 @@ static int update_local_ref(struct ref *ref, }if (!update_head_ok && - !is_null_oid (&ref->old_oid) && -branch_checked_out (ref->name, &path)) { + !is_null_oid (&ref->old_oid)) { +int in_set =strmap_contains (checked_out_branches_map (), + ref->name);/* * If this is the head, and it's not okay to update * the head, and the old value of the head isn't empty... */ format_display (display,'!' ,_ ("[rejected]" ), - path ?_ ("can't fetch in current branch" ) : + in_set ?_ ("can't fetch in current branch" ) :_ ("checked out in another worktree" ), remote, pretty_ref, summary_width); -free (path);return 1 ; }
@@ -1445,7 +1445,8 @@ static void check_not_current_branch(struct ref *ref_map)for (; ref_map; ref_map = ref_map->next)if (ref_map->peer_ref &&starts_with (ref_map->peer_ref->name,"refs/heads/" ) && -branch_checked_out (ref_map->peer_ref->name, &path)) + (path =strmap_get (checked_out_branches_map (), + ref_map->peer_ref->name)))die (_ ("refusing to fetch into branch '%s' " "checked out at '%s'" ), ref_map->peer_ref->name, path);
Re: [PATCH 5/5] branch: fix branch_checked_out() leaks
In reply to Ævar Arnfjörð BjarmasonQuoted 34 lines · Ævar Arnfjörð Bjarmason
Quoted 5 lines · Derrick Stolee
On 6/8/2022 4:08 PM, Derrick Stolee via GitGitGadget wrote:
While
TEST_PASSES_SANITIZE_LEAKwould be helpful to demonstrate the leakage and prevent it in the future, t2407 uses helpers such as 'git clone' that cause the test to fail under that mode.If you apply this:
diff --git a/t/t2407-worktree-heads.sh b/t/t2407-worktree-heads.sh index 0760595337b..d41171acb83 100755 --- a/t/t2407-worktree-heads.sh +++ b/t/t2407-worktree-heads.sh @@ -10,16 +10,8 @@ test_expect_success 'setup' ' test_commit $i && git branch wt-$i && git worktree add wt-$i wt-$i || return 1 - done && - - # Create a server that updates each branch by one commit - git clone . server && - git remote add server ./server && - for i in 1 2 3 4 - do - git -C server checkout wt-$i && - test_commit -C server A-$i || return 1 done + ' test_expect_success 'refuse to overwrite: checked out in worktree' '
Quoted 3 lines
And compile with SANITIZE=leak then this will pass as:
./t2407-worktree-heads.sh --run=1,6
Of course this works for the tests that don't need the 'server' repo, but it fails in the tests that _do_ need it.
I'm able to make this work by creating the 'server' with init and creating the wt-$i branches from scratch (they don't need to be fast-forward updates).
The linux-leaks tests still fail due to 'git fetch' and 'git bisect' calls, but these can be avoided by carefully splitting the tests and using the !SANITIZE_LEAK prereq.
Quoted 5 lines
Normally I'd just say "let's leave it for later", but in this case the entire point of the commit and the relatively lengthy test is to deal with a memory leak, so just copy/pasting the few lines of setup you actually need to a new test & testing with SANITIZE=leak seems worth the effort in this case.
Well, the point isn't to use automation to check for leaks, but instead to fix leaks and add tests for the case where we previously had leaks. The tests demonstrate that we aren't accidentally introducing a use-after- free or double-free.
Thanks,
-Stolee
Re: [PATCH 5/5] branch: fix branch_checked_out() leaks
Quoted 42 lines · Derrick Stolee
Quoted 34 lines · Ævar Arnfjörð Bjarmason
Quoted 5 lines · Derrick Stolee
On 6/8/2022 4:08 PM, Derrick Stolee via GitGitGadget wrote:
While
TEST_PASSES_SANITIZE_LEAKwould be helpful to demonstrate the leakage and prevent it in the future, t2407 uses helpers such as 'git clone' that cause the test to fail under that mode.If you apply this:
diff --git a/t/t2407-worktree-heads.sh b/t/t2407-worktree-heads.sh index 0760595337b..d41171acb83 100755 --- a/t/t2407-worktree-heads.sh +++ b/t/t2407-worktree-heads.sh @@ -10,16 +10,8 @@ test_expect_success 'setup' ' test_commit $i && git branch wt-$i && git worktree add wt-$i wt-$i || return 1 - done && - - # Create a server that updates each branch by one commit - git clone . server && - git remote add server ./server && - for i in 1 2 3 4 - do - git -C server checkout wt-$i && - test_commit -C server A-$i || return 1 done + ' test_expect_success 'refuse to overwrite: checked out in worktree' 'Quoted 3 lines
And compile with SANITIZE=leak then this will pass as:
./t2407-worktree-heads.sh --run=1,6Of course this works for the tests that don't need the 'server' repo, but it fails in the tests that _do_ need it.
Quoted 7 lines
I'm able to make this work by creating the 'server' with init and creating the wt-$i branches from scratch (they don't need to be fast-forward updates).
The linux-leaks tests still fail due to 'git fetch' and 'git bisect' calls, but these can be avoided by carefully splitting the tests and using the !SANITIZE_LEAK prereq.
I mean copy/paste the existing file to a new one with this on top, then remove all tests that aren't your new one. That worked for me, doesn't it work for you?
Quoted 10 lines
Quoted 5 lines
Normally I'd just say "let's leave it for later", but in this case the entire point of the commit and the relatively lengthy test is to deal with a memory leak, so just copy/pasting the few lines of setup you actually need to a new test & testing with SANITIZE=leak seems worth the effort in this case.
Well, the point isn't to use automation to check for leaks, but instead to fix leaks and add tests for the case where we previously had leaks. The tests demonstrate that we aren't accidentally introducing a use-after- free or double-free.
I mean that the patch subject is "fix[...]leaks", and this gives it a CI'd regression test, which seems worth it.
Of course it's also nice to check for use-after-fre etc., but we have other coverage for that. E.g. glibc in CI spots double-free issues with how we set it up, and we tend to segfault soon enough (or someone runs SANITIZE=address) on use-after-free.
v2 cover Create branch_checked_out() helper
In reply to Derrick Stolee via GitGitGadgetThis is a replacement for some patches from v2 of my 'git rebase --update-refs' topic [1]. After some feedback from Philip, I've decided to pull that topic while I rework how I track the refs to rewrite [2]. This series moves forward with the branch_checked_out() helper that was a bit more complicated than expected at first glance. This series is a culmination of the discussion started by Junio at [3].
[1]
https://lore.kernel.org/git/[email protected]
[2]
https://lore.kernel.org/git/[email protected]/
[3] https://lore.kernel.org/git/[email protected]/
Here is the patch outline:
1. Add a basic form of branch_checked_out() that only looks at the HEAD of each worktree. Since this doesn't look at BISECT_HEAD or REBASE_HEAD, the helper isn't linked to any consumer in this patch. A test script is added that verifies the current behavior checks that are implemented, even if they are not connected yet.
2. Make branch_checked_out() actually look at BISECT_HEAD and REBASE_HEAD. Add tests for those cases, which was previously untested in the Git test suite. Use branch_checked_out() in 'git branch -f' to demonstrate this helper works as expected.
3. Use branch_checked_out() in 'git fetch' when checking refs that would be updated by the refspec. Add tests for that scenario, too.
4. Use branch_checked_out() in 'git branch -D' to prevent deleting refs from under an existing checkout. The commit message here describes some barriers to removing other uses of find_shared_symref() that could be good investigations for later.
5. Be careful when constructing the strmap to free old values, even though this should only happen in error scenarios. Add tests that verify this behavior.
Updates in v2
=============
* branch_checked_out() has a new prototype where it returns a 'const char *' instead of copying the path.
* The test script is marked with TEST_PASSES_SANITIZE_LEAK and test are careful to avoid using 'git rebase' or 'git bisect' when possible.
* Tests that cannot avoid memory-loss from 'git fetch' are marked with the "!SANITIZE_LEAK" prereq.
* A previous replacement of 'wt->current' with 'path' is removed. This changes an error message, but it is a very rare scenario where this error would be output.
Thanks, -Stolee
Derrick Stolee (5):
branch: add branch_checked_out() helper
branch: check for bisects and rebases
fetch: use new branch_checked_out() and add tests
branch: use branch_checked_out() when deleting refs
branch: fix branch_checked_out() leaks
branch.c | 76 +++++++++++++++++++--- branch.h | 7 +++ builtin/branch.c | 7 +-- builtin/fetch.c | 22 +++---- t/t2407-worktree-heads.sh | 129 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 215 insertions(+), 26 deletions(-) create mode 100755 t/t2407-worktree-heads.sh
base-commit: 2668e3608e47494f2f10ef2b6e69f08a84816bcb
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1254%2Fderrickstolee%2Fbranch-checked-out-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1254/derrickstolee/branch-checked-out-v2
Pull-Request: https://github.com/gitgitgadget/git/pull/1254
Range-diff vs v1:
1: dbb7eae390c ! 1: bf701119edc branch: add branch_checked_out() helper
@@ branch.c: int validate_branchname(const char *name, struct strbuf *ref)
+ free_worktrees(worktrees);
+}
+
-+int branch_checked_out(const char *refname, char **path)
++const char *branch_checked_out(const char *refname)
+{
-+ const char *path_in_set;
+ prepare_checked_out_branches();
-+
-+ path_in_set = strmap_get(¤t_checked_out_branches, refname);
-+ if (path_in_set && path)
-+ *path = xstrdup(path_in_set);
-+
-+ return !!path_in_set;
++ return strmap_get(¤t_checked_out_branches, refname);
+}
+
/*
@@ branch.h: void create_branches_recursively(struct repository *r, const char *nam
int dry_run);
+
+/*
-+ * Returns true if the branch at 'refname' is checked out at any
-+ * non-bare worktree. The path of the worktree is stored in the
-+ * given 'path', if provided.
++ * If the branch at 'refname' is currently checked out in a worktree,
++ * then return the path to that worktree.
+ */
-+int branch_checked_out(const char *refname, char **path);
++const char *branch_checked_out(const char *refname);
+
/*
* Check if 'name' can be a valid name for a branch; die otherwise.
@@ t/t2407-worktree-heads.sh (new)
+
+test_description='test operations trying to overwrite refs at worktree HEAD'
+
++TEST_PASSES_SANITIZE_LEAK=true
+. ./test-lib.sh
+
+test_expect_success 'setup' '
++ test_commit init &&
++ git branch -f fake-1 &&
++ git branch -f fake-2 &&
++
+ for i in 1 2 3 4
+ do
+ test_commit $i &&
2: 18bad9b0c49 ! 2: 9347303db89 branch: check for bisects and rebases
@@ branch.c: static void prepare_checked_out_branches(void)
}
free_worktrees(worktrees);
-@@ branch.c: int branch_checked_out(const char *refname, char **path)
+@@ branch.c: const char *branch_checked_out(const char *refname)
*/
int validate_new_branchname(const char *name, struct strbuf *ref, int force)
{
- struct worktree **worktrees;
- const struct worktree *wt;
-
-+ char *path;
++ const char *path;
if (!validate_branchname(name, ref))
return 0;
@@ branch.c: int validate_new_branchname(const char *name, struct strbuf *ref, int
- worktrees = get_worktrees();
- wt = find_shared_symref(worktrees, "HEAD", ref->buf);
- if (wt && !wt->is_bare)
-+ if (branch_checked_out(ref->buf, &path))
++ if ((path = branch_checked_out(ref->buf)))
die(_("cannot force update the branch '%s' "
"checked out at '%s'"),
- ref->buf + strlen("refs/heads/"), wt->path);
@@ t/t2407-worktree-heads.sh: test_expect_success 'refuse to overwrite: checked out
'
+test_expect_success 'refuse to overwrite: worktree in bisect' '
-+ test_when_finished test_might_fail git -C wt-4 bisect reset &&
++ test_when_finished rm -rf .git/worktrees/wt-*/BISECT_* &&
+
-+ (
-+ git -C wt-4 bisect start &&
-+ git -C wt-4 bisect bad HEAD &&
-+ git -C wt-4 bisect good HEAD~3
-+ ) &&
++ touch .git/worktrees/wt-4/BISECT_LOG &&
++ echo refs/heads/fake-2 >.git/worktrees/wt-4/BISECT_START &&
+
-+ test_must_fail git branch -f wt-4 HEAD 2>err &&
-+ grep "cannot force update the branch '\''wt-4'\'' checked out at" err
++ test_must_fail git branch -f fake-2 HEAD 2>err &&
++ grep "cannot force update the branch '\''fake-2'\'' checked out at.*wt-4" err
+'
+
-+. "$TEST_DIRECTORY"/lib-rebase.sh
-+
+test_expect_success 'refuse to overwrite: worktree in rebase' '
-+ test_when_finished test_might_fail git -C wt-4 rebase --abort &&
++ test_when_finished rm -rf .git/worktrees/wt-*/rebase-merge &&
+
-+ (
-+ set_fake_editor &&
-+ FAKE_LINES="edit 1 2 3" \
-+ git -C wt-4 rebase -i HEAD~3 >rebase &&
-+ git -C wt-4 status
-+ ) &&
++ mkdir -p .git/worktrees/wt-3/rebase-merge &&
++ touch .git/worktrees/wt-3/rebase-merge/interactive &&
++ echo refs/heads/fake-1 >.git/worktrees/wt-3/rebase-merge/head-name &&
++ echo refs/heads/fake-2 >.git/worktrees/wt-3/rebase-merge/onto &&
+
-+ test_must_fail git branch -f wt-4 HEAD 2>err &&
-+ grep "cannot force update the branch '\''wt-4'\'' checked out at" err
++ test_must_fail git branch -f fake-1 HEAD 2>err &&
++ grep "cannot force update the branch '\''fake-1'\'' checked out at.*wt-3" err
+'
+
test_done
3: 4540dbeed38 ! 3: 1c764bfcfe4 fetch: use new branch_checked_out() and add tests
@@ Commit message
concurrent updates to the filesystem. Thus, it is beneficial to keep
that extra check for the sake of defense-in-depth. However, we should
not attempt to test the check, as the effort required is too
- complicated to be worth the effort.
+ complicated to be worth the effort. This use in update_local_ref()
+ also requires a change in the error message because we no longer have
+ access to the worktree struct, only the path of the worktree. This error
+ is so rare that making a distinction between the two is not critical.
Signed-off-by: Derrick Stolee <[email protected]>
@@ builtin/fetch.c: static int update_local_ref(struct ref *ref,
{
struct commit *current = NULL, *updated;
- const struct worktree *wt;
-+ char *path = NULL;
const char *pretty_ref = prettify_refname(ref->name);
int fast_forward = 0;
@@ builtin/fetch.c: static int update_local_ref(struct ref *ref,
- (wt = find_shared_symref(worktrees, "HEAD", ref->name)) &&
- !wt->is_bare && !is_null_oid(&ref->old_oid)) {
+ !is_null_oid(&ref->old_oid) &&
-+ branch_checked_out(ref->name, &path)) {
++ branch_checked_out(ref->name)) {
/*
* If this is the head, and it's not okay to update
* the head, and the old value of the head isn't empty...
@@ builtin/fetch.c: static int update_local_ref(struct ref *ref,
- wt->is_current ?
- _("can't fetch in current branch") :
- _("checked out in another worktree"),
-+ path ? _("can't fetch in current branch") :
-+ _("checked out in another worktree"),
++ _("can't fetch into checked-out branch"),
remote, pretty_ref, summary_width);
-+ free(path);
return 1;
}
-
@@ builtin/fetch.c: cleanup:
return result;
}
@@ builtin/fetch.c: cleanup:
+static void check_not_current_branch(struct ref *ref_map)
{
- const struct worktree *wt;
-+ char *path;
++ const char *path;
for (; ref_map; ref_map = ref_map->next)
if (ref_map->peer_ref &&
starts_with(ref_map->peer_ref->name, "refs/heads/") &&
- (wt = find_shared_symref(worktrees, "HEAD",
- ref_map->peer_ref->name)) &&
- !wt->is_bare)
-+ branch_checked_out(ref_map->peer_ref->name, &path))
++ (path = branch_checked_out(ref_map->peer_ref->name)))
die(_("refusing to fetch into branch '%s' "
"checked out at '%s'"),
- ref_map->peer_ref->name, wt->path);
@@ t/t2407-worktree-heads.sh: test_expect_success 'setup' '
+ done &&
+
+ # Create a server that updates each branch by one commit
-+ git clone . server &&
++ git init server &&
++ test_commit -C server initial &&
+ git remote add server ./server &&
+ for i in 1 2 3 4
+ do
-+ git -C server checkout wt-$i &&
++ git -C server checkout -b wt-$i &&
+ test_commit -C server A-$i || return 1
++ done &&
++ for i in 1 2
++ do
++ git -C server checkout -b fake-$i &&
++ test_commit -C server f-$i || return 1
done
'
-@@ t/t2407-worktree-heads.sh: test_expect_success 'refuse to overwrite: checked out in worktree' '
- done
+@@ t/t2407-worktree-heads.sh: test_expect_success 'refuse to overwrite: worktree in rebase' '
+ grep "cannot force update the branch '\''fake-1'\'' checked out at.*wt-3" err
'
-+test_expect_success 'refuse to overwrite during fetch' '
++test_expect_success !SANITIZE_LEAK 'refuse to fetch over ref: checked out' '
+ test_must_fail git fetch server +refs/heads/wt-3:refs/heads/wt-3 2>err &&
+ grep "refusing to fetch into branch '\''refs/heads/wt-3'\''" err &&
+
@@ t/t2407-worktree-heads.sh: test_expect_success 'refuse to overwrite: checked out
+ grep "refusing to fetch into branch" err
+'
+
- test_expect_success 'refuse to overwrite: worktree in bisect' '
- test_when_finished test_might_fail git -C wt-4 bisect reset &&
-
-@@ t/t2407-worktree-heads.sh: test_expect_success 'refuse to overwrite: worktree in bisect' '
- ) &&
-
- test_must_fail git branch -f wt-4 HEAD 2>err &&
-- grep "cannot force update the branch '\''wt-4'\'' checked out at" err
-+ grep "cannot force update the branch '\''wt-4'\'' checked out at" err &&
++test_expect_success !SANITIZE_LEAK 'refuse to fetch over ref: worktree in bisect' '
++ test_when_finished rm -rf .git/worktrees/wt-*/BISECT_* &&
+
-+ test_must_fail git fetch server +refs/heads/wt-4:refs/heads/wt-4 2>err &&
-+ grep "refusing to fetch into branch '\''refs/heads/wt-4'\''" err
- '
-
- . "$TEST_DIRECTORY"/lib-rebase.sh
-@@ t/t2407-worktree-heads.sh: test_expect_success 'refuse to overwrite: worktree in rebase' '
- ) &&
-
- test_must_fail git branch -f wt-4 HEAD 2>err &&
-- grep "cannot force update the branch '\''wt-4'\'' checked out at" err
-+ grep "cannot force update the branch '\''wt-4'\'' checked out at" err &&
++ touch .git/worktrees/wt-4/BISECT_LOG &&
++ echo refs/heads/fake-2 >.git/worktrees/wt-4/BISECT_START &&
++
++ test_must_fail git fetch server +refs/heads/fake-2:refs/heads/fake-2 2>err &&
++ grep "refusing to fetch into branch" err
++'
++
++test_expect_success !SANITIZE_LEAK 'refuse to fetch over ref: worktree in rebase' '
++ test_when_finished rm -rf .git/worktrees/wt-*/rebase-merge &&
++
++ mkdir -p .git/worktrees/wt-4/rebase-merge &&
++ touch .git/worktrees/wt-4/rebase-merge/interactive &&
++ echo refs/heads/fake-1 >.git/worktrees/wt-4/rebase-merge/head-name &&
++ echo refs/heads/fake-2 >.git/worktrees/wt-4/rebase-merge/onto &&
++
++ test_must_fail git fetch server +refs/heads/fake-1:refs/heads/fake-1 2>err &&
++ grep "refusing to fetch into branch" err
++'
+
-+ test_must_fail git fetch server +refs/heads/wt-4:refs/heads/wt-4 2>err &&
-+ grep "refusing to fetch into branch '\''refs/heads/wt-4'\''" err
- '
-
test_done
4: af645b43032 ! 4: d7e63f9dfd6 branch: use branch_checked_out() when deleting refs
@@ builtin/branch.c: static int delete_branches(int argc, const char **argv, int fo
- const struct worktree *wt =
- find_shared_symref(worktrees, "HEAD", name);
- if (wt) {
-+ char *path;
-+ if (branch_checked_out(name, &path)) {
++ const char *path;
++ if ((path = branch_checked_out(name))) {
error(_("Cannot delete branch '%s' "
"checked out at '%s'"),
- bname.buf, wt->path);
+ bname.buf, path);
-+ free(path);
ret = 1;
continue;
} -: ----------- > 5: 0aa9478bc38 branch: fix branch_checked_out() leaks
Signature · 1 line
gitgitgadget
PATCH v2 1/5 branch: add branch_checked_out() helper
From: Derrick Stolee <[email protected]>
The validate_new_branchname() method contains a check to see if a branch is checked out in any non-bare worktree. This is intended to prevent a force push that will mess up an existing checkout. This helper is not suitable to performing just that check, because the method will die() when the branch is checked out instead of returning an error code.
Create a new branch_checked_out() helper that performs the most basic form of this check. To ensure we can call branch_checked_out() in a loop with good performance, do a single preparation step that iterates over all worktrees and stores their current HEAD branches in a strmap. The branch_checked_out() helper can then discover these branches using a hash lookup.
This helper is currently missing some key functionality. Namely: it doesn't look for active rebases or bisects which mean that the branch is "checked out" even though HEAD doesn't point to that ref. This functionality will be added in a coming change.
We could use branch_checked_out() in validate_new_branchname(), but this missing functionality would be a regression. However, we have no tests that cover this case!
Add a new test script that will be expanded with these cross-worktree ref updates. The current tests would still pass if we refactored validate_new_branchname() to use this version of branch_checked_out(). The next change will fix that functionality and add the proper test coverage.
--- branch.c | 36 ++++++++++++++++++++++++++++++++++++ branch.h | 7 +++++++ t/t2407-worktree-heads.sh | 29 +++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100755 t/t2407-worktree-heads.sh
branch.c +36 -0
@@ -10,6 +10,7 @@#include "worktree.h" #include "submodule-config.h" #include "run-command.h" +#include "strmap.h" struct tracking {struct refspec_item spec;
@@ -369,6 +370,41 @@ int validate_branchname(const char *name, struct strbuf *ref)return ref_exists (ref->buf); } +static int initialized_checked_out_branches; +static struct strmap current_checked_out_branches =STRMAP_INIT ; + +static void prepare_checked_out_branches (void ) +{ +int i =0 ; +struct worktree **worktrees; + +if (initialized_checked_out_branches) +return ; + initialized_checked_out_branches =1 ; + + worktrees =get_worktrees (); + +while (worktrees[i]) { +struct worktree *wt = worktrees[i++]; + +if (wt->is_bare) +continue ; + +if (wt->head_ref) +strmap_put (¤t_checked_out_branches, + wt->head_ref, +xstrdup (wt->path)); + } + +free_worktrees (worktrees); +} + +const char *branch_checked_out (const char *refname) +{ +prepare_checked_out_branches (); +return strmap_get (¤t_checked_out_branches, refname); +} +/* * Check if a branch 'name' can be created as a new branch; die otherwise. * 'force' can be used when it is OK for the named branch already exists.
branch.h +7 -0
@@ -101,6 +101,13 @@ void create_branches_recursively(struct repository *r, const char *name, const char *tracking_name, int force, int reflog, int quiet, enum branch_track track, int dry_run); + +/* +* If the branch at 'refname' is currently checked out in a worktree, +* then return the path to that worktree. +*/ +const char *branch_checked_out (const char *refname); +/* * Check if 'name' can be a valid name for a branch; die otherwise. * Return 1 if the named branch already exists; return 0 otherwise.
t/t2407-worktree-heads.sh new file +29 -0
@@ -0,0 +1,29 @@ +#!/bin/sh + +test_description='test operations trying to overwrite refs at worktree HEAD' + +TEST_PASSES_SANITIZE_LEAK=true +. ./test-lib.sh + +test_expect_success 'setup' ' + test_commit init && + git branch -f fake-1 && + git branch -f fake-2 && + + for i in 1 2 3 4 + do + test_commit $i && + git branch wt-$i && + git worktree add wt-$i wt-$i || return 1 + done +' + +test_expect_success 'refuse to overwrite: checked out in worktree' ' + for i in 1 2 3 4 + do + test_must_fail git branch -f wt-$i HEAD 2>err + grep "cannot force update the branch" err || return 1 + done +' + +test_done
Signature · 1 line
gitgitgadget
PATCH v2 2/5 branch: check for bisects and rebases
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
The branch_checked_out() helper was added by the previous change, but it used an over-simplified view to check if a branch is checked out. It only focused on the HEAD symref, but ignored whether a bisect or rebase was happening.
Teach branch_checked_out() to check for these things, and also add tests to ensure that we do not lose this functionality in the future.
Now that this test coverage exists, we can safely refactor validate_new_branchname() to use branch_checked_out().
Note that we need to prepend "refs/heads/" to the 'state.branch' after calling wt_status_check_*(). We also need to duplicate wt->path so the value is not freed at the end of the call.
--- branch.c | 35 +++++++++++++++++++++++++++-------- t/t2407-worktree-heads.sh | 22 ++++++++++++++++++++++ 2 files changed, 49 insertions(+), 8 deletions(-)
branch.c +27 -8
@@ -385,6 +385,7 @@ static void prepare_checked_out_branches(void) worktrees =get_worktrees ();while (worktrees[i]) { +struct wt_status_state state = {0 };struct worktree *wt = worktrees[i++];if (wt->is_bare)
@@ -394,6 +395,29 @@ static void prepare_checked_out_branches(void)strmap_put (¤t_checked_out_branches, wt->head_ref,xstrdup (wt->path)); + +if (wt_status_check_rebase (wt, &state) && + (state.rebase_in_progress || state.rebase_interactive_in_progress) && + state.branch) { +struct strbuf ref =STRBUF_INIT ; +strbuf_addf (&ref,"refs/heads/%s" , state.branch); +strmap_put (¤t_checked_out_branches, + ref.buf, +xstrdup (wt->path)); +strbuf_release (&ref); + } +wt_status_state_free_buffers (&state); + +if (wt_status_check_bisect (wt, &state) && + state.branch) { +struct strbuf ref =STRBUF_INIT ; +strbuf_addf (&ref,"refs/heads/%s" , state.branch); +strmap_put (¤t_checked_out_branches, + ref.buf, +xstrdup (wt->path)); +strbuf_release (&ref); + } +wt_status_state_free_buffers (&state); }free_worktrees (worktrees);
@@ -413,9 +437,7 @@ const char *branch_checked_out(const char *refname)*/ int validate_new_branchname (const char *name,struct strbuf *ref,int force) { -struct worktree **worktrees; -const struct worktree *wt; - +const char *path;if (!validate_branchname (name, ref))return 0 ;
@@ -423,13 +445,10 @@ int validate_new_branchname(const char *name, struct strbuf *ref, int force)die (_ ("a branch named '%s' already exists" ), ref->buf +strlen ("refs/heads/" )); - worktrees =get_worktrees (); - wt =find_shared_symref (worktrees,"HEAD" , ref->buf); -if (wt && !wt->is_bare) +if ((path =branch_checked_out (ref->buf)))die (_ ("cannot force update the branch '%s' " "checked out at '%s'" ), - ref->buf +strlen ("refs/heads/" ), wt->path); -free_worktrees (worktrees); + ref->buf +strlen ("refs/heads/" ), path);return 1 ; }
t/t2407-worktree-heads.sh +22 -0
@@ -26,4 +26,26 @@ test_expect_success 'refuse to overwrite: checked out in worktree' ' done ' +test_expect_success 'refuse to overwrite: worktree in bisect' ' + test_when_finished rm -rf .git/worktrees/wt-*/BISECT_* && + + touch .git/worktrees/wt-4/BISECT_LOG && + echo refs/heads/fake-2 >.git/worktrees/wt-4/BISECT_START && + + test_must_fail git branch -f fake-2 HEAD 2>err && + grep "cannot force update the branch '\''fake-2'\'' checked out at.*wt-4" err +' + +test_expect_success 'refuse to overwrite: worktree in rebase' ' + test_when_finished rm -rf .git/worktrees/wt-*/rebase-merge && + + mkdir -p .git/worktrees/wt-3/rebase-merge && + touch .git/worktrees/wt-3/rebase-merge/interactive && + echo refs/heads/fake-1 >.git/worktrees/wt-3/rebase-merge/head-name && + echo refs/heads/fake-2 >.git/worktrees/wt-3/rebase-merge/onto && + + test_must_fail git branch -f fake-1 HEAD 2>err && + grep "cannot force update the branch '\''fake-1'\'' checked out at.*wt-3" err +' + test_done
Signature · 1 line
gitgitgadget
PATCH v2 3/5 fetch: use new branch_checked_out() and add tests
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
When fetching refs from a remote, it is possible that the refspec will cause use to overwrite a ref that is checked out in a worktree. The existing logic in builtin/fetch.c uses a possibly-slow mechanism. Update those sections to use the new, more efficient branch_checked_out() helper.
These uses were not previously tested, so add a test case that can be used for these kinds of collisions. There is only one test now, but more tests will be added as other consumers of branch_checked_out() are added.
Note that there are two uses in builtin/fetch.c, but only one of the messages is tested. This is because the tested check is run before completing the fetch, and the untested check is not reachable without concurrent updates to the filesystem. Thus, it is beneficial to keep that extra check for the sake of defense-in-depth. However, we should not attempt to test the check, as the effort required is too complicated to be worth the effort. This use in update_local_ref() also requires a change in the error message because we no longer have access to the worktree struct, only the path of the worktree. This error is so rare that making a distinction between the two is not critical.
--- builtin/fetch.c | 22 +++++++----------- t/t2407-worktree-heads.sh | 47 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 14 deletions(-)
builtin/fetch.c +8 -14
@@ -885,7 +885,6 @@ static int update_local_ref(struct ref *ref, struct worktree **worktrees) {struct commit *current =NULL , *updated; -const struct worktree *wt;const char *pretty_ref =prettify_refname (ref->name);int fast_forward =0 ;
@@ -900,16 +899,14 @@ static int update_local_ref(struct ref *ref, }if (!update_head_ok && - (wt =find_shared_symref (worktrees,"HEAD" , ref->name)) && - !wt->is_bare && !is_null_oid (&ref->old_oid)) { + !is_null_oid (&ref->old_oid) && +branch_checked_out (ref->name)) {/* * If this is the head, and it's not okay to update * the head, and the old value of the head isn't empty... */ format_display (display,'!' ,_ ("[rejected]" ), - wt->is_current ? -_ ("can't fetch in current branch" ) : -_ ("checked out in another worktree" ), +_ ("can't fetch into checked-out branch" ), remote, pretty_ref, summary_width);return 1 ; }
@@ -1434,19 +1431,16 @@ cleanup:return result; } -static void check_not_current_branch (struct ref *ref_map, -struct worktree **worktrees) +static void check_not_current_branch (struct ref *ref_map) { -const struct worktree *wt; +const char *path;for (; ref_map; ref_map = ref_map->next)if (ref_map->peer_ref &&starts_with (ref_map->peer_ref->name,"refs/heads/" ) && - (wt =find_shared_symref (worktrees,"HEAD" , - ref_map->peer_ref->name)) && - !wt->is_bare) + (path =branch_checked_out (ref_map->peer_ref->name)))die (_ ("refusing to fetch into branch '%s' " "checked out at '%s'" ), - ref_map->peer_ref->name, wt->path); + ref_map->peer_ref->name, path); }static int truncate_fetch_head (void )
@@ -1650,7 +1644,7 @@ static int do_fetch(struct transport *transport, ref_map =get_ref_map (transport->remote, remote_refs, rs, tags, &autotags);if (!update_head_ok) -check_not_current_branch (ref_map, worktrees); +check_not_current_branch (ref_map); retcode =open_fetch_head (&fetch_head);if (retcode)
t/t2407-worktree-heads.sh +47 -0
57 lines not on this page · raw message
Signature · 1 line
gitgitgadget
PATCH v2 4/5 branch: use branch_checked_out() when deleting refs
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
This is the last current use of find_shared_symref() that can easily be replaced by branch_checked_out(). The benefit of this switch is that the code is a bit simpler, but also it is faster on repeated calls.
The remaining uses of find_shared_symref() are non-trivial to remove, so we probably should not continue in that direction:
* builtin/notes.c uses find_shared_symref() with "NOTES_MERGE_REF" instead of "HEAD", so it doesn't have an immediate analogue with branch_checked_out(). Perhaps we should consider extending it to include that symref in addition to HEAD, BISECT_HEAD, and REBASE_HEAD.
* receive-pack.c checks to see if a worktree has a checkout for the ref that is being updated. The tricky part is that it can actually decide to update the worktree directly instead of just skipping the update. This all depends on the receive.denyCurrentBranch config option. The implementation currenty cares about receiving the worktree in the result, so the current branch_checked_out() prototype is insufficient currently. This is something to investigate later, though, since a large number of refs could be updated at the same time and using the strmap implementation of branch_checked_out() could be beneficial.
--- builtin/branch.c | 7 +++---- t/t2407-worktree-heads.sh | 5 ++++- 2 files changed, 7 insertions(+), 5 deletions(-)
builtin/branch.c +3 -4
@@ -253,12 +253,11 @@ static int delete_branches(int argc, const char **argv, int force, int kinds, name =mkpathdup (fmt, bname.buf);if (kinds ==FILTER_REFS_BRANCHES ) { -const struct worktree *wt = -find_shared_symref (worktrees,"HEAD" , name); -if (wt) { +const char *path; +if ((path =branch_checked_out (name))) {error (_ ("Cannot delete branch '%s' " "checked out at '%s'" ), - bname.buf, wt->path); + bname.buf, path); ret =1 ;continue ; }
t/t2407-worktree-heads.sh +4 -1
@@ -37,7 +37,10 @@ test_expect_success 'refuse to overwrite: checked out in worktree' ' for i in 1 2 3 4 do test_must_fail git branch -f wt-$i HEAD 2>err - grep "cannot force update the branch" err || return 1 + grep "cannot force update the branch" err && + + test_must_fail git branch -D wt-$i 2>err + grep "Cannot delete branch" err || return 1 done '
Signature · 1 line
gitgitgadget
PATCH v2 5/5 branch: fix branch_checked_out() leaks
In reply to Derrick Stolee via GitGitGadgetFrom: Derrick Stolee <[email protected]>
The branch_checked_out() method populates a strmap linking a refname to a worktree that has that branch checked out. While unlikely, it is possible that a bug or filesystem manipulation could create a scenario where the same ref is checked out in multiple places. Further, there are some states in an interactive rebase where HEAD and REBASE_HEAD point to the same ref, leading to multiple insertions into the strmap. In either case, the strmap_put() method returns the old value which is leaked.
Update branch_checked_out() to consume that pointer and free it.
Add a test in t2407 that checks this erroneous case. The test "checks itself" by first confirming that the filesystem manipulations it makes trigger the branch_checked_out() logic, and then sets up similar manipulations to make it look like there are multiple worktrees pointing to the same ref.
While TEST_PASSES_SANITIZE_LEAK would be helpful to demonstrate the leakage and prevent it in the future, t2407 uses helpers such as 'git clone' that cause the test to fail under that mode.
--- branch.c | 25 +++++++++++++++---------- t/t2407-worktree-heads.sh | 28 ++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 10 deletions(-)
branch.c +15 -10
@@ -385,25 +385,29 @@ static void prepare_checked_out_branches(void) worktrees =get_worktrees ();while (worktrees[i]) { +char *old;struct wt_status_state state = {0 };struct worktree *wt = worktrees[i++];if (wt->is_bare)continue ; -if (wt->head_ref) -strmap_put (¤t_checked_out_branches, - wt->head_ref, -xstrdup (wt->path)); +if (wt->head_ref) { + old =strmap_put (¤t_checked_out_branches, + wt->head_ref, +xstrdup (wt->path)); +free (old); + }if (wt_status_check_rebase (wt, &state) && (state.rebase_in_progress || state.rebase_interactive_in_progress) && state.branch) {struct strbuf ref =STRBUF_INIT ;strbuf_addf (&ref,"refs/heads/%s" , state.branch); -strmap_put (¤t_checked_out_branches, - ref.buf, -xstrdup (wt->path)); + old =strmap_put (¤t_checked_out_branches, + ref.buf, +xstrdup (wt->path)); +free (old);strbuf_release (&ref); }wt_status_state_free_buffers (&state);
@@ -412,9 +416,10 @@ static void prepare_checked_out_branches(void) state.branch) {struct strbuf ref =STRBUF_INIT ;strbuf_addf (&ref,"refs/heads/%s" , state.branch); -strmap_put (¤t_checked_out_branches, - ref.buf, -xstrdup (wt->path)); + old =strmap_put (¤t_checked_out_branches, + ref.buf, +xstrdup (wt->path)); +free (old);strbuf_release (&ref); }wt_status_state_free_buffers (&state);
t/t2407-worktree-heads.sh +28 -0
@@ -98,4 +98,32 @@ test_expect_success !SANITIZE_LEAK 'refuse to fetch over ref: worktree in rebase grep "refusing to fetch into branch" err ' +test_expect_success 'refuse to overwrite when in error states' ' + test_when_finished rm -rf .git/worktrees/wt-*/rebase-merge && + test_when_finished rm -rf .git/worktrees/wt-*/BISECT_* && + + # Both branches are currently under rebase. + mkdir -p .git/worktrees/wt-3/rebase-merge && + touch .git/worktrees/wt-3/rebase-merge/interactive && + echo refs/heads/fake-1 >.git/worktrees/wt-3/rebase-merge/head-name && + echo refs/heads/fake-2 >.git/worktrees/wt-3/rebase-merge/onto && + mkdir -p .git/worktrees/wt-4/rebase-merge && + touch .git/worktrees/wt-4/rebase-merge/interactive && + echo refs/heads/fake-2 >.git/worktrees/wt-4/rebase-merge/head-name && + echo refs/heads/fake-1 >.git/worktrees/wt-4/rebase-merge/onto && + + # Both branches are currently under bisect. + touch .git/worktrees/wt-4/BISECT_LOG && + echo refs/heads/fake-2 >.git/worktrees/wt-4/BISECT_START && + touch .git/worktrees/wt-1/BISECT_LOG && + echo refs/heads/fake-1 >.git/worktrees/wt-1/BISECT_START && + + for i in 1 2 + do + test_must_fail git branch -f fake-$i HEAD 2>err && + grep "cannot force update the branch '\''fake-$i'\'' checked out at" err || + return 1 + done +' + test_done
Signature · 1 line
gitgitgadget
Hi Stolee
Quoted 38 lines · Derrick Stolee via GitGitGadget
This is a replacement for some patches from v2 of my 'git rebase --update-refs' topic [1]. After some feedback from Philip, I've decided to pull that topic while I rework how I track the refs to rewrite [2]. This series moves forward with the
branch_checked_out()helper that was a bit more complicated than expected at first glance. This series is a culmination of the discussion started by Junio at [3].[1]
https://lore.kernel.org/git/[email protected]
[2]
https://lore.kernel.org/git/[email protected]/
[3] https://lore.kernel.org/git/[email protected]/Here is the patch outline:
1. Add a basic form of
branch_checked_out()that only looks at the HEAD of each worktree. Since this doesn't look atBISECT_HEADorREBASE_HEAD, the helper isn't linked to any consumer in this patch. A test script is added that verifies the current behavior checks that are implemented, even if they are not connected yet.
2. Makebranch_checked_out()actually look atBISECT_HEADandREBASE_HEAD. Add tests for those cases, which was previously untested in the Git test suite. Usebranch_checked_out()in 'git branch -f' to demonstrate this helper works as expected.
3. Usebranch_checked_out()in 'git fetch' when checking refs that would be updated by the refspec. Add tests for that scenario, too.
4. Usebranch_checked_out()in 'git branch -D' to prevent deleting refs from under an existing checkout. The commit message here describes some barriers to removing other uses offind_shared_symref()that could be good investigations for later.
5. Be careful when constructing the strmap to free old values, even though this should only happen in error scenarios. Add tests that verify this behavior.Updates in v2
=============
Quoted 9 lines
*
branch_checked_out()has a new prototype where it returns a 'const char *' instead of copying the path.
* The test script is marked withTEST_PASSES_SANITIZE_LEAKand test are careful to avoid using 'git rebase' or 'git bisect' when possible.
* Tests that cannot avoid memory-loss from 'git fetch' are marked with the "!SANITIZE_LEAK" prereq.
* A previous replacement of 'wt->current' with 'path' is removed. This changes an error message, but it is a very rare scenario where this error would be output.
The changes look good to me (modulo Peff's fixup), thanks for working on this
Best Wishes
Phillip