From a0cc58450a8ac81ba405f1e161599263d1678686 Mon Sep 17 00:00:00 2001 From: Thomas Gummerer Date: Thu, 20 Dec 2018 13:48:13 +0000 Subject: move worktree tests to t24* The 'git worktree' command used to be just another mode in 'git checkout', namely 'git checkout --to'. When the tests for the latter were retrofitted for the former, the test name was adjusted, but the test number was kept, even though the test is testing a different command now. t/README states: "Second digit tells the particular command we are testing.", so 'git worktree' should have a separate number just for itself. Move the worktree tests to t24* to adhere to that guideline. We're going to make use of the free'd up numbers in a subsequent commit. Signed-off-by: Thomas Gummerer Signed-off-by: Junio C Hamano diff --git a/t/t2025-worktree-add.sh b/t/t2025-worktree-add.sh deleted file mode 100755 index 286bba3..0000000 --- a/t/t2025-worktree-add.sh +++ /dev/null @@ -1,573 +0,0 @@ -#!/bin/sh - -test_description='test git worktree add' - -. ./test-lib.sh - -. "$TEST_DIRECTORY"/lib-rebase.sh - -test_expect_success 'setup' ' - test_commit init -' - -test_expect_success '"add" an existing worktree' ' - mkdir -p existing/subtree && - test_must_fail git worktree add --detach existing master -' - -test_expect_success '"add" an existing empty worktree' ' - mkdir existing_empty && - git worktree add --detach existing_empty master -' - -test_expect_success '"add" using shorthand - fails when no previous branch' ' - test_must_fail git worktree add existing_short - -' - -test_expect_success '"add" using - shorthand' ' - git checkout -b newbranch && - echo hello >myworld && - git add myworld && - git commit -m myworld && - git checkout master && - git worktree add short-hand - && - echo refs/heads/newbranch >expect && - git -C short-hand rev-parse --symbolic-full-name HEAD >actual && - test_cmp expect actual -' - -test_expect_success '"add" refuses to checkout locked branch' ' - test_must_fail git worktree add zere master && - ! test -d zere && - ! test -d .git/worktrees/zere -' - -test_expect_success 'checking out paths not complaining about linked checkouts' ' - ( - cd existing_empty && - echo dirty >>init.t && - git checkout master -- init.t - ) -' - -test_expect_success '"add" worktree' ' - git rev-parse HEAD >expect && - git worktree add --detach here master && - ( - cd here && - test_cmp ../init.t init.t && - test_must_fail git symbolic-ref HEAD && - git rev-parse HEAD >actual && - test_cmp ../expect actual && - git fsck - ) -' - -test_expect_success '"add" worktree with lock' ' - git rev-parse HEAD >expect && - git worktree add --detach --lock here-with-lock master && - test -f .git/worktrees/here-with-lock/locked -' - -test_expect_success '"add" worktree from a subdir' ' - ( - mkdir sub && - cd sub && - git worktree add --detach here master && - cd here && - test_cmp ../../init.t init.t - ) -' - -test_expect_success '"add" from a linked checkout' ' - ( - cd here && - git worktree add --detach nested-here master && - cd nested-here && - git fsck - ) -' - -test_expect_success '"add" worktree creating new branch' ' - git worktree add -b newmaster there master && - ( - cd there && - test_cmp ../init.t init.t && - git symbolic-ref HEAD >actual && - echo refs/heads/newmaster >expect && - test_cmp expect actual && - git fsck - ) -' - -test_expect_success 'die the same branch is already checked out' ' - ( - cd here && - test_must_fail git checkout newmaster - ) -' - -test_expect_success SYMLINKS 'die the same branch is already checked out (symlink)' ' - head=$(git -C there rev-parse --git-path HEAD) && - ref=$(git -C there symbolic-ref HEAD) && - rm "$head" && - ln -s "$ref" "$head" && - test_must_fail git -C here checkout newmaster -' - -test_expect_success 'not die the same branch is already checked out' ' - ( - cd here && - git worktree add --force anothernewmaster newmaster - ) -' - -test_expect_success 'not die on re-checking out current branch' ' - ( - cd there && - git checkout newmaster - ) -' - -test_expect_success '"add" from a bare repo' ' - ( - git clone --bare . bare && - cd bare && - git worktree add -b bare-master ../there2 master - ) -' - -test_expect_success 'checkout from a bare repo without "add"' ' - ( - cd bare && - test_must_fail git checkout master - ) -' - -test_expect_success '"add" default branch of a bare repo' ' - ( - git clone --bare . bare2 && - cd bare2 && - git worktree add ../there3 master - ) -' - -test_expect_success 'checkout with grafts' ' - test_when_finished rm .git/info/grafts && - test_commit abc && - SHA1=$(git rev-parse HEAD) && - test_commit def && - test_commit xyz && - echo "$(git rev-parse HEAD) $SHA1" >.git/info/grafts && - cat >expected <<-\EOF && - xyz - abc - EOF - git log --format=%s -2 >actual && - test_cmp expected actual && - git worktree add --detach grafted master && - git --git-dir=grafted/.git log --format=%s -2 >actual && - test_cmp expected actual -' - -test_expect_success '"add" from relative HEAD' ' - test_commit a && - test_commit b && - test_commit c && - git rev-parse HEAD~1 >expected && - git worktree add relhead HEAD~1 && - git -C relhead rev-parse HEAD >actual && - test_cmp expected actual -' - -test_expect_success '"add -b" with omitted' ' - git worktree add -b burble flornk && - test_cmp_rev HEAD burble -' - -test_expect_success '"add --detach" with omitted' ' - git worktree add --detach fishhook && - git rev-parse HEAD >expected && - git -C fishhook rev-parse HEAD >actual && - test_cmp expected actual && - test_must_fail git -C fishhook symbolic-ref HEAD -' - -test_expect_success '"add" with omitted' ' - git worktree add wiffle/bat && - test_cmp_rev HEAD bat -' - -test_expect_success '"add" checks out existing branch of dwimd name' ' - git branch dwim HEAD~1 && - git worktree add dwim && - test_cmp_rev HEAD~1 dwim && - ( - cd dwim && - test_cmp_rev HEAD dwim - ) -' - -test_expect_success '"add " dwim fails with checked out branch' ' - git checkout -b test-branch && - test_must_fail git worktree add test-branch && - test_path_is_missing test-branch -' - -test_expect_success '"add --force" with existing dwimd name doesnt die' ' - git checkout test-branch && - git worktree add --force test-branch -' - -test_expect_success '"add" no auto-vivify with --detach and omitted' ' - git worktree add --detach mish/mash && - test_must_fail git rev-parse mash -- && - test_must_fail git -C mish/mash symbolic-ref HEAD -' - -test_expect_success '"add" -b/-B mutually exclusive' ' - test_must_fail git worktree add -b poodle -B poodle bamboo master -' - -test_expect_success '"add" -b/--detach mutually exclusive' ' - test_must_fail git worktree add -b poodle --detach bamboo master -' - -test_expect_success '"add" -B/--detach mutually exclusive' ' - test_must_fail git worktree add -B poodle --detach bamboo master -' - -test_expect_success '"add -B" fails if the branch is checked out' ' - git rev-parse newmaster >before && - test_must_fail git worktree add -B newmaster bamboo master && - git rev-parse newmaster >after && - test_cmp before after -' - -test_expect_success 'add -B' ' - git worktree add -B poodle bamboo2 master^ && - git -C bamboo2 symbolic-ref HEAD >actual && - echo refs/heads/poodle >expected && - test_cmp expected actual && - test_cmp_rev master^ poodle -' - -test_expect_success 'add --quiet' ' - git worktree add --quiet another-worktree master 2>actual && - test_must_be_empty actual -' - -test_expect_success 'local clone from linked checkout' ' - git clone --local here here-clone && - ( cd here-clone && git fsck ) -' - -test_expect_success 'local clone --shared from linked checkout' ' - git -C bare worktree add --detach ../baretree && - git clone --local --shared baretree bare-clone && - grep /bare/ bare-clone/.git/objects/info/alternates -' - -test_expect_success '"add" worktree with --no-checkout' ' - git worktree add --no-checkout -b swamp swamp && - ! test -e swamp/init.t && - git -C swamp reset --hard && - test_cmp init.t swamp/init.t -' - -test_expect_success '"add" worktree with --checkout' ' - git worktree add --checkout -b swmap2 swamp2 && - test_cmp init.t swamp2/init.t -' - -test_expect_success 'put a worktree under rebase' ' - git worktree add under-rebase && - ( - cd under-rebase && - set_fake_editor && - FAKE_LINES="edit 1" git rebase -i HEAD^ && - git worktree list | grep "under-rebase.*detached HEAD" - ) -' - -test_expect_success 'add a worktree, checking out a rebased branch' ' - test_must_fail git worktree add new-rebase under-rebase && - ! test -d new-rebase -' - -test_expect_success 'checking out a rebased branch from another worktree' ' - git worktree add new-place && - test_must_fail git -C new-place checkout under-rebase -' - -test_expect_success 'not allow to delete a branch under rebase' ' - ( - cd under-rebase && - test_must_fail git branch -D under-rebase - ) -' - -test_expect_success 'rename a branch under rebase not allowed' ' - test_must_fail git branch -M under-rebase rebase-with-new-name -' - -test_expect_success 'check out from current worktree branch ok' ' - ( - cd under-rebase && - git checkout under-rebase && - git checkout - && - git rebase --abort - ) -' - -test_expect_success 'checkout a branch under bisect' ' - git worktree add under-bisect && - ( - cd under-bisect && - git bisect start && - git bisect bad && - git bisect good HEAD~2 && - git worktree list | grep "under-bisect.*detached HEAD" && - test_must_fail git worktree add new-bisect under-bisect && - ! test -d new-bisect - ) -' - -test_expect_success 'rename a branch under bisect not allowed' ' - test_must_fail git branch -M under-bisect bisect-with-new-name -' -# Is branch "refs/heads/$1" set to pull from "$2/$3"? -test_branch_upstream () { - printf "%s\n" "$2" "refs/heads/$3" >expect.upstream && - { - git config "branch.$1.remote" && - git config "branch.$1.merge" - } >actual.upstream && - test_cmp expect.upstream actual.upstream -} - -test_expect_success '--track sets up tracking' ' - test_when_finished rm -rf track && - git worktree add --track -b track track master && - test_branch_upstream track . master -' - -# setup remote repository $1 and repository $2 with $1 set up as -# remote. The remote has two branches, master and foo. -setup_remote_repo () { - git init $1 && - ( - cd $1 && - test_commit $1_master && - git checkout -b foo && - test_commit upstream_foo - ) && - git init $2 && - ( - cd $2 && - test_commit $2_master && - git remote add $1 ../$1 && - git config remote.$1.fetch \ - "refs/heads/*:refs/remotes/$1/*" && - git fetch --all - ) -} - -test_expect_success '--no-track avoids setting up tracking' ' - test_when_finished rm -rf repo_upstream repo_local foo && - setup_remote_repo repo_upstream repo_local && - ( - cd repo_local && - git worktree add --no-track -b foo ../foo repo_upstream/foo - ) && - ( - cd foo && - test_must_fail git config "branch.foo.remote" && - test_must_fail git config "branch.foo.merge" && - test_cmp_rev refs/remotes/repo_upstream/foo refs/heads/foo - ) -' - -test_expect_success '"add" fails' ' - test_must_fail git worktree add foo non-existent -' - -test_expect_success '"add" dwims' ' - test_when_finished rm -rf repo_upstream repo_dwim foo && - setup_remote_repo repo_upstream repo_dwim && - git init repo_dwim && - ( - cd repo_dwim && - git worktree add ../foo foo - ) && - ( - cd foo && - test_branch_upstream foo repo_upstream foo && - test_cmp_rev refs/remotes/repo_upstream/foo refs/heads/foo - ) -' - -test_expect_success '"add" dwims with checkout.defaultRemote' ' - test_when_finished rm -rf repo_upstream repo_dwim foo && - setup_remote_repo repo_upstream repo_dwim && - git init repo_dwim && - ( - cd repo_dwim && - git remote add repo_upstream2 ../repo_upstream && - git fetch repo_upstream2 && - test_must_fail git worktree add ../foo foo && - git -c checkout.defaultRemote=repo_upstream worktree add ../foo foo && - git status -uno --porcelain >status.actual && - test_must_be_empty status.actual - ) && - ( - cd foo && - test_branch_upstream foo repo_upstream foo && - test_cmp_rev refs/remotes/repo_upstream/foo refs/heads/foo - ) -' - -test_expect_success 'git worktree add does not match remote' ' - test_when_finished rm -rf repo_a repo_b foo && - setup_remote_repo repo_a repo_b && - ( - cd repo_b && - git worktree add ../foo - ) && - ( - cd foo && - test_must_fail git config "branch.foo.remote" && - test_must_fail git config "branch.foo.merge" && - ! test_cmp_rev refs/remotes/repo_a/foo refs/heads/foo - ) -' - -test_expect_success 'git worktree add --guess-remote sets up tracking' ' - test_when_finished rm -rf repo_a repo_b foo && - setup_remote_repo repo_a repo_b && - ( - cd repo_b && - git worktree add --guess-remote ../foo - ) && - ( - cd foo && - test_branch_upstream foo repo_a foo && - test_cmp_rev refs/remotes/repo_a/foo refs/heads/foo - ) -' - -test_expect_success 'git worktree add with worktree.guessRemote sets up tracking' ' - test_when_finished rm -rf repo_a repo_b foo && - setup_remote_repo repo_a repo_b && - ( - cd repo_b && - git config worktree.guessRemote true && - git worktree add ../foo - ) && - ( - cd foo && - test_branch_upstream foo repo_a foo && - test_cmp_rev refs/remotes/repo_a/foo refs/heads/foo - ) -' - -test_expect_success 'git worktree --no-guess-remote option overrides config' ' - test_when_finished rm -rf repo_a repo_b foo && - setup_remote_repo repo_a repo_b && - ( - cd repo_b && - git config worktree.guessRemote true && - git worktree add --no-guess-remote ../foo - ) && - ( - cd foo && - test_must_fail git config "branch.foo.remote" && - test_must_fail git config "branch.foo.merge" && - ! test_cmp_rev refs/remotes/repo_a/foo refs/heads/foo - ) -' - -post_checkout_hook () { - gitdir=${1:-.git} - test_when_finished "rm -f $gitdir/hooks/post-checkout" && - mkdir -p $gitdir/hooks && - write_script $gitdir/hooks/post-checkout <<-\EOF - { - echo $* - git rev-parse --git-dir --show-toplevel - } >hook.actual - EOF -} - -test_expect_success '"add" invokes post-checkout hook (branch)' ' - post_checkout_hook && - { - echo $ZERO_OID $(git rev-parse HEAD) 1 && - echo $(pwd)/.git/worktrees/gumby && - echo $(pwd)/gumby - } >hook.expect && - git worktree add gumby && - test_cmp hook.expect gumby/hook.actual -' - -test_expect_success '"add" invokes post-checkout hook (detached)' ' - post_checkout_hook && - { - echo $ZERO_OID $(git rev-parse HEAD) 1 && - echo $(pwd)/.git/worktrees/grumpy && - echo $(pwd)/grumpy - } >hook.expect && - git worktree add --detach grumpy && - test_cmp hook.expect grumpy/hook.actual -' - -test_expect_success '"add --no-checkout" suppresses post-checkout hook' ' - post_checkout_hook && - rm -f hook.actual && - git worktree add --no-checkout gloopy && - test_path_is_missing gloopy/hook.actual -' - -test_expect_success '"add" in other worktree invokes post-checkout hook' ' - post_checkout_hook && - { - echo $ZERO_OID $(git rev-parse HEAD) 1 && - echo $(pwd)/.git/worktrees/guppy && - echo $(pwd)/guppy - } >hook.expect && - git -C gloopy worktree add --detach ../guppy && - test_cmp hook.expect guppy/hook.actual -' - -test_expect_success '"add" in bare repo invokes post-checkout hook' ' - rm -rf bare && - git clone --bare . bare && - { - echo $ZERO_OID $(git --git-dir=bare rev-parse HEAD) 1 && - echo $(pwd)/bare/worktrees/goozy && - echo $(pwd)/goozy - } >hook.expect && - post_checkout_hook bare && - git -C bare worktree add --detach ../goozy && - test_cmp hook.expect goozy/hook.actual -' - -test_expect_success '"add" an existing but missing worktree' ' - git worktree add --detach pneu && - test_must_fail git worktree add --detach pneu && - rm -fr pneu && - test_must_fail git worktree add --detach pneu && - git worktree add --force --detach pneu -' - -test_expect_success '"add" an existing locked but missing worktree' ' - git worktree add --detach gnoo && - git worktree lock gnoo && - test_when_finished "git worktree unlock gnoo || :" && - rm -fr gnoo && - test_must_fail git worktree add --detach gnoo && - test_must_fail git worktree add --force --detach gnoo && - git worktree add --force --force --detach gnoo -' - -test_done diff --git a/t/t2026-worktree-prune.sh b/t/t2026-worktree-prune.sh deleted file mode 100755 index b7d6d5d..0000000 --- a/t/t2026-worktree-prune.sh +++ /dev/null @@ -1,95 +0,0 @@ -#!/bin/sh - -test_description='prune $GIT_DIR/worktrees' - -. ./test-lib.sh - -test_expect_success initialize ' - git commit --allow-empty -m init -' - -test_expect_success 'worktree prune on normal repo' ' - git worktree prune && - test_must_fail git worktree prune abc -' - -test_expect_success 'prune files inside $GIT_DIR/worktrees' ' - mkdir .git/worktrees && - : >.git/worktrees/abc && - git worktree prune --verbose >actual && - cat >expect <.git/worktrees/def/def && - cat >expect <actual && - test_i18ncmp expect actual && - ! test -d .git/worktrees/def && - ! test -d .git/worktrees -' - -test_expect_success SANITY 'prune directories with unreadable gitdir' ' - mkdir -p .git/worktrees/def/abc && - : >.git/worktrees/def/def && - : >.git/worktrees/def/gitdir && - chmod u-r .git/worktrees/def/gitdir && - git worktree prune --verbose >actual && - test_i18ngrep "Removing worktrees/def: unable to read gitdir file" actual && - ! test -d .git/worktrees/def && - ! test -d .git/worktrees -' - -test_expect_success 'prune directories with invalid gitdir' ' - mkdir -p .git/worktrees/def/abc && - : >.git/worktrees/def/def && - : >.git/worktrees/def/gitdir && - git worktree prune --verbose >actual && - test_i18ngrep "Removing worktrees/def: invalid gitdir file" actual && - ! test -d .git/worktrees/def && - ! test -d .git/worktrees -' - -test_expect_success 'prune directories with gitdir pointing to nowhere' ' - mkdir -p .git/worktrees/def/abc && - : >.git/worktrees/def/def && - echo "$(pwd)"/nowhere >.git/worktrees/def/gitdir && - git worktree prune --verbose >actual && - test_i18ngrep "Removing worktrees/def: gitdir file points to non-existent location" actual && - ! test -d .git/worktrees/def && - ! test -d .git/worktrees -' - -test_expect_success 'not prune locked checkout' ' - test_when_finished rm -r .git/worktrees && - mkdir -p .git/worktrees/ghi && - : >.git/worktrees/ghi/locked && - git worktree prune && - test -d .git/worktrees/ghi -' - -test_expect_success 'not prune recent checkouts' ' - test_when_finished rm -r .git/worktrees && - git worktree add jlm HEAD && - test -d .git/worktrees/jlm && - rm -rf jlm && - git worktree prune --verbose --expire=2.days.ago && - test -d .git/worktrees/jlm -' - -test_expect_success 'not prune proper checkouts' ' - test_when_finished rm -r .git/worktrees && - git worktree add --detach "$PWD/nop" master && - git worktree prune && - test -d .git/worktrees/nop -' - -test_done diff --git a/t/t2027-worktree-list.sh b/t/t2027-worktree-list.sh deleted file mode 100755 index bb6fb9b..0000000 --- a/t/t2027-worktree-list.sh +++ /dev/null @@ -1,154 +0,0 @@ -#!/bin/sh - -test_description='test git worktree list' - -. ./test-lib.sh - -test_expect_success 'setup' ' - test_commit init -' - -test_expect_success 'rev-parse --git-common-dir on main worktree' ' - git rev-parse --git-common-dir >actual && - echo .git >expected && - test_cmp expected actual && - mkdir sub && - git -C sub rev-parse --git-common-dir >actual2 && - echo ../.git >expected2 && - test_cmp expected2 actual2 -' - -test_expect_success 'rev-parse --git-path objects linked worktree' ' - echo "$(git rev-parse --show-toplevel)/.git/objects" >expect && - test_when_finished "rm -rf linked-tree actual expect && git worktree prune" && - git worktree add --detach linked-tree master && - git -C linked-tree rev-parse --git-path objects >actual && - test_cmp expect actual -' - -test_expect_success '"list" all worktrees from main' ' - echo "$(git rev-parse --show-toplevel) $(git rev-parse --short HEAD) [$(git symbolic-ref --short HEAD)]" >expect && - test_when_finished "rm -rf here out actual expect && git worktree prune" && - git worktree add --detach here master && - echo "$(git -C here rev-parse --show-toplevel) $(git rev-parse --short HEAD) (detached HEAD)" >>expect && - git worktree list >out && - sed "s/ */ /g" actual && - test_cmp expect actual -' - -test_expect_success '"list" all worktrees from linked' ' - echo "$(git rev-parse --show-toplevel) $(git rev-parse --short HEAD) [$(git symbolic-ref --short HEAD)]" >expect && - test_when_finished "rm -rf here out actual expect && git worktree prune" && - git worktree add --detach here master && - echo "$(git -C here rev-parse --show-toplevel) $(git rev-parse --short HEAD) (detached HEAD)" >>expect && - git -C here worktree list >out && - sed "s/ */ /g" actual && - test_cmp expect actual -' - -test_expect_success '"list" all worktrees --porcelain' ' - echo "worktree $(git rev-parse --show-toplevel)" >expect && - echo "HEAD $(git rev-parse HEAD)" >>expect && - echo "branch $(git symbolic-ref HEAD)" >>expect && - echo >>expect && - test_when_finished "rm -rf here actual expect && git worktree prune" && - git worktree add --detach here master && - echo "worktree $(git -C here rev-parse --show-toplevel)" >>expect && - echo "HEAD $(git rev-parse HEAD)" >>expect && - echo "detached" >>expect && - echo >>expect && - git worktree list --porcelain >actual && - test_cmp expect actual -' - -test_expect_success 'bare repo setup' ' - git init --bare bare1 && - echo "data" >file1 && - git add file1 && - git commit -m"File1: add data" && - git push bare1 master && - git reset --hard HEAD^ -' - -test_expect_success '"list" all worktrees from bare main' ' - test_when_finished "rm -rf there out actual expect && git -C bare1 worktree prune" && - git -C bare1 worktree add --detach ../there master && - echo "$(pwd)/bare1 (bare)" >expect && - echo "$(git -C there rev-parse --show-toplevel) $(git -C there rev-parse --short HEAD) (detached HEAD)" >>expect && - git -C bare1 worktree list >out && - sed "s/ */ /g" actual && - test_cmp expect actual -' - -test_expect_success '"list" all worktrees --porcelain from bare main' ' - test_when_finished "rm -rf there actual expect && git -C bare1 worktree prune" && - git -C bare1 worktree add --detach ../there master && - echo "worktree $(pwd)/bare1" >expect && - echo "bare" >>expect && - echo >>expect && - echo "worktree $(git -C there rev-parse --show-toplevel)" >>expect && - echo "HEAD $(git -C there rev-parse HEAD)" >>expect && - echo "detached" >>expect && - echo >>expect && - git -C bare1 worktree list --porcelain >actual && - test_cmp expect actual -' - -test_expect_success '"list" all worktrees from linked with a bare main' ' - test_when_finished "rm -rf there out actual expect && git -C bare1 worktree prune" && - git -C bare1 worktree add --detach ../there master && - echo "$(pwd)/bare1 (bare)" >expect && - echo "$(git -C there rev-parse --show-toplevel) $(git -C there rev-parse --short HEAD) (detached HEAD)" >>expect && - git -C there worktree list >out && - sed "s/ */ /g" actual && - test_cmp expect actual -' - -test_expect_success 'bare repo cleanup' ' - rm -rf bare1 -' - -test_expect_success 'broken main worktree still at the top' ' - git init broken-main && - ( - cd broken-main && - test_commit new && - git worktree add linked && - cat >expected <<-EOF && - worktree $(pwd) - HEAD $ZERO_OID - - EOF - cd linked && - echo "worktree $(pwd)" >expected && - echo "ref: .broken" >../.git/HEAD && - git worktree list --porcelain >out && - head -n 3 out >actual && - test_cmp ../expected actual && - git worktree list >out && - head -n 1 out >actual.2 && - grep -F "(error)" actual.2 - ) -' - -test_expect_success 'linked worktrees are sorted' ' - mkdir sorted && - git init sorted/main && - ( - cd sorted/main && - test_tick && - test_commit new && - git worktree add ../first && - git worktree add ../second && - git worktree list --porcelain >out && - grep ^worktree out >actual - ) && - cat >expected <<-EOF && - worktree $(pwd)/sorted/main - worktree $(pwd)/sorted/first - worktree $(pwd)/sorted/second - EOF - test_cmp expected sorted/main/actual -' - -test_done diff --git a/t/t2028-worktree-move.sh b/t/t2028-worktree-move.sh deleted file mode 100755 index 33c0337..0000000 --- a/t/t2028-worktree-move.sh +++ /dev/null @@ -1,188 +0,0 @@ -#!/bin/sh - -test_description='test git worktree move, remove, lock and unlock' - -. ./test-lib.sh - -test_expect_success 'setup' ' - test_commit init && - git worktree add source && - git worktree list --porcelain >out && - grep "^worktree" out >actual && - cat <<-EOF >expected && - worktree $(pwd) - worktree $(pwd)/source - EOF - test_cmp expected actual -' - -test_expect_success 'lock main worktree' ' - test_must_fail git worktree lock . -' - -test_expect_success 'lock linked worktree' ' - git worktree lock --reason hahaha source && - echo hahaha >expected && - test_cmp expected .git/worktrees/source/locked -' - -test_expect_success 'lock linked worktree from another worktree' ' - rm .git/worktrees/source/locked && - git worktree add elsewhere && - git -C elsewhere worktree lock --reason hahaha ../source && - echo hahaha >expected && - test_cmp expected .git/worktrees/source/locked -' - -test_expect_success 'lock worktree twice' ' - test_must_fail git worktree lock source && - echo hahaha >expected && - test_cmp expected .git/worktrees/source/locked -' - -test_expect_success 'lock worktree twice (from the locked worktree)' ' - test_must_fail git -C source worktree lock . && - echo hahaha >expected && - test_cmp expected .git/worktrees/source/locked -' - -test_expect_success 'unlock main worktree' ' - test_must_fail git worktree unlock . -' - -test_expect_success 'unlock linked worktree' ' - git worktree unlock source && - test_path_is_missing .git/worktrees/source/locked -' - -test_expect_success 'unlock worktree twice' ' - test_must_fail git worktree unlock source && - test_path_is_missing .git/worktrees/source/locked -' - -test_expect_success 'move non-worktree' ' - mkdir abc && - test_must_fail git worktree move abc def -' - -test_expect_success 'move locked worktree' ' - git worktree lock source && - test_when_finished "git worktree unlock source" && - test_must_fail git worktree move source destination -' - -test_expect_success 'move worktree' ' - git worktree move source destination && - test_path_is_missing source && - git worktree list --porcelain >out && - grep "^worktree.*/destination$" out && - ! grep "^worktree.*/source$" out && - git -C destination log --format=%s >actual2 && - echo init >expected2 && - test_cmp expected2 actual2 -' - -test_expect_success 'move main worktree' ' - test_must_fail git worktree move . def -' - -test_expect_success 'move worktree to another dir' ' - mkdir some-dir && - git worktree move destination some-dir && - test_when_finished "git worktree move some-dir/destination destination" && - test_path_is_missing destination && - git worktree list --porcelain >out && - grep "^worktree.*/some-dir/destination$" out && - git -C some-dir/destination log --format=%s >actual2 && - echo init >expected2 && - test_cmp expected2 actual2 -' - -test_expect_success 'move locked worktree (force)' ' - test_when_finished " - git worktree unlock flump || : - git worktree remove flump || : - git worktree unlock ploof || : - git worktree remove ploof || : - " && - git worktree add --detach flump && - git worktree lock flump && - test_must_fail git worktree move flump ploof" && - test_must_fail git worktree move --force flump ploof" && - git worktree move --force --force flump ploof -' - -test_expect_success 'remove main worktree' ' - test_must_fail git worktree remove . -' - -test_expect_success 'remove locked worktree' ' - git worktree lock destination && - test_when_finished "git worktree unlock destination" && - test_must_fail git worktree remove destination -' - -test_expect_success 'remove worktree with dirty tracked file' ' - echo dirty >>destination/init.t && - test_when_finished "git -C destination checkout init.t" && - test_must_fail git worktree remove destination -' - -test_expect_success 'remove worktree with untracked file' ' - : >destination/untracked && - test_must_fail git worktree remove destination -' - -test_expect_success 'force remove worktree with untracked file' ' - git worktree remove --force destination && - test_path_is_missing destination -' - -test_expect_success 'remove missing worktree' ' - git worktree add to-be-gone && - test -d .git/worktrees/to-be-gone && - mv to-be-gone gone && - git worktree remove to-be-gone && - test_path_is_missing .git/worktrees/to-be-gone -' - -test_expect_success 'NOT remove missing-but-locked worktree' ' - git worktree add gone-but-locked && - git worktree lock gone-but-locked && - test -d .git/worktrees/gone-but-locked && - mv gone-but-locked really-gone-now && - test_must_fail git worktree remove gone-but-locked && - test_path_is_dir .git/worktrees/gone-but-locked -' - -test_expect_success 'proper error when worktree not found' ' - for i in noodle noodle/bork - do - test_must_fail git worktree lock $i 2>err && - test_i18ngrep "not a working tree" err || return 1 - done -' - -test_expect_success 'remove locked worktree (force)' ' - git worktree add --detach gumby && - test_when_finished "git worktree remove gumby || :" && - git worktree lock gumby && - test_when_finished "git worktree unlock gumby || :" && - test_must_fail git worktree remove gumby && - test_must_fail git worktree remove --force gumby && - git worktree remove --force --force gumby -' - -test_expect_success 'remove cleans up .git/worktrees when empty' ' - git init moog && - ( - cd moog && - test_commit bim && - git worktree add --detach goom && - test_path_exists .git/worktrees && - git worktree remove goom && - test_path_is_missing .git/worktrees - ) -' - -test_done diff --git a/t/t2029-worktree-config.sh b/t/t2029-worktree-config.sh deleted file mode 100755 index 286121d..0000000 --- a/t/t2029-worktree-config.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/bin/sh - -test_description="config file in multi worktree" - -. ./test-lib.sh - -test_expect_success 'setup' ' - test_commit start -' - -test_expect_success 'config --worktree in single worktree' ' - git config --worktree foo.bar true && - test_cmp_config true foo.bar -' - -test_expect_success 'add worktrees' ' - git worktree add wt1 && - git worktree add wt2 -' - -test_expect_success 'config --worktree without extension' ' - test_must_fail git config --worktree foo.bar false -' - -test_expect_success 'enable worktreeConfig extension' ' - git config extensions.worktreeConfig true && - test_cmp_config true extensions.worktreeConfig -' - -test_expect_success 'config is shared as before' ' - git config this.is shared && - test_cmp_config shared this.is && - test_cmp_config -C wt1 shared this.is && - test_cmp_config -C wt2 shared this.is -' - -test_expect_success 'config is shared (set from another worktree)' ' - git -C wt1 config that.is also-shared && - test_cmp_config also-shared that.is && - test_cmp_config -C wt1 also-shared that.is && - test_cmp_config -C wt2 also-shared that.is -' - -test_expect_success 'config private to main worktree' ' - git config --worktree this.is for-main && - test_cmp_config for-main this.is && - test_cmp_config -C wt1 shared this.is && - test_cmp_config -C wt2 shared this.is -' - -test_expect_success 'config private to linked worktree' ' - git -C wt1 config --worktree this.is for-wt1 && - test_cmp_config for-main this.is && - test_cmp_config -C wt1 for-wt1 this.is && - test_cmp_config -C wt2 shared this.is -' - -test_expect_success 'core.bare no longer for main only' ' - test_config core.bare true && - test "$(git rev-parse --is-bare-repository)" = true && - test "$(git -C wt1 rev-parse --is-bare-repository)" = true && - test "$(git -C wt2 rev-parse --is-bare-repository)" = true -' - -test_expect_success 'per-worktree core.bare is picked up' ' - git -C wt1 config --worktree core.bare true && - test "$(git rev-parse --is-bare-repository)" = false && - test "$(git -C wt1 rev-parse --is-bare-repository)" = true && - test "$(git -C wt2 rev-parse --is-bare-repository)" = false -' - -test_expect_success 'config.worktree no longer read without extension' ' - git config --unset extensions.worktreeConfig && - test_cmp_config shared this.is && - test_cmp_config -C wt1 shared this.is && - test_cmp_config -C wt2 shared this.is -' - -test_done diff --git a/t/t2400-worktree-add.sh b/t/t2400-worktree-add.sh new file mode 100755 index 0000000..286bba3 --- /dev/null +++ b/t/t2400-worktree-add.sh @@ -0,0 +1,573 @@ +#!/bin/sh + +test_description='test git worktree add' + +. ./test-lib.sh + +. "$TEST_DIRECTORY"/lib-rebase.sh + +test_expect_success 'setup' ' + test_commit init +' + +test_expect_success '"add" an existing worktree' ' + mkdir -p existing/subtree && + test_must_fail git worktree add --detach existing master +' + +test_expect_success '"add" an existing empty worktree' ' + mkdir existing_empty && + git worktree add --detach existing_empty master +' + +test_expect_success '"add" using shorthand - fails when no previous branch' ' + test_must_fail git worktree add existing_short - +' + +test_expect_success '"add" using - shorthand' ' + git checkout -b newbranch && + echo hello >myworld && + git add myworld && + git commit -m myworld && + git checkout master && + git worktree add short-hand - && + echo refs/heads/newbranch >expect && + git -C short-hand rev-parse --symbolic-full-name HEAD >actual && + test_cmp expect actual +' + +test_expect_success '"add" refuses to checkout locked branch' ' + test_must_fail git worktree add zere master && + ! test -d zere && + ! test -d .git/worktrees/zere +' + +test_expect_success 'checking out paths not complaining about linked checkouts' ' + ( + cd existing_empty && + echo dirty >>init.t && + git checkout master -- init.t + ) +' + +test_expect_success '"add" worktree' ' + git rev-parse HEAD >expect && + git worktree add --detach here master && + ( + cd here && + test_cmp ../init.t init.t && + test_must_fail git symbolic-ref HEAD && + git rev-parse HEAD >actual && + test_cmp ../expect actual && + git fsck + ) +' + +test_expect_success '"add" worktree with lock' ' + git rev-parse HEAD >expect && + git worktree add --detach --lock here-with-lock master && + test -f .git/worktrees/here-with-lock/locked +' + +test_expect_success '"add" worktree from a subdir' ' + ( + mkdir sub && + cd sub && + git worktree add --detach here master && + cd here && + test_cmp ../../init.t init.t + ) +' + +test_expect_success '"add" from a linked checkout' ' + ( + cd here && + git worktree add --detach nested-here master && + cd nested-here && + git fsck + ) +' + +test_expect_success '"add" worktree creating new branch' ' + git worktree add -b newmaster there master && + ( + cd there && + test_cmp ../init.t init.t && + git symbolic-ref HEAD >actual && + echo refs/heads/newmaster >expect && + test_cmp expect actual && + git fsck + ) +' + +test_expect_success 'die the same branch is already checked out' ' + ( + cd here && + test_must_fail git checkout newmaster + ) +' + +test_expect_success SYMLINKS 'die the same branch is already checked out (symlink)' ' + head=$(git -C there rev-parse --git-path HEAD) && + ref=$(git -C there symbolic-ref HEAD) && + rm "$head" && + ln -s "$ref" "$head" && + test_must_fail git -C here checkout newmaster +' + +test_expect_success 'not die the same branch is already checked out' ' + ( + cd here && + git worktree add --force anothernewmaster newmaster + ) +' + +test_expect_success 'not die on re-checking out current branch' ' + ( + cd there && + git checkout newmaster + ) +' + +test_expect_success '"add" from a bare repo' ' + ( + git clone --bare . bare && + cd bare && + git worktree add -b bare-master ../there2 master + ) +' + +test_expect_success 'checkout from a bare repo without "add"' ' + ( + cd bare && + test_must_fail git checkout master + ) +' + +test_expect_success '"add" default branch of a bare repo' ' + ( + git clone --bare . bare2 && + cd bare2 && + git worktree add ../there3 master + ) +' + +test_expect_success 'checkout with grafts' ' + test_when_finished rm .git/info/grafts && + test_commit abc && + SHA1=$(git rev-parse HEAD) && + test_commit def && + test_commit xyz && + echo "$(git rev-parse HEAD) $SHA1" >.git/info/grafts && + cat >expected <<-\EOF && + xyz + abc + EOF + git log --format=%s -2 >actual && + test_cmp expected actual && + git worktree add --detach grafted master && + git --git-dir=grafted/.git log --format=%s -2 >actual && + test_cmp expected actual +' + +test_expect_success '"add" from relative HEAD' ' + test_commit a && + test_commit b && + test_commit c && + git rev-parse HEAD~1 >expected && + git worktree add relhead HEAD~1 && + git -C relhead rev-parse HEAD >actual && + test_cmp expected actual +' + +test_expect_success '"add -b" with omitted' ' + git worktree add -b burble flornk && + test_cmp_rev HEAD burble +' + +test_expect_success '"add --detach" with omitted' ' + git worktree add --detach fishhook && + git rev-parse HEAD >expected && + git -C fishhook rev-parse HEAD >actual && + test_cmp expected actual && + test_must_fail git -C fishhook symbolic-ref HEAD +' + +test_expect_success '"add" with omitted' ' + git worktree add wiffle/bat && + test_cmp_rev HEAD bat +' + +test_expect_success '"add" checks out existing branch of dwimd name' ' + git branch dwim HEAD~1 && + git worktree add dwim && + test_cmp_rev HEAD~1 dwim && + ( + cd dwim && + test_cmp_rev HEAD dwim + ) +' + +test_expect_success '"add " dwim fails with checked out branch' ' + git checkout -b test-branch && + test_must_fail git worktree add test-branch && + test_path_is_missing test-branch +' + +test_expect_success '"add --force" with existing dwimd name doesnt die' ' + git checkout test-branch && + git worktree add --force test-branch +' + +test_expect_success '"add" no auto-vivify with --detach and omitted' ' + git worktree add --detach mish/mash && + test_must_fail git rev-parse mash -- && + test_must_fail git -C mish/mash symbolic-ref HEAD +' + +test_expect_success '"add" -b/-B mutually exclusive' ' + test_must_fail git worktree add -b poodle -B poodle bamboo master +' + +test_expect_success '"add" -b/--detach mutually exclusive' ' + test_must_fail git worktree add -b poodle --detach bamboo master +' + +test_expect_success '"add" -B/--detach mutually exclusive' ' + test_must_fail git worktree add -B poodle --detach bamboo master +' + +test_expect_success '"add -B" fails if the branch is checked out' ' + git rev-parse newmaster >before && + test_must_fail git worktree add -B newmaster bamboo master && + git rev-parse newmaster >after && + test_cmp before after +' + +test_expect_success 'add -B' ' + git worktree add -B poodle bamboo2 master^ && + git -C bamboo2 symbolic-ref HEAD >actual && + echo refs/heads/poodle >expected && + test_cmp expected actual && + test_cmp_rev master^ poodle +' + +test_expect_success 'add --quiet' ' + git worktree add --quiet another-worktree master 2>actual && + test_must_be_empty actual +' + +test_expect_success 'local clone from linked checkout' ' + git clone --local here here-clone && + ( cd here-clone && git fsck ) +' + +test_expect_success 'local clone --shared from linked checkout' ' + git -C bare worktree add --detach ../baretree && + git clone --local --shared baretree bare-clone && + grep /bare/ bare-clone/.git/objects/info/alternates +' + +test_expect_success '"add" worktree with --no-checkout' ' + git worktree add --no-checkout -b swamp swamp && + ! test -e swamp/init.t && + git -C swamp reset --hard && + test_cmp init.t swamp/init.t +' + +test_expect_success '"add" worktree with --checkout' ' + git worktree add --checkout -b swmap2 swamp2 && + test_cmp init.t swamp2/init.t +' + +test_expect_success 'put a worktree under rebase' ' + git worktree add under-rebase && + ( + cd under-rebase && + set_fake_editor && + FAKE_LINES="edit 1" git rebase -i HEAD^ && + git worktree list | grep "under-rebase.*detached HEAD" + ) +' + +test_expect_success 'add a worktree, checking out a rebased branch' ' + test_must_fail git worktree add new-rebase under-rebase && + ! test -d new-rebase +' + +test_expect_success 'checking out a rebased branch from another worktree' ' + git worktree add new-place && + test_must_fail git -C new-place checkout under-rebase +' + +test_expect_success 'not allow to delete a branch under rebase' ' + ( + cd under-rebase && + test_must_fail git branch -D under-rebase + ) +' + +test_expect_success 'rename a branch under rebase not allowed' ' + test_must_fail git branch -M under-rebase rebase-with-new-name +' + +test_expect_success 'check out from current worktree branch ok' ' + ( + cd under-rebase && + git checkout under-rebase && + git checkout - && + git rebase --abort + ) +' + +test_expect_success 'checkout a branch under bisect' ' + git worktree add under-bisect && + ( + cd under-bisect && + git bisect start && + git bisect bad && + git bisect good HEAD~2 && + git worktree list | grep "under-bisect.*detached HEAD" && + test_must_fail git worktree add new-bisect under-bisect && + ! test -d new-bisect + ) +' + +test_expect_success 'rename a branch under bisect not allowed' ' + test_must_fail git branch -M under-bisect bisect-with-new-name +' +# Is branch "refs/heads/$1" set to pull from "$2/$3"? +test_branch_upstream () { + printf "%s\n" "$2" "refs/heads/$3" >expect.upstream && + { + git config "branch.$1.remote" && + git config "branch.$1.merge" + } >actual.upstream && + test_cmp expect.upstream actual.upstream +} + +test_expect_success '--track sets up tracking' ' + test_when_finished rm -rf track && + git worktree add --track -b track track master && + test_branch_upstream track . master +' + +# setup remote repository $1 and repository $2 with $1 set up as +# remote. The remote has two branches, master and foo. +setup_remote_repo () { + git init $1 && + ( + cd $1 && + test_commit $1_master && + git checkout -b foo && + test_commit upstream_foo + ) && + git init $2 && + ( + cd $2 && + test_commit $2_master && + git remote add $1 ../$1 && + git config remote.$1.fetch \ + "refs/heads/*:refs/remotes/$1/*" && + git fetch --all + ) +} + +test_expect_success '--no-track avoids setting up tracking' ' + test_when_finished rm -rf repo_upstream repo_local foo && + setup_remote_repo repo_upstream repo_local && + ( + cd repo_local && + git worktree add --no-track -b foo ../foo repo_upstream/foo + ) && + ( + cd foo && + test_must_fail git config "branch.foo.remote" && + test_must_fail git config "branch.foo.merge" && + test_cmp_rev refs/remotes/repo_upstream/foo refs/heads/foo + ) +' + +test_expect_success '"add" fails' ' + test_must_fail git worktree add foo non-existent +' + +test_expect_success '"add" dwims' ' + test_when_finished rm -rf repo_upstream repo_dwim foo && + setup_remote_repo repo_upstream repo_dwim && + git init repo_dwim && + ( + cd repo_dwim && + git worktree add ../foo foo + ) && + ( + cd foo && + test_branch_upstream foo repo_upstream foo && + test_cmp_rev refs/remotes/repo_upstream/foo refs/heads/foo + ) +' + +test_expect_success '"add" dwims with checkout.defaultRemote' ' + test_when_finished rm -rf repo_upstream repo_dwim foo && + setup_remote_repo repo_upstream repo_dwim && + git init repo_dwim && + ( + cd repo_dwim && + git remote add repo_upstream2 ../repo_upstream && + git fetch repo_upstream2 && + test_must_fail git worktree add ../foo foo && + git -c checkout.defaultRemote=repo_upstream worktree add ../foo foo && + git status -uno --porcelain >status.actual && + test_must_be_empty status.actual + ) && + ( + cd foo && + test_branch_upstream foo repo_upstream foo && + test_cmp_rev refs/remotes/repo_upstream/foo refs/heads/foo + ) +' + +test_expect_success 'git worktree add does not match remote' ' + test_when_finished rm -rf repo_a repo_b foo && + setup_remote_repo repo_a repo_b && + ( + cd repo_b && + git worktree add ../foo + ) && + ( + cd foo && + test_must_fail git config "branch.foo.remote" && + test_must_fail git config "branch.foo.merge" && + ! test_cmp_rev refs/remotes/repo_a/foo refs/heads/foo + ) +' + +test_expect_success 'git worktree add --guess-remote sets up tracking' ' + test_when_finished rm -rf repo_a repo_b foo && + setup_remote_repo repo_a repo_b && + ( + cd repo_b && + git worktree add --guess-remote ../foo + ) && + ( + cd foo && + test_branch_upstream foo repo_a foo && + test_cmp_rev refs/remotes/repo_a/foo refs/heads/foo + ) +' + +test_expect_success 'git worktree add with worktree.guessRemote sets up tracking' ' + test_when_finished rm -rf repo_a repo_b foo && + setup_remote_repo repo_a repo_b && + ( + cd repo_b && + git config worktree.guessRemote true && + git worktree add ../foo + ) && + ( + cd foo && + test_branch_upstream foo repo_a foo && + test_cmp_rev refs/remotes/repo_a/foo refs/heads/foo + ) +' + +test_expect_success 'git worktree --no-guess-remote option overrides config' ' + test_when_finished rm -rf repo_a repo_b foo && + setup_remote_repo repo_a repo_b && + ( + cd repo_b && + git config worktree.guessRemote true && + git worktree add --no-guess-remote ../foo + ) && + ( + cd foo && + test_must_fail git config "branch.foo.remote" && + test_must_fail git config "branch.foo.merge" && + ! test_cmp_rev refs/remotes/repo_a/foo refs/heads/foo + ) +' + +post_checkout_hook () { + gitdir=${1:-.git} + test_when_finished "rm -f $gitdir/hooks/post-checkout" && + mkdir -p $gitdir/hooks && + write_script $gitdir/hooks/post-checkout <<-\EOF + { + echo $* + git rev-parse --git-dir --show-toplevel + } >hook.actual + EOF +} + +test_expect_success '"add" invokes post-checkout hook (branch)' ' + post_checkout_hook && + { + echo $ZERO_OID $(git rev-parse HEAD) 1 && + echo $(pwd)/.git/worktrees/gumby && + echo $(pwd)/gumby + } >hook.expect && + git worktree add gumby && + test_cmp hook.expect gumby/hook.actual +' + +test_expect_success '"add" invokes post-checkout hook (detached)' ' + post_checkout_hook && + { + echo $ZERO_OID $(git rev-parse HEAD) 1 && + echo $(pwd)/.git/worktrees/grumpy && + echo $(pwd)/grumpy + } >hook.expect && + git worktree add --detach grumpy && + test_cmp hook.expect grumpy/hook.actual +' + +test_expect_success '"add --no-checkout" suppresses post-checkout hook' ' + post_checkout_hook && + rm -f hook.actual && + git worktree add --no-checkout gloopy && + test_path_is_missing gloopy/hook.actual +' + +test_expect_success '"add" in other worktree invokes post-checkout hook' ' + post_checkout_hook && + { + echo $ZERO_OID $(git rev-parse HEAD) 1 && + echo $(pwd)/.git/worktrees/guppy && + echo $(pwd)/guppy + } >hook.expect && + git -C gloopy worktree add --detach ../guppy && + test_cmp hook.expect guppy/hook.actual +' + +test_expect_success '"add" in bare repo invokes post-checkout hook' ' + rm -rf bare && + git clone --bare . bare && + { + echo $ZERO_OID $(git --git-dir=bare rev-parse HEAD) 1 && + echo $(pwd)/bare/worktrees/goozy && + echo $(pwd)/goozy + } >hook.expect && + post_checkout_hook bare && + git -C bare worktree add --detach ../goozy && + test_cmp hook.expect goozy/hook.actual +' + +test_expect_success '"add" an existing but missing worktree' ' + git worktree add --detach pneu && + test_must_fail git worktree add --detach pneu && + rm -fr pneu && + test_must_fail git worktree add --detach pneu && + git worktree add --force --detach pneu +' + +test_expect_success '"add" an existing locked but missing worktree' ' + git worktree add --detach gnoo && + git worktree lock gnoo && + test_when_finished "git worktree unlock gnoo || :" && + rm -fr gnoo && + test_must_fail git worktree add --detach gnoo && + test_must_fail git worktree add --force --detach gnoo && + git worktree add --force --force --detach gnoo +' + +test_done diff --git a/t/t2401-worktree-prune.sh b/t/t2401-worktree-prune.sh new file mode 100755 index 0000000..b7d6d5d --- /dev/null +++ b/t/t2401-worktree-prune.sh @@ -0,0 +1,95 @@ +#!/bin/sh + +test_description='prune $GIT_DIR/worktrees' + +. ./test-lib.sh + +test_expect_success initialize ' + git commit --allow-empty -m init +' + +test_expect_success 'worktree prune on normal repo' ' + git worktree prune && + test_must_fail git worktree prune abc +' + +test_expect_success 'prune files inside $GIT_DIR/worktrees' ' + mkdir .git/worktrees && + : >.git/worktrees/abc && + git worktree prune --verbose >actual && + cat >expect <.git/worktrees/def/def && + cat >expect <actual && + test_i18ncmp expect actual && + ! test -d .git/worktrees/def && + ! test -d .git/worktrees +' + +test_expect_success SANITY 'prune directories with unreadable gitdir' ' + mkdir -p .git/worktrees/def/abc && + : >.git/worktrees/def/def && + : >.git/worktrees/def/gitdir && + chmod u-r .git/worktrees/def/gitdir && + git worktree prune --verbose >actual && + test_i18ngrep "Removing worktrees/def: unable to read gitdir file" actual && + ! test -d .git/worktrees/def && + ! test -d .git/worktrees +' + +test_expect_success 'prune directories with invalid gitdir' ' + mkdir -p .git/worktrees/def/abc && + : >.git/worktrees/def/def && + : >.git/worktrees/def/gitdir && + git worktree prune --verbose >actual && + test_i18ngrep "Removing worktrees/def: invalid gitdir file" actual && + ! test -d .git/worktrees/def && + ! test -d .git/worktrees +' + +test_expect_success 'prune directories with gitdir pointing to nowhere' ' + mkdir -p .git/worktrees/def/abc && + : >.git/worktrees/def/def && + echo "$(pwd)"/nowhere >.git/worktrees/def/gitdir && + git worktree prune --verbose >actual && + test_i18ngrep "Removing worktrees/def: gitdir file points to non-existent location" actual && + ! test -d .git/worktrees/def && + ! test -d .git/worktrees +' + +test_expect_success 'not prune locked checkout' ' + test_when_finished rm -r .git/worktrees && + mkdir -p .git/worktrees/ghi && + : >.git/worktrees/ghi/locked && + git worktree prune && + test -d .git/worktrees/ghi +' + +test_expect_success 'not prune recent checkouts' ' + test_when_finished rm -r .git/worktrees && + git worktree add jlm HEAD && + test -d .git/worktrees/jlm && + rm -rf jlm && + git worktree prune --verbose --expire=2.days.ago && + test -d .git/worktrees/jlm +' + +test_expect_success 'not prune proper checkouts' ' + test_when_finished rm -r .git/worktrees && + git worktree add --detach "$PWD/nop" master && + git worktree prune && + test -d .git/worktrees/nop +' + +test_done diff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh new file mode 100755 index 0000000..bb6fb9b --- /dev/null +++ b/t/t2402-worktree-list.sh @@ -0,0 +1,154 @@ +#!/bin/sh + +test_description='test git worktree list' + +. ./test-lib.sh + +test_expect_success 'setup' ' + test_commit init +' + +test_expect_success 'rev-parse --git-common-dir on main worktree' ' + git rev-parse --git-common-dir >actual && + echo .git >expected && + test_cmp expected actual && + mkdir sub && + git -C sub rev-parse --git-common-dir >actual2 && + echo ../.git >expected2 && + test_cmp expected2 actual2 +' + +test_expect_success 'rev-parse --git-path objects linked worktree' ' + echo "$(git rev-parse --show-toplevel)/.git/objects" >expect && + test_when_finished "rm -rf linked-tree actual expect && git worktree prune" && + git worktree add --detach linked-tree master && + git -C linked-tree rev-parse --git-path objects >actual && + test_cmp expect actual +' + +test_expect_success '"list" all worktrees from main' ' + echo "$(git rev-parse --show-toplevel) $(git rev-parse --short HEAD) [$(git symbolic-ref --short HEAD)]" >expect && + test_when_finished "rm -rf here out actual expect && git worktree prune" && + git worktree add --detach here master && + echo "$(git -C here rev-parse --show-toplevel) $(git rev-parse --short HEAD) (detached HEAD)" >>expect && + git worktree list >out && + sed "s/ */ /g" actual && + test_cmp expect actual +' + +test_expect_success '"list" all worktrees from linked' ' + echo "$(git rev-parse --show-toplevel) $(git rev-parse --short HEAD) [$(git symbolic-ref --short HEAD)]" >expect && + test_when_finished "rm -rf here out actual expect && git worktree prune" && + git worktree add --detach here master && + echo "$(git -C here rev-parse --show-toplevel) $(git rev-parse --short HEAD) (detached HEAD)" >>expect && + git -C here worktree list >out && + sed "s/ */ /g" actual && + test_cmp expect actual +' + +test_expect_success '"list" all worktrees --porcelain' ' + echo "worktree $(git rev-parse --show-toplevel)" >expect && + echo "HEAD $(git rev-parse HEAD)" >>expect && + echo "branch $(git symbolic-ref HEAD)" >>expect && + echo >>expect && + test_when_finished "rm -rf here actual expect && git worktree prune" && + git worktree add --detach here master && + echo "worktree $(git -C here rev-parse --show-toplevel)" >>expect && + echo "HEAD $(git rev-parse HEAD)" >>expect && + echo "detached" >>expect && + echo >>expect && + git worktree list --porcelain >actual && + test_cmp expect actual +' + +test_expect_success 'bare repo setup' ' + git init --bare bare1 && + echo "data" >file1 && + git add file1 && + git commit -m"File1: add data" && + git push bare1 master && + git reset --hard HEAD^ +' + +test_expect_success '"list" all worktrees from bare main' ' + test_when_finished "rm -rf there out actual expect && git -C bare1 worktree prune" && + git -C bare1 worktree add --detach ../there master && + echo "$(pwd)/bare1 (bare)" >expect && + echo "$(git -C there rev-parse --show-toplevel) $(git -C there rev-parse --short HEAD) (detached HEAD)" >>expect && + git -C bare1 worktree list >out && + sed "s/ */ /g" actual && + test_cmp expect actual +' + +test_expect_success '"list" all worktrees --porcelain from bare main' ' + test_when_finished "rm -rf there actual expect && git -C bare1 worktree prune" && + git -C bare1 worktree add --detach ../there master && + echo "worktree $(pwd)/bare1" >expect && + echo "bare" >>expect && + echo >>expect && + echo "worktree $(git -C there rev-parse --show-toplevel)" >>expect && + echo "HEAD $(git -C there rev-parse HEAD)" >>expect && + echo "detached" >>expect && + echo >>expect && + git -C bare1 worktree list --porcelain >actual && + test_cmp expect actual +' + +test_expect_success '"list" all worktrees from linked with a bare main' ' + test_when_finished "rm -rf there out actual expect && git -C bare1 worktree prune" && + git -C bare1 worktree add --detach ../there master && + echo "$(pwd)/bare1 (bare)" >expect && + echo "$(git -C there rev-parse --show-toplevel) $(git -C there rev-parse --short HEAD) (detached HEAD)" >>expect && + git -C there worktree list >out && + sed "s/ */ /g" actual && + test_cmp expect actual +' + +test_expect_success 'bare repo cleanup' ' + rm -rf bare1 +' + +test_expect_success 'broken main worktree still at the top' ' + git init broken-main && + ( + cd broken-main && + test_commit new && + git worktree add linked && + cat >expected <<-EOF && + worktree $(pwd) + HEAD $ZERO_OID + + EOF + cd linked && + echo "worktree $(pwd)" >expected && + echo "ref: .broken" >../.git/HEAD && + git worktree list --porcelain >out && + head -n 3 out >actual && + test_cmp ../expected actual && + git worktree list >out && + head -n 1 out >actual.2 && + grep -F "(error)" actual.2 + ) +' + +test_expect_success 'linked worktrees are sorted' ' + mkdir sorted && + git init sorted/main && + ( + cd sorted/main && + test_tick && + test_commit new && + git worktree add ../first && + git worktree add ../second && + git worktree list --porcelain >out && + grep ^worktree out >actual + ) && + cat >expected <<-EOF && + worktree $(pwd)/sorted/main + worktree $(pwd)/sorted/first + worktree $(pwd)/sorted/second + EOF + test_cmp expected sorted/main/actual +' + +test_done diff --git a/t/t2403-worktree-move.sh b/t/t2403-worktree-move.sh new file mode 100755 index 0000000..33c0337 --- /dev/null +++ b/t/t2403-worktree-move.sh @@ -0,0 +1,188 @@ +#!/bin/sh + +test_description='test git worktree move, remove, lock and unlock' + +. ./test-lib.sh + +test_expect_success 'setup' ' + test_commit init && + git worktree add source && + git worktree list --porcelain >out && + grep "^worktree" out >actual && + cat <<-EOF >expected && + worktree $(pwd) + worktree $(pwd)/source + EOF + test_cmp expected actual +' + +test_expect_success 'lock main worktree' ' + test_must_fail git worktree lock . +' + +test_expect_success 'lock linked worktree' ' + git worktree lock --reason hahaha source && + echo hahaha >expected && + test_cmp expected .git/worktrees/source/locked +' + +test_expect_success 'lock linked worktree from another worktree' ' + rm .git/worktrees/source/locked && + git worktree add elsewhere && + git -C elsewhere worktree lock --reason hahaha ../source && + echo hahaha >expected && + test_cmp expected .git/worktrees/source/locked +' + +test_expect_success 'lock worktree twice' ' + test_must_fail git worktree lock source && + echo hahaha >expected && + test_cmp expected .git/worktrees/source/locked +' + +test_expect_success 'lock worktree twice (from the locked worktree)' ' + test_must_fail git -C source worktree lock . && + echo hahaha >expected && + test_cmp expected .git/worktrees/source/locked +' + +test_expect_success 'unlock main worktree' ' + test_must_fail git worktree unlock . +' + +test_expect_success 'unlock linked worktree' ' + git worktree unlock source && + test_path_is_missing .git/worktrees/source/locked +' + +test_expect_success 'unlock worktree twice' ' + test_must_fail git worktree unlock source && + test_path_is_missing .git/worktrees/source/locked +' + +test_expect_success 'move non-worktree' ' + mkdir abc && + test_must_fail git worktree move abc def +' + +test_expect_success 'move locked worktree' ' + git worktree lock source && + test_when_finished "git worktree unlock source" && + test_must_fail git worktree move source destination +' + +test_expect_success 'move worktree' ' + git worktree move source destination && + test_path_is_missing source && + git worktree list --porcelain >out && + grep "^worktree.*/destination$" out && + ! grep "^worktree.*/source$" out && + git -C destination log --format=%s >actual2 && + echo init >expected2 && + test_cmp expected2 actual2 +' + +test_expect_success 'move main worktree' ' + test_must_fail git worktree move . def +' + +test_expect_success 'move worktree to another dir' ' + mkdir some-dir && + git worktree move destination some-dir && + test_when_finished "git worktree move some-dir/destination destination" && + test_path_is_missing destination && + git worktree list --porcelain >out && + grep "^worktree.*/some-dir/destination$" out && + git -C some-dir/destination log --format=%s >actual2 && + echo init >expected2 && + test_cmp expected2 actual2 +' + +test_expect_success 'move locked worktree (force)' ' + test_when_finished " + git worktree unlock flump || : + git worktree remove flump || : + git worktree unlock ploof || : + git worktree remove ploof || : + " && + git worktree add --detach flump && + git worktree lock flump && + test_must_fail git worktree move flump ploof" && + test_must_fail git worktree move --force flump ploof" && + git worktree move --force --force flump ploof +' + +test_expect_success 'remove main worktree' ' + test_must_fail git worktree remove . +' + +test_expect_success 'remove locked worktree' ' + git worktree lock destination && + test_when_finished "git worktree unlock destination" && + test_must_fail git worktree remove destination +' + +test_expect_success 'remove worktree with dirty tracked file' ' + echo dirty >>destination/init.t && + test_when_finished "git -C destination checkout init.t" && + test_must_fail git worktree remove destination +' + +test_expect_success 'remove worktree with untracked file' ' + : >destination/untracked && + test_must_fail git worktree remove destination +' + +test_expect_success 'force remove worktree with untracked file' ' + git worktree remove --force destination && + test_path_is_missing destination +' + +test_expect_success 'remove missing worktree' ' + git worktree add to-be-gone && + test -d .git/worktrees/to-be-gone && + mv to-be-gone gone && + git worktree remove to-be-gone && + test_path_is_missing .git/worktrees/to-be-gone +' + +test_expect_success 'NOT remove missing-but-locked worktree' ' + git worktree add gone-but-locked && + git worktree lock gone-but-locked && + test -d .git/worktrees/gone-but-locked && + mv gone-but-locked really-gone-now && + test_must_fail git worktree remove gone-but-locked && + test_path_is_dir .git/worktrees/gone-but-locked +' + +test_expect_success 'proper error when worktree not found' ' + for i in noodle noodle/bork + do + test_must_fail git worktree lock $i 2>err && + test_i18ngrep "not a working tree" err || return 1 + done +' + +test_expect_success 'remove locked worktree (force)' ' + git worktree add --detach gumby && + test_when_finished "git worktree remove gumby || :" && + git worktree lock gumby && + test_when_finished "git worktree unlock gumby || :" && + test_must_fail git worktree remove gumby && + test_must_fail git worktree remove --force gumby && + git worktree remove --force --force gumby +' + +test_expect_success 'remove cleans up .git/worktrees when empty' ' + git init moog && + ( + cd moog && + test_commit bim && + git worktree add --detach goom && + test_path_exists .git/worktrees && + git worktree remove goom && + test_path_is_missing .git/worktrees + ) +' + +test_done diff --git a/t/t2404-worktree-config.sh b/t/t2404-worktree-config.sh new file mode 100755 index 0000000..286121d --- /dev/null +++ b/t/t2404-worktree-config.sh @@ -0,0 +1,79 @@ +#!/bin/sh + +test_description="config file in multi worktree" + +. ./test-lib.sh + +test_expect_success 'setup' ' + test_commit start +' + +test_expect_success 'config --worktree in single worktree' ' + git config --worktree foo.bar true && + test_cmp_config true foo.bar +' + +test_expect_success 'add worktrees' ' + git worktree add wt1 && + git worktree add wt2 +' + +test_expect_success 'config --worktree without extension' ' + test_must_fail git config --worktree foo.bar false +' + +test_expect_success 'enable worktreeConfig extension' ' + git config extensions.worktreeConfig true && + test_cmp_config true extensions.worktreeConfig +' + +test_expect_success 'config is shared as before' ' + git config this.is shared && + test_cmp_config shared this.is && + test_cmp_config -C wt1 shared this.is && + test_cmp_config -C wt2 shared this.is +' + +test_expect_success 'config is shared (set from another worktree)' ' + git -C wt1 config that.is also-shared && + test_cmp_config also-shared that.is && + test_cmp_config -C wt1 also-shared that.is && + test_cmp_config -C wt2 also-shared that.is +' + +test_expect_success 'config private to main worktree' ' + git config --worktree this.is for-main && + test_cmp_config for-main this.is && + test_cmp_config -C wt1 shared this.is && + test_cmp_config -C wt2 shared this.is +' + +test_expect_success 'config private to linked worktree' ' + git -C wt1 config --worktree this.is for-wt1 && + test_cmp_config for-main this.is && + test_cmp_config -C wt1 for-wt1 this.is && + test_cmp_config -C wt2 shared this.is +' + +test_expect_success 'core.bare no longer for main only' ' + test_config core.bare true && + test "$(git rev-parse --is-bare-repository)" = true && + test "$(git -C wt1 rev-parse --is-bare-repository)" = true && + test "$(git -C wt2 rev-parse --is-bare-repository)" = true +' + +test_expect_success 'per-worktree core.bare is picked up' ' + git -C wt1 config --worktree core.bare true && + test "$(git rev-parse --is-bare-repository)" = false && + test "$(git -C wt1 rev-parse --is-bare-repository)" = true && + test "$(git -C wt2 rev-parse --is-bare-repository)" = false +' + +test_expect_success 'config.worktree no longer read without extension' ' + git config --unset extensions.worktreeConfig && + test_cmp_config shared this.is && + test_cmp_config -C wt1 shared this.is && + test_cmp_config -C wt2 shared this.is +' + +test_done -- cgit v0.10.2-6-g49f6 From b702dd12d52816e192578c6206db5e6c332ba49b Mon Sep 17 00:00:00 2001 From: Thomas Gummerer Date: Thu, 20 Dec 2018 13:48:14 +0000 Subject: entry: factor out unlink_entry function Factor out the 'unlink_entry()' function from unpack-trees.c to entry.c. It will be used in other places as well in subsequent steps. As it's no longer a static function, also move the documentation to the header file to make it more discoverable. Signed-off-by: Thomas Gummerer Signed-off-by: Junio C Hamano diff --git a/cache.h b/cache.h index ca36b44..c1c953e 100644 --- a/cache.h +++ b/cache.h @@ -1542,6 +1542,11 @@ struct checkout { extern int checkout_entry(struct cache_entry *ce, const struct checkout *state, char *topath); extern void enable_delayed_checkout(struct checkout *state); extern int finish_delayed_checkout(struct checkout *state); +/* + * Unlink the last component and schedule the leading directories for + * removal, such that empty directories get removed. + */ +extern void unlink_entry(const struct cache_entry *ce); struct cache_def { struct strbuf path; diff --git a/entry.c b/entry.c index 0a3c451..b9eef57 100644 --- a/entry.c +++ b/entry.c @@ -508,3 +508,18 @@ int checkout_entry(struct cache_entry *ce, create_directories(path.buf, path.len, state); return write_entry(ce, path.buf, state, 0); } + +void unlink_entry(const struct cache_entry *ce) +{ + const struct submodule *sub = submodule_from_ce(ce); + if (sub) { + /* state.force is set at the caller. */ + submodule_move_head(ce->name, "HEAD", NULL, + SUBMODULE_MOVE_HEAD_FORCE); + } + if (!check_leading_path(ce->name, ce_namelen(ce))) + return; + if (remove_or_warn(ce->ce_mode, ce->name)) + return; + schedule_dir_for_removal(ce->name, ce_namelen(ce)); +} diff --git a/unpack-trees.c b/unpack-trees.c index 7570df4..e8d1a6a 100644 --- a/unpack-trees.c +++ b/unpack-trees.c @@ -300,25 +300,6 @@ static void load_gitmodules_file(struct index_state *index, } } -/* - * Unlink the last component and schedule the leading directories for - * removal, such that empty directories get removed. - */ -static void unlink_entry(const struct cache_entry *ce) -{ - const struct submodule *sub = submodule_from_ce(ce); - if (sub) { - /* state.force is set at the caller. */ - submodule_move_head(ce->name, "HEAD", NULL, - SUBMODULE_MOVE_HEAD_FORCE); - } - if (!check_leading_path(ce->name, ce_namelen(ce))) - return; - if (remove_or_warn(ce->ce_mode, ce->name)) - return; - schedule_dir_for_removal(ce->name, ce_namelen(ce)); -} - static struct progress *get_progress(struct unpack_trees_options *o) { unsigned cnt = 0, total = 0; -- cgit v0.10.2-6-g49f6 From 536ec1839dbde8b9a6b38e6ccb5ab01b2b6311f9 Mon Sep 17 00:00:00 2001 From: Thomas Gummerer Date: Thu, 20 Dec 2018 13:48:15 +0000 Subject: entry: support CE_WT_REMOVE flag in checkout_entry 'checkout_entry()' currently only supports creating new entries in the working tree, but not deleting them. Add the ability to remove entries at the same time if the entry is marked with the CE_WT_REMOVE flag. Currently this doesn't have any effect, as the CE_WT_REMOVE flag is only used in unpack-tree, however we will make use of this in a subsequent step in the series. Signed-off-by: Thomas Gummerer Signed-off-by: Junio C Hamano diff --git a/entry.c b/entry.c index b9eef57..3d3701e 100644 --- a/entry.c +++ b/entry.c @@ -441,6 +441,17 @@ int checkout_entry(struct cache_entry *ce, static struct strbuf path = STRBUF_INIT; struct stat st; + if (ce->ce_flags & CE_WT_REMOVE) { + if (topath) + /* + * No content and thus no path to create, so we have + * no pathname to return. + */ + BUG("Can't remove entry to a path"); + unlink_entry(ce); + return 0; + } + if (topath) return write_entry(ce, topath, state, 1); -- cgit v0.10.2-6-g49f6 From 6fdc2057225ad1ae735ecaacdcace77c8b0b6b76 Mon Sep 17 00:00:00 2001 From: Thomas Gummerer Date: Thu, 20 Dec 2018 13:48:16 +0000 Subject: read-cache: add invalidate parameter to remove_marked_cache_entries When marking cache entries for removal, and later removing them all at once using 'remove_marked_cache_entries()', cache entries currently have to be invalidated manually in the cache tree and in the untracked cache. Add an invalidate flag to the function. With the flag set, the function will take care of invalidating the path in the cache tree and in the untracked cache. Note that the current callsites already do the invalidation properly in other places, so we're just passing 0 from there to keep the status quo. This will be useful in a subsequent commit. Signed-off-by: Thomas Gummerer Signed-off-by: Junio C Hamano diff --git a/cache.h b/cache.h index c1c953e..1deee48 100644 --- a/cache.h +++ b/cache.h @@ -751,7 +751,7 @@ extern void rename_index_entry_at(struct index_state *, int pos, const char *new /* Remove entry, return true if there are more entries to go. */ extern int remove_index_entry_at(struct index_state *, int pos); -extern void remove_marked_cache_entries(struct index_state *istate); +extern void remove_marked_cache_entries(struct index_state *istate, int invalidate); extern int remove_file_from_index(struct index_state *, const char *path); #define ADD_CACHE_VERBOSE 1 #define ADD_CACHE_PRETEND 2 diff --git a/read-cache.c b/read-cache.c index bd45dc3..978d43f 100644 --- a/read-cache.c +++ b/read-cache.c @@ -590,13 +590,19 @@ int remove_index_entry_at(struct index_state *istate, int pos) * CE_REMOVE is set in ce_flags. This is much more effective than * calling remove_index_entry_at() for each entry to be removed. */ -void remove_marked_cache_entries(struct index_state *istate) +void remove_marked_cache_entries(struct index_state *istate, int invalidate) { struct cache_entry **ce_array = istate->cache; unsigned int i, j; for (i = j = 0; i < istate->cache_nr; i++) { if (ce_array[i]->ce_flags & CE_REMOVE) { + if (invalidate) { + cache_tree_invalidate_path(istate, + ce_array[i]->name); + untracked_cache_remove_from_index(istate, + ce_array[i]->name); + } remove_name_hash(istate, ce_array[i]); save_or_free_index_entry(istate, ce_array[i]); } diff --git a/split-index.c b/split-index.c index 5820412..8aebc36 100644 --- a/split-index.c +++ b/split-index.c @@ -162,7 +162,7 @@ void merge_base_index(struct index_state *istate) ewah_each_bit(si->replace_bitmap, replace_entry, istate); ewah_each_bit(si->delete_bitmap, mark_entry_for_delete, istate); if (si->nr_deletions) - remove_marked_cache_entries(istate); + remove_marked_cache_entries(istate, 0); for (i = si->nr_replacements; i < si->saved_cache_nr; i++) { if (!ce_namelen(si->saved_cache[i])) diff --git a/unpack-trees.c b/unpack-trees.c index e8d1a6a..8e6afa9 100644 --- a/unpack-trees.c +++ b/unpack-trees.c @@ -392,7 +392,7 @@ static int check_updates(struct unpack_trees_options *o) unlink_entry(ce); } } - remove_marked_cache_entries(index); + remove_marked_cache_entries(index, 0); remove_scheduled_dirs(); if (should_update_submodules() && o->update && !o->dry_run) -- cgit v0.10.2-6-g49f6 From 5160fa05623dc2e9e8348d48b91f1f0bf44d369e Mon Sep 17 00:00:00 2001 From: Thomas Gummerer Date: Thu, 20 Dec 2018 13:48:17 +0000 Subject: checkout: clarify comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The key point for the if statement is that read_tree_some did not update the entry, because either it doesn't exist in tree-ish or doesn't match the pathspec. Clarify that. Suggested-by: Nguyễn Thái Ngọc Duy Signed-off-by: Thomas Gummerer Signed-off-by: Junio C Hamano diff --git a/builtin/checkout.c b/builtin/checkout.c index acdafc6..cb166b2 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -304,10 +304,10 @@ static int checkout_paths(const struct checkout_opts *opts, continue; if (opts->source_tree && !(ce->ce_flags & CE_UPDATE)) /* - * "git checkout tree-ish -- path", but this entry - * is in the original index; it will not be checked - * out to the working tree and it does not matter - * if pathspec matched this entry. We will not do + * "git checkout tree-ish -- path" and this entry + * is in the original index, but is not in tree-ish + * or does not match the pathspec; it will not be + * checked out to the working tree. We will not do * anything to this entry at all. */ continue; -- cgit v0.10.2-6-g49f6 From b7033e73b7b671670160726f62ac16d88161e3d0 Mon Sep 17 00:00:00 2001 From: Thomas Gummerer Date: Thu, 20 Dec 2018 13:48:18 +0000 Subject: checkout: factor out mark_cache_entry_for_checkout function Factor out the code that marks a cache entry as matched for checkout into a separate function. We are going to introduce a new mode in 'git checkout' in a subsequent commit, that is going to have a slightly different logic. This would make this code unnecessarily complex. Moving that complexity into separate functions will make the code in the subsequent step easier to follow. Signed-off-by: Thomas Gummerer Signed-off-by: Junio C Hamano diff --git a/builtin/checkout.c b/builtin/checkout.c index cb166b2..32c4b7f 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -247,6 +247,40 @@ static int checkout_merged(int pos, const struct checkout *state) return status; } +static void mark_ce_for_checkout(struct cache_entry *ce, + char *ps_matched, + const struct checkout_opts *opts) +{ + ce->ce_flags &= ~CE_MATCHED; + if (!opts->ignore_skipworktree && ce_skip_worktree(ce)) + return; + if (opts->source_tree && !(ce->ce_flags & CE_UPDATE)) + /* + * "git checkout tree-ish -- path", but this entry + * is in the original index but is not in tree-ish + * or does not match the pathspec; it will not be + * checked out to the working tree. We will not do + * anything to this entry at all. + */ + return; + /* + * Either this entry came from the tree-ish we are + * checking the paths out of, or we are checking out + * of the index. + * + * If it comes from the tree-ish, we already know it + * matches the pathspec and could just stamp + * CE_MATCHED to it from update_some(). But we still + * need ps_matched and read_tree_recursive (and + * eventually tree_entry_interesting) cannot fill + * ps_matched yet. Once it can, we can avoid calling + * match_pathspec() for _all_ entries when + * opts->source_tree != NULL. + */ + if (ce_path_match(&the_index, ce, &opts->pathspec, ps_matched)) + ce->ce_flags |= CE_MATCHED; +} + static int checkout_paths(const struct checkout_opts *opts, const char *revision) { @@ -297,37 +331,8 @@ static int checkout_paths(const struct checkout_opts *opts, * Make sure all pathspecs participated in locating the paths * to be checked out. */ - for (pos = 0; pos < active_nr; pos++) { - struct cache_entry *ce = active_cache[pos]; - ce->ce_flags &= ~CE_MATCHED; - if (!opts->ignore_skipworktree && ce_skip_worktree(ce)) - continue; - if (opts->source_tree && !(ce->ce_flags & CE_UPDATE)) - /* - * "git checkout tree-ish -- path" and this entry - * is in the original index, but is not in tree-ish - * or does not match the pathspec; it will not be - * checked out to the working tree. We will not do - * anything to this entry at all. - */ - continue; - /* - * Either this entry came from the tree-ish we are - * checking the paths out of, or we are checking out - * of the index. - * - * If it comes from the tree-ish, we already know it - * matches the pathspec and could just stamp - * CE_MATCHED to it from update_some(). But we still - * need ps_matched and read_tree_recursive (and - * eventually tree_entry_interesting) cannot fill - * ps_matched yet. Once it can, we can avoid calling - * match_pathspec() for _all_ entries when - * opts->source_tree != NULL. - */ - if (ce_path_match(&the_index, ce, &opts->pathspec, ps_matched)) - ce->ce_flags |= CE_MATCHED; - } + for (pos = 0; pos < active_nr; pos++) + mark_ce_for_checkout(active_cache[pos], ps_matched, opts); if (report_path_error(ps_matched, &opts->pathspec, opts->prefix)) { free(ps_matched); -- cgit v0.10.2-6-g49f6 From 091e04bc8cbb0c89c8112c4784f02a44decc257e Mon Sep 17 00:00:00 2001 From: Thomas Gummerer Date: Tue, 8 Jan 2019 21:52:24 +0000 Subject: checkout: introduce --{,no-}overlay option Currently 'git checkout' is defined as an overlay operation, which means that if in 'git checkout -- []' we have an entry in the index that matches , but that doesn't exist in , that entry will not be removed from the index or the working tree. Introduce a new --{,no-}overlay option, which allows using 'git checkout' in non-overlay mode, thus removing files from the working tree if they do not exist in but match . Note that 'git checkout -p -- []' already works this way, so no changes are needed for the patch mode. We disallow 'git checkout --overlay -p' to avoid confusing users who would expect to be able to force overlay mode in 'git checkout -p' this way. Untracked files are not affected by this change, so 'git checkout --no-overlay HEAD -- untracked' will not remove untracked from the working tree. This is so e.g. 'git checkout --no-overlay HEAD -- dir/' doesn't delete all untracked files in dir/, but rather just resets the state of files that are known to git. Suggested-by: Junio C Hamano Signed-off-by: Thomas Gummerer Signed-off-by: Junio C Hamano diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt index 801de2f..24e52b0 100644 --- a/Documentation/git-checkout.txt +++ b/Documentation/git-checkout.txt @@ -260,6 +260,9 @@ the conflicted merge in the specified paths. This means that you can use `git checkout -p` to selectively discard edits from your current working tree. See the ``Interactive Mode'' section of linkgit:git-add[1] to learn how to operate the `--patch` mode. ++ +Note that this option uses the no overlay mode by default (see also +`--[no-]overlay`), and currently doesn't support overlay mode. --ignore-other-worktrees:: `git checkout` refuses when the wanted ref is already checked @@ -276,6 +279,13 @@ section of linkgit:git-add[1] to learn how to operate the `--patch` mode. Just like linkgit:git-submodule[1], this will detach the submodules HEAD. +--[no-]overlay:: + In the default overlay mode, `git checkout` never + removes files from the index or the working tree. When + specifying `--no-overlay`, files that appear in the index and + working tree, but not in are removed, to make them + match exactly. + :: Branch to checkout; if it refers to a branch (i.e., a name that, when prepended with "refs/heads/", is a valid ref), then that diff --git a/builtin/checkout.c b/builtin/checkout.c index 32c4b7f..0c5fe94 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -44,6 +44,7 @@ struct checkout_opts { int ignore_skipworktree; int ignore_other_worktrees; int show_progress; + int overlay_mode; /* * If new checkout options are added, skip_merge_working_tree * should be updated accordingly. @@ -132,7 +133,8 @@ static int skip_same_name(const struct cache_entry *ce, int pos) return pos; } -static int check_stage(int stage, const struct cache_entry *ce, int pos) +static int check_stage(int stage, const struct cache_entry *ce, int pos, + int overlay_mode) { while (pos < active_nr && !strcmp(active_cache[pos]->name, ce->name)) { @@ -140,6 +142,8 @@ static int check_stage(int stage, const struct cache_entry *ce, int pos) return 0; pos++; } + if (!overlay_mode) + return 0; if (stage == 2) return error(_("path '%s' does not have our version"), ce->name); else @@ -165,7 +169,7 @@ static int check_stages(unsigned stages, const struct cache_entry *ce, int pos) } static int checkout_stage(int stage, const struct cache_entry *ce, int pos, - const struct checkout *state) + const struct checkout *state, int overlay_mode) { while (pos < active_nr && !strcmp(active_cache[pos]->name, ce->name)) { @@ -173,6 +177,10 @@ static int checkout_stage(int stage, const struct cache_entry *ce, int pos, return checkout_entry(active_cache[pos], state, NULL); pos++; } + if (!overlay_mode) { + unlink_entry(ce); + return 0; + } if (stage == 2) return error(_("path '%s' does not have our version"), ce->name); else @@ -247,9 +255,9 @@ static int checkout_merged(int pos, const struct checkout *state) return status; } -static void mark_ce_for_checkout(struct cache_entry *ce, - char *ps_matched, - const struct checkout_opts *opts) +static void mark_ce_for_checkout_overlay(struct cache_entry *ce, + char *ps_matched, + const struct checkout_opts *opts) { ce->ce_flags &= ~CE_MATCHED; if (!opts->ignore_skipworktree && ce_skip_worktree(ce)) @@ -281,6 +289,25 @@ static void mark_ce_for_checkout(struct cache_entry *ce, ce->ce_flags |= CE_MATCHED; } +static void mark_ce_for_checkout_no_overlay(struct cache_entry *ce, + char *ps_matched, + const struct checkout_opts *opts) +{ + ce->ce_flags &= ~CE_MATCHED; + if (!opts->ignore_skipworktree && ce_skip_worktree(ce)) + return; + if (ce_path_match(&the_index, ce, &opts->pathspec, ps_matched)) { + ce->ce_flags |= CE_MATCHED; + if (opts->source_tree && !(ce->ce_flags & CE_UPDATE)) + /* + * In overlay mode, but the path is not in + * tree-ish, which means we should remove it + * from the index and the working tree. + */ + ce->ce_flags |= CE_REMOVE | CE_WT_REMOVE; + } +} + static int checkout_paths(const struct checkout_opts *opts, const char *revision) { @@ -332,7 +359,14 @@ static int checkout_paths(const struct checkout_opts *opts, * to be checked out. */ for (pos = 0; pos < active_nr; pos++) - mark_ce_for_checkout(active_cache[pos], ps_matched, opts); + if (opts->overlay_mode) + mark_ce_for_checkout_overlay(active_cache[pos], + ps_matched, + opts); + else + mark_ce_for_checkout_no_overlay(active_cache[pos], + ps_matched, + opts); if (report_path_error(ps_matched, &opts->pathspec, opts->prefix)) { free(ps_matched); @@ -353,7 +387,7 @@ static int checkout_paths(const struct checkout_opts *opts, if (opts->force) { warning(_("path '%s' is unmerged"), ce->name); } else if (opts->writeout_stage) { - errs |= check_stage(opts->writeout_stage, ce, pos); + errs |= check_stage(opts->writeout_stage, ce, pos, opts->overlay_mode); } else if (opts->merge) { errs |= check_stages((1<<2) | (1<<3), ce, pos); } else { @@ -380,12 +414,14 @@ static int checkout_paths(const struct checkout_opts *opts, continue; } if (opts->writeout_stage) - errs |= checkout_stage(opts->writeout_stage, ce, pos, &state); + errs |= checkout_stage(opts->writeout_stage, ce, pos, &state, opts->overlay_mode); else if (opts->merge) errs |= checkout_merged(pos, &state); pos = skip_same_name(ce, pos) - 1; } } + remove_marked_cache_entries(&the_index, 1); + remove_scheduled_dirs(); errs |= finish_delayed_checkout(&state); if (write_locked_index(&the_index, &lock_file, COMMIT_LOCK)) @@ -548,6 +584,11 @@ static int skip_merge_working_tree(const struct checkout_opts *opts, */ /* + * opts->overlay_mode cannot be used with switching branches so is + * not tested here + */ + + /* * If we aren't creating a new branch any changes or updates will * happen in the existing branch. Since that could only be updating * the index and working directory, we don't want to skip those steps @@ -1183,6 +1224,10 @@ static int checkout_branch(struct checkout_opts *opts, die(_("'%s' cannot be used with switching branches"), "--patch"); + if (!opts->overlay_mode) + die(_("'%s' cannot be used with switching branches"), + "--no-overlay"); + if (opts->writeout_stage) die(_("'%s' cannot be used with switching branches"), "--ours/--theirs"); @@ -1271,6 +1316,7 @@ int cmd_checkout(int argc, const char **argv, const char *prefix) "checkout", "control recursive updating of submodules", PARSE_OPT_OPTARG, option_parse_recurse_submodules_worktree_updater }, OPT_BOOL(0, "progress", &opts.show_progress, N_("force progress reporting")), + OPT_BOOL(0, "overlay", &opts.overlay_mode, N_("use overlay mode (default)")), OPT_END(), }; @@ -1279,6 +1325,7 @@ int cmd_checkout(int argc, const char **argv, const char *prefix) opts.overwrite_ignore = 1; opts.prefix = prefix; opts.show_progress = -1; + opts.overlay_mode = -1; git_config(git_checkout_config, &opts); @@ -1302,6 +1349,9 @@ int cmd_checkout(int argc, const char **argv, const char *prefix) if ((!!opts.new_branch + !!opts.new_branch_force + !!opts.new_orphan_branch) > 1) die(_("-b, -B and --orphan are mutually exclusive")); + if (opts.overlay_mode == 1 && opts.patch_mode) + die(_("-p and --overlay are mutually exclusive")); + /* * From here on, new_branch will contain the branch to be checked out, * and new_branch_force and new_orphan_branch will tell us which one of diff --git a/t/t2025-checkout-no-overlay.sh b/t/t2025-checkout-no-overlay.sh new file mode 100755 index 0000000..76330cb --- /dev/null +++ b/t/t2025-checkout-no-overlay.sh @@ -0,0 +1,47 @@ +#!/bin/sh + +test_description='checkout --no-overlay -- ' + +. ./test-lib.sh + +test_expect_success 'setup' ' + git commit --allow-empty -m "initial" +' + +test_expect_success 'checkout --no-overlay deletes files not in ' ' + >file && + mkdir dir && + >dir/file1 && + git add file dir/file1 && + git checkout --no-overlay HEAD -- file && + test_path_is_missing file && + test_path_is_file dir/file1 +' + +test_expect_success 'checkout --no-overlay removing last file from directory' ' + git checkout --no-overlay HEAD -- dir/file1 && + test_path_is_missing dir +' + +test_expect_success 'checkout -p --overlay is disallowed' ' + test_must_fail git checkout -p --overlay HEAD 2>actual && + test_i18ngrep "fatal: -p and --overlay are mutually exclusive" actual +' + +test_expect_success '--no-overlay --theirs with D/F conflict deletes file' ' + test_commit file1 file1 && + test_commit file2 file2 && + git rm --cached file1 && + echo 1234 >file1 && + F1=$(git rev-parse HEAD:file1) && + F2=$(git rev-parse HEAD:file2) && + { + echo "100644 $F1 1 file1" && + echo "100644 $F2 2 file1" + } | git update-index --index-info && + test_path_is_file file1 && + git checkout --theirs --no-overlay -- file1 && + test_path_is_missing file1 +' + +test_done diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh index d01ad8e..5758fff 100755 --- a/t/t9902-completion.sh +++ b/t/t9902-completion.sh @@ -1436,6 +1436,7 @@ test_expect_success 'double dash "git checkout"' ' --progress Z --no-quiet Z --no-... Z + --overlay Z EOF ' -- cgit v0.10.2-6-g49f6 From 1495ff7da526c61bff88e31fcdf419fb023a42c5 Mon Sep 17 00:00:00 2001 From: Thomas Gummerer Date: Tue, 8 Jan 2019 21:52:25 +0000 Subject: checkout: introduce checkout.overlayMode config In the previous patch we introduced a new no-overlay mode for git checkout. Some users (such as the author of this commit) may want to have this mode turned on by default as it matches their mental model more closely. Make that possible by introducing a new config option to that extend. Signed-off-by: Thomas Gummerer Signed-off-by: Junio C Hamano diff --git a/Documentation/config/checkout.txt b/Documentation/config/checkout.txt index c4118fa..73380a8 100644 --- a/Documentation/config/checkout.txt +++ b/Documentation/config/checkout.txt @@ -21,3 +21,10 @@ checkout.optimizeNewBranch:: will not update the skip-worktree bit in the index nor add/remove files in the working directory to reflect the current sparse checkout settings nor will it show the local changes. + +checkout.overlayMode:: + In the default overlay mode, `git checkout` never + removes files from the index or the working tree. When + setting `checkout.overlayMode` to false, files that appear in + the index and working tree, but not in are removed, + to make them match exactly. diff --git a/builtin/checkout.c b/builtin/checkout.c index 0c5fe94..b5dfc45 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -1019,13 +1019,19 @@ static int switch_branches(const struct checkout_opts *opts, static int git_checkout_config(const char *var, const char *value, void *cb) { + struct checkout_opts *opts = cb; + if (!strcmp(var, "checkout.optimizenewbranch")) { checkout_optimize_new_branch = git_config_bool(var, value); return 0; } + if (!strcmp(var, "checkout.overlaymode")) { + opts->overlay_mode = git_config_bool(var, value); + return 0; + } + if (!strcmp(var, "diff.ignoresubmodules")) { - struct checkout_opts *opts = cb; handle_ignore_submodules_arg(&opts->diff_options, value); return 0; } diff --git a/t/t2025-checkout-no-overlay.sh b/t/t2025-checkout-no-overlay.sh index 76330cb..a4912e3 100755 --- a/t/t2025-checkout-no-overlay.sh +++ b/t/t2025-checkout-no-overlay.sh @@ -44,4 +44,14 @@ test_expect_success '--no-overlay --theirs with D/F conflict deletes file' ' test_path_is_missing file1 ' +test_expect_success 'checkout with checkout.overlayMode=false deletes files not in ' ' + >file && + mkdir dir && + >dir/file1 && + git add file dir/file1 && + git -c checkout.overlayMode=false checkout HEAD -- file && + test_path_is_missing file && + test_path_is_file dir/file1 +' + test_done -- cgit v0.10.2-6-g49f6 From e92aa0e4ef5a91781530449f9466a45c16c91f7f Mon Sep 17 00:00:00 2001 From: Thomas Gummerer Date: Mon, 4 Feb 2019 21:13:16 +0000 Subject: revert "checkout: introduce checkout.overlayMode config" This reverts 1495ff7da5 ("checkout: introduce checkout.overlayMode config", 2019-01-08) and thus removes the checkout.overlayMode config option. The option was originally introduced to give users the option to make the new no-overlay behaviour the default. However users may be using 'git checkout' in scripts, even though it is porcelain. Users setting the option to false may actually end up accidentally breaking scripts. With the introduction of a new subcommand that will make the behaviour the default, the config option will not be needed anymore anyway. Revert the commit and remove the config option, so we don't risk breaking scripts. Suggested-by: Jonathan Nieder Signed-off-by: Thomas Gummerer Signed-off-by: Junio C Hamano diff --git a/Documentation/config/checkout.txt b/Documentation/config/checkout.txt index 73380a8..c4118fa 100644 --- a/Documentation/config/checkout.txt +++ b/Documentation/config/checkout.txt @@ -21,10 +21,3 @@ checkout.optimizeNewBranch:: will not update the skip-worktree bit in the index nor add/remove files in the working directory to reflect the current sparse checkout settings nor will it show the local changes. - -checkout.overlayMode:: - In the default overlay mode, `git checkout` never - removes files from the index or the working tree. When - setting `checkout.overlayMode` to false, files that appear in - the index and working tree, but not in are removed, - to make them match exactly. diff --git a/builtin/checkout.c b/builtin/checkout.c index b5dfc45..0c5fe94 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -1019,19 +1019,13 @@ static int switch_branches(const struct checkout_opts *opts, static int git_checkout_config(const char *var, const char *value, void *cb) { - struct checkout_opts *opts = cb; - if (!strcmp(var, "checkout.optimizenewbranch")) { checkout_optimize_new_branch = git_config_bool(var, value); return 0; } - if (!strcmp(var, "checkout.overlaymode")) { - opts->overlay_mode = git_config_bool(var, value); - return 0; - } - if (!strcmp(var, "diff.ignoresubmodules")) { + struct checkout_opts *opts = cb; handle_ignore_submodules_arg(&opts->diff_options, value); return 0; } diff --git a/t/t2025-checkout-no-overlay.sh b/t/t2025-checkout-no-overlay.sh index a4912e3..76330cb 100755 --- a/t/t2025-checkout-no-overlay.sh +++ b/t/t2025-checkout-no-overlay.sh @@ -44,14 +44,4 @@ test_expect_success '--no-overlay --theirs with D/F conflict deletes file' ' test_path_is_missing file1 ' -test_expect_success 'checkout with checkout.overlayMode=false deletes files not in ' ' - >file && - mkdir dir && - >dir/file1 && - git add file dir/file1 && - git -c checkout.overlayMode=false checkout HEAD -- file && - test_path_is_missing file && - test_path_is_file dir/file1 -' - test_done -- cgit v0.10.2-6-g49f6