diff options
-rw-r--r-- | Documentation/config/extensions.txt | 6 | ||||
-rw-r--r-- | Documentation/config/worktree.txt | 10 | ||||
-rw-r--r-- | Documentation/git-worktree.txt | 8 | ||||
-rw-r--r-- | builtin/worktree.c | 29 | ||||
-rw-r--r-- | repository.c | 1 | ||||
-rw-r--r-- | repository.h | 1 | ||||
-rw-r--r-- | setup.c | 39 | ||||
-rw-r--r-- | setup.h | 1 | ||||
-rwxr-xr-x | t/t0001-init.sh | 22 | ||||
-rwxr-xr-x | t/t2400-worktree-add.sh | 46 | ||||
-rwxr-xr-x | t/t2401-worktree-prune.sh | 3 | ||||
-rwxr-xr-x | t/t2402-worktree-list.sh | 22 | ||||
-rwxr-xr-x | t/t2403-worktree-move.sh | 25 | ||||
-rwxr-xr-x | t/t2406-worktree-repair.sh | 39 | ||||
-rwxr-xr-x | t/t2408-worktree-relative.sh | 38 | ||||
-rwxr-xr-x | t/t5504-fetch-receive-strict.sh | 6 | ||||
-rw-r--r-- | worktree.c | 157 | ||||
-rw-r--r-- | worktree.h | 22 |
18 files changed, 334 insertions, 141 deletions
diff --git a/Documentation/config/extensions.txt b/Documentation/config/extensions.txt index 5dc569d1c9..5cb4721a0e 100644 --- a/Documentation/config/extensions.txt +++ b/Documentation/config/extensions.txt @@ -63,6 +63,12 @@ Note that this setting should only be set by linkgit:git-init[1] or linkgit:git-clone[1]. Trying to change it after initialization will not work and will produce hard-to-diagnose issues. +relativeWorktrees:: + If enabled, indicates at least one worktree has been linked with + relative paths. Automatically set if a worktree has been created or + repaired with either the `--relative-paths` option or with the + `worktree.useRelativePaths` config set to `true`. + worktreeConfig:: If enabled, then worktrees will load config settings from the `$GIT_DIR/config.worktree` file in addition to the diff --git a/Documentation/config/worktree.txt b/Documentation/config/worktree.txt index 048e349482..5e35c7d018 100644 --- a/Documentation/config/worktree.txt +++ b/Documentation/config/worktree.txt @@ -7,3 +7,13 @@ worktree.guessRemote:: such a branch exists, it is checked out and set as "upstream" for the new branch. If no such match can be found, it falls back to creating a new branch from the current HEAD. + +worktree.useRelativePaths:: + Link worktrees using relative paths (when "true") or absolute + paths (when "false"). This is particularly useful for setups + where the repository and worktrees may be moved between + different locations or environments. Defaults to "false". ++ +Note that setting `worktree.useRelativePaths` to "true" implies enabling the +`extension.relativeWorktrees` config (see linkgit:git-config[1]), +thus making it incompatible with older versions of Git. diff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt index 70437c815f..8340b7f028 100644 --- a/Documentation/git-worktree.txt +++ b/Documentation/git-worktree.txt @@ -216,6 +216,14 @@ To remove a locked worktree, specify `--force` twice. This can also be set up as the default behaviour by using the `worktree.guessRemote` config option. +--[no-]relative-paths:: + Link worktrees using relative paths or absolute paths (default). + Overrides the `worktree.useRelativePaths` config option, see + linkgit:git-config[1]. ++ +With `repair`, the linking files will be updated if there's an absolute/relative +mismatch, even if the links are correct. + --[no-]track:: When creating a new branch, if `<commit-ish>` is a branch, mark it as "upstream" from the new branch. This is the diff --git a/builtin/worktree.c b/builtin/worktree.c index 824dd71d64..5dfc446242 100644 --- a/builtin/worktree.c +++ b/builtin/worktree.c @@ -120,12 +120,14 @@ struct add_opts { int quiet; int checkout; int orphan; + int relative_paths; const char *keep_locked; }; static int show_only; static int verbose; static int guess_remote; +static int use_relative_paths; static timestamp_t expire; static int git_worktree_config(const char *var, const char *value, @@ -134,6 +136,9 @@ static int git_worktree_config(const char *var, const char *value, if (!strcmp(var, "worktree.guessremote")) { guess_remote = git_config_bool(var, value); return 0; + } else if (!strcmp(var, "worktree.userelativepaths")) { + use_relative_paths = git_config_bool(var, value); + return 0; } return git_default_config(var, value, ctx, cb); @@ -415,8 +420,7 @@ static int add_worktree(const char *path, const char *refname, const struct add_opts *opts) { struct strbuf sb_git = STRBUF_INIT, sb_repo = STRBUF_INIT; - struct strbuf sb = STRBUF_INIT, sb_tmp = STRBUF_INIT; - struct strbuf sb_path_realpath = STRBUF_INIT, sb_repo_realpath = STRBUF_INIT; + struct strbuf sb = STRBUF_INIT; const char *name; struct strvec child_env = STRVEC_INIT; unsigned int counter = 0; @@ -492,10 +496,7 @@ static int add_worktree(const char *path, const char *refname, strbuf_reset(&sb); strbuf_addf(&sb, "%s/gitdir", sb_repo.buf); - strbuf_realpath(&sb_path_realpath, path, 1); - strbuf_realpath(&sb_repo_realpath, sb_repo.buf, 1); - write_file(sb.buf, "%s/.git", relative_path(sb_path_realpath.buf, sb_repo_realpath.buf, &sb_tmp)); - write_file(sb_git.buf, "gitdir: %s", relative_path(sb_repo_realpath.buf, sb_path_realpath.buf, &sb_tmp)); + write_worktree_linking_files(sb_git, sb, opts->relative_paths); strbuf_reset(&sb); strbuf_addf(&sb, "%s/commondir", sb_repo.buf); write_file(sb.buf, "../.."); @@ -579,12 +580,9 @@ done: strvec_clear(&child_env); strbuf_release(&sb); - strbuf_release(&sb_tmp); strbuf_release(&symref); strbuf_release(&sb_repo); - strbuf_release(&sb_repo_realpath); strbuf_release(&sb_git); - strbuf_release(&sb_path_realpath); strbuf_release(&sb_name); free_worktree(wt); return ret; @@ -798,12 +796,15 @@ static int add(int ac, const char **av, const char *prefix, PARSE_OPT_NOARG | PARSE_OPT_OPTARG), OPT_BOOL(0, "guess-remote", &guess_remote, N_("try to match the new branch name with a remote-tracking branch")), + OPT_BOOL(0, "relative-paths", &opts.relative_paths, + N_("use relative paths for worktrees")), OPT_END() }; int ret; memset(&opts, 0, sizeof(opts)); opts.checkout = 1; + opts.relative_paths = use_relative_paths; ac = parse_options(ac, av, prefix, options, git_worktree_add_usage, 0); if (!!opts.detach + !!new_branch + !!new_branch_force > 1) die(_("options '%s', '%s', and '%s' cannot be used together"), "-b", "-B", "--detach"); @@ -1195,6 +1196,8 @@ static int move_worktree(int ac, const char **av, const char *prefix, OPT__FORCE(&force, N_("force move even if worktree is dirty or locked"), PARSE_OPT_NOCOMPLETE), + OPT_BOOL(0, "relative-paths", &use_relative_paths, + N_("use relative paths for worktrees")), OPT_END() }; struct worktree **worktrees, *wt; @@ -1247,7 +1250,7 @@ static int move_worktree(int ac, const char **av, const char *prefix, if (rename(wt->path, dst.buf) == -1) die_errno(_("failed to move '%s' to '%s'"), wt->path, dst.buf); - update_worktree_location(wt, dst.buf); + update_worktree_location(wt, dst.buf, use_relative_paths); strbuf_release(&dst); free_worktrees(worktrees); @@ -1390,6 +1393,8 @@ static int repair(int ac, const char **av, const char *prefix, const char **p; const char *self[] = { ".", NULL }; struct option options[] = { + OPT_BOOL(0, "relative-paths", &use_relative_paths, + N_("use relative paths for worktrees")), OPT_END() }; int rc = 0; @@ -1397,8 +1402,8 @@ static int repair(int ac, const char **av, const char *prefix, ac = parse_options(ac, av, prefix, options, git_worktree_repair_usage, 0); p = ac > 0 ? av : self; for (; *p; p++) - repair_worktree_at_path(*p, report_repair, &rc); - repair_worktrees(report_repair, &rc); + repair_worktree_at_path(*p, report_repair, &rc, use_relative_paths); + repair_worktrees(report_repair, &rc, use_relative_paths); return rc; } diff --git a/repository.c b/repository.c index f988b8ae68..1a6a62bbd0 100644 --- a/repository.c +++ b/repository.c @@ -283,6 +283,7 @@ int repo_init(struct repository *repo, repo_set_compat_hash_algo(repo, format.compat_hash_algo); repo_set_ref_storage_format(repo, format.ref_storage_format); repo->repository_format_worktree_config = format.worktree_config; + repo->repository_format_relative_worktrees = format.relative_worktrees; /* take ownership of format.partial_clone */ repo->repository_format_partial_clone = format.partial_clone; diff --git a/repository.h b/repository.h index 24a66a496a..c4c92b2ab9 100644 --- a/repository.h +++ b/repository.h @@ -150,6 +150,7 @@ struct repository { /* Configurations */ int repository_format_worktree_config; + int repository_format_relative_worktrees; /* Indicate if a repository has a different 'commondir' from 'gitdir' */ unsigned different_commondir:1; @@ -683,6 +683,9 @@ static enum extension_result handle_extension(const char *var, "extensions.refstorage", value); data->ref_storage_format = format; return EXTENSION_OK; + } else if (!strcmp(ext, "relativeworktrees")) { + data->relative_worktrees = git_config_bool(var, value); + return EXTENSION_OK; } return EXTENSION_UNKNOWN; } @@ -1854,6 +1857,8 @@ const char *setup_git_directory_gently(int *nongit_ok) repo_fmt.ref_storage_format); the_repository->repository_format_worktree_config = repo_fmt.worktree_config; + the_repository->repository_format_relative_worktrees = + repo_fmt.relative_worktrees; /* take ownership of repo_fmt.partial_clone */ the_repository->repository_format_partial_clone = repo_fmt.partial_clone; @@ -1950,6 +1955,8 @@ void check_repository_format(struct repository_format *fmt) fmt->ref_storage_format); the_repository->repository_format_worktree_config = fmt->worktree_config; + the_repository->repository_format_relative_worktrees = + fmt->relative_worktrees; the_repository->repository_format_partial_clone = xstrdup_or_null(fmt->partial_clone); clear_repository_format(&repo_fmt); @@ -2204,8 +2211,8 @@ void initialize_repository_version(int hash_algo, enum ref_storage_format ref_storage_format, int reinit) { - char repo_version_string[10]; - int repo_version = GIT_REPO_VERSION; + struct strbuf repo_version = STRBUF_INIT; + int target_version = GIT_REPO_VERSION; /* * Note that we initialize the repository version to 1 when the ref @@ -2216,12 +2223,7 @@ void initialize_repository_version(int hash_algo, */ if (hash_algo != GIT_HASH_SHA1 || ref_storage_format != REF_STORAGE_FORMAT_FILES) - repo_version = GIT_REPO_VERSION_READ; - - /* This forces creation of new config file */ - xsnprintf(repo_version_string, sizeof(repo_version_string), - "%d", repo_version); - git_config_set("core.repositoryformatversion", repo_version_string); + target_version = GIT_REPO_VERSION_READ; if (hash_algo != GIT_HASH_SHA1 && hash_algo != GIT_HASH_UNKNOWN) git_config_set("extensions.objectformat", @@ -2234,6 +2236,25 @@ void initialize_repository_version(int hash_algo, ref_storage_format_to_name(ref_storage_format)); else if (reinit) git_config_set_gently("extensions.refstorage", NULL); + + if (reinit) { + struct strbuf config = STRBUF_INIT; + struct repository_format repo_fmt = REPOSITORY_FORMAT_INIT; + + strbuf_git_common_path(&config, the_repository, "config"); + read_repository_format(&repo_fmt, config.buf); + + if (repo_fmt.v1_only_extensions.nr) + target_version = GIT_REPO_VERSION_READ; + + strbuf_release(&config); + clear_repository_format(&repo_fmt); + } + + strbuf_addf(&repo_version, "%d", target_version); + git_config_set("core.repositoryformatversion", repo_version.buf); + + strbuf_release(&repo_version); } static int is_reinit(void) @@ -2333,7 +2354,7 @@ static int create_default_files(const char *template_path, adjust_shared_perm(repo_get_git_dir(the_repository)); } - initialize_repository_version(fmt->hash_algo, fmt->ref_storage_format, 0); + initialize_repository_version(fmt->hash_algo, fmt->ref_storage_format, reinit); /* Check filemode trustability */ path = git_path_buf(&buf, "config"); @@ -129,6 +129,7 @@ struct repository_format { int precious_objects; char *partial_clone; /* value of extensions.partialclone */ int worktree_config; + int relative_worktrees; int is_bare; int hash_algo; int compat_hash_algo; diff --git a/t/t0001-init.sh b/t/t0001-init.sh index 4890dff4b2..72a0c2e7d4 100755 --- a/t/t0001-init.sh +++ b/t/t0001-init.sh @@ -433,6 +433,12 @@ test_expect_success SYMLINKS 're-init to move gitdir symlink' ' sep_git_dir_worktree () { test_when_finished "rm -rf mainwt linkwt seprepo" && git init mainwt && + if test "relative" = $2 + then + test_config -C mainwt worktree.useRelativePaths true + else + test_config -C mainwt worktree.useRelativePaths false + fi test_commit -C mainwt gumby && git -C mainwt worktree add --detach ../linkwt && git -C "$1" init --separate-git-dir ../seprepo && @@ -441,12 +447,20 @@ sep_git_dir_worktree () { test_cmp expect actual } -test_expect_success 're-init to move gitdir with linked worktrees' ' - sep_git_dir_worktree mainwt +test_expect_success 're-init to move gitdir with linked worktrees (absolute)' ' + sep_git_dir_worktree mainwt absolute +' + +test_expect_success 're-init to move gitdir within linked worktree (absolute)' ' + sep_git_dir_worktree linkwt absolute +' + +test_expect_success 're-init to move gitdir with linked worktrees (relative)' ' + sep_git_dir_worktree mainwt relative ' -test_expect_success 're-init to move gitdir within linked worktree' ' - sep_git_dir_worktree linkwt +test_expect_success 're-init to move gitdir within linked worktree (relative)' ' + sep_git_dir_worktree linkwt relative ' test_expect_success MINGW '.git hidden' ' diff --git a/t/t2400-worktree-add.sh b/t/t2400-worktree-add.sh index ba320dc417..90638fa886 100755 --- a/t/t2400-worktree-add.sh +++ b/t/t2400-worktree-add.sh @@ -1206,4 +1206,50 @@ test_expect_success '"add" with initialized submodule, with submodule.recurse se git -C project-clone -c submodule.recurse worktree add ../project-5 ' +test_expect_success 'can create worktrees with relative paths' ' + test_when_finished "git worktree remove relative" && + test_config worktree.useRelativePaths false && + git worktree add --relative-paths ./relative && + echo "gitdir: ../.git/worktrees/relative" >expect && + test_cmp expect relative/.git && + echo "../../../relative/.git" >expect && + test_cmp expect .git/worktrees/relative/gitdir +' + +test_expect_success 'can create worktrees with absolute paths' ' + test_config worktree.useRelativePaths true && + git worktree add ./relative && + echo "gitdir: ../.git/worktrees/relative" >expect && + test_cmp expect relative/.git && + git worktree add --no-relative-paths ./absolute && + echo "gitdir: $(pwd)/.git/worktrees/absolute" >expect && + test_cmp expect absolute/.git && + echo "$(pwd)/absolute/.git" >expect && + test_cmp expect .git/worktrees/absolute/gitdir +' + +test_expect_success 'move repo without breaking relative internal links' ' + test_when_finished rm -rf repo moved && + git init repo && + ( + cd repo && + test_commit initial && + git worktree add --relative-paths wt1 && + cd .. && + mv repo moved && + cd moved/wt1 && + git worktree list >out 2>err && + test_must_be_empty err + ) +' + +test_expect_success 'relative worktree sets extension config' ' + test_when_finished "rm -rf repo" && + git init repo && + git -C repo commit --allow-empty -m base && + git -C repo worktree add --relative-paths ./foo && + test_cmp_config -C repo 1 core.repositoryformatversion && + test_cmp_config -C repo true extensions.relativeworktrees +' + test_done diff --git a/t/t2401-worktree-prune.sh b/t/t2401-worktree-prune.sh index aa5b42c0f7..fe671d4197 100755 --- a/t/t2401-worktree-prune.sh +++ b/t/t2401-worktree-prune.sh @@ -119,11 +119,12 @@ test_expect_success 'prune duplicate (main/linked)' ' ! test -d .git/worktrees/wt ' -test_expect_success 'not prune proper worktrees when run inside linked worktree' ' +test_expect_success 'not prune proper worktrees inside linked worktree with relative paths' ' test_when_finished rm -rf repo wt_ext && git init repo && ( cd repo && + git config worktree.useRelativePaths true && echo content >file && git add file && git commit -m msg && diff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh index cb125ec226..8ef1cad7f2 100755 --- a/t/t2402-worktree-list.sh +++ b/t/t2402-worktree-list.sh @@ -260,6 +260,7 @@ test_expect_success 'broken main worktree still at the top' ' ' test_expect_success 'linked worktrees are sorted' ' + test_when_finished "rm -rf sorted" && mkdir sorted && git init sorted/main && ( @@ -279,6 +280,27 @@ test_expect_success 'linked worktrees are sorted' ' test_cmp expected sorted/main/actual ' +test_expect_success 'linked worktrees with relative paths are shown with absolute paths' ' + test_when_finished "rm -rf sorted" && + mkdir sorted && + git init sorted/main && + ( + cd sorted/main && + test_tick && + test_commit new && + git worktree add --relative-paths ../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_expect_success 'worktree path when called in .git directory' ' git worktree list >list1 && git -C .git worktree list >list2 && diff --git a/t/t2403-worktree-move.sh b/t/t2403-worktree-move.sh index 08531deb5b..0bb33e8b1b 100755 --- a/t/t2403-worktree-move.sh +++ b/t/t2403-worktree-move.sh @@ -246,4 +246,29 @@ test_expect_success 'not remove a repo with initialized submodule' ' ) ' +test_expect_success 'move worktree with absolute path to relative path' ' + test_config worktree.useRelativePaths false && + git worktree add ./absolute && + git worktree move --relative-paths absolute relative && + echo "gitdir: ../.git/worktrees/absolute" >expect && + test_cmp expect relative/.git && + echo "../../../relative/.git" >expect && + test_cmp expect .git/worktrees/absolute/gitdir && + test_config worktree.useRelativePaths true && + git worktree move relative relative2 && + echo "gitdir: ../.git/worktrees/absolute" >expect && + test_cmp expect relative2/.git && + echo "../../../relative2/.git" >expect && + test_cmp expect .git/worktrees/absolute/gitdir +' + +test_expect_success 'move worktree with relative path to absolute path' ' + test_config worktree.useRelativePaths true && + git worktree move --no-relative-paths relative2 absolute && + echo "gitdir: $(pwd)/.git/worktrees/absolute" >expect && + test_cmp expect absolute/.git && + echo "$(pwd)/absolute/.git" >expect && + test_cmp expect .git/worktrees/absolute/gitdir +' + test_done diff --git a/t/t2406-worktree-repair.sh b/t/t2406-worktree-repair.sh index bf340b8772..f5f19b3169 100755 --- a/t/t2406-worktree-repair.sh +++ b/t/t2406-worktree-repair.sh @@ -215,4 +215,43 @@ test_expect_success 'repair copied main and linked worktrees' ' test_cmp dup/linked.expect dup/linked/.git ' +test_expect_success 'repair worktree with relative path with missing gitfile' ' + test_when_finished "rm -rf main wt" && + test_create_repo main && + git -C main config worktree.useRelativePaths true && + test_commit -C main init && + git -C main worktree add --detach ../wt && + rm wt/.git && + test_path_is_missing wt/.git && + git -C main worktree repair && + echo "gitdir: ../main/.git/worktrees/wt" >expect && + test_cmp expect wt/.git +' + +test_expect_success 'repair absolute worktree to use relative paths' ' + test_when_finished "rm -rf main side sidemoved" && + test_create_repo main && + test_commit -C main init && + git -C main worktree add --detach ../side && + echo "../../../../sidemoved/.git" >expect-gitdir && + echo "gitdir: ../main/.git/worktrees/side" >expect-gitfile && + mv side sidemoved && + git -C main worktree repair --relative-paths ../sidemoved && + test_cmp expect-gitdir main/.git/worktrees/side/gitdir && + test_cmp expect-gitfile sidemoved/.git +' + +test_expect_success 'repair relative worktree to use absolute paths' ' + test_when_finished "rm -rf main side sidemoved" && + test_create_repo main && + test_commit -C main init && + git -C main worktree add --relative-paths --detach ../side && + echo "$(pwd)/sidemoved/.git" >expect-gitdir && + echo "gitdir: $(pwd)/main/.git/worktrees/side" >expect-gitfile && + mv side sidemoved && + git -C main worktree repair ../sidemoved && + test_cmp expect-gitdir main/.git/worktrees/side/gitdir && + test_cmp expect-gitfile sidemoved/.git +' + test_done diff --git a/t/t2408-worktree-relative.sh b/t/t2408-worktree-relative.sh deleted file mode 100755 index d51cc8c5ab..0000000000 --- a/t/t2408-worktree-relative.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/sh - -test_description='test worktrees linked with relative paths' - -. ./test-lib.sh - -test_expect_success 'links worktrees with relative paths' ' - test_when_finished rm -rf repo && - git init repo && - ( - cd repo && - test_commit initial && - git worktree add wt1 && - echo "../../../wt1/.git" >expected_gitdir && - cat .git/worktrees/wt1/gitdir >actual_gitdir && - echo "gitdir: ../.git/worktrees/wt1" >expected_git && - cat wt1/.git >actual_git && - test_cmp expected_gitdir actual_gitdir && - test_cmp expected_git actual_git - ) -' - -test_expect_success 'move repo without breaking relative internal links' ' - test_when_finished rm -rf repo moved && - git init repo && - ( - cd repo && - test_commit initial && - git worktree add wt1 && - cd .. && - mv repo moved && - cd moved/wt1 && - git status >out 2>err && - test_must_be_empty err - ) -' - -test_done diff --git a/t/t5504-fetch-receive-strict.sh b/t/t5504-fetch-receive-strict.sh index 53dbc8ce3a..8212a70be8 100755 --- a/t/t5504-fetch-receive-strict.sh +++ b/t/t5504-fetch-receive-strict.sh @@ -170,7 +170,7 @@ test_expect_success 'fsck with invalid or bogus skipList input' ' test_must_fail git -c fsck.skipList=does-not-exist -c fsck.missingEmail=ignore fsck 2>err && test_grep "could not open.*: does-not-exist" err && test_must_fail git -c fsck.skipList=.git/config -c fsck.missingEmail=ignore fsck 2>err && - test_grep "invalid object name: \[core\]" err + test_grep "invalid object name: " err ' test_expect_success 'fsck with other accepted skipList input (comments & empty lines)' ' @@ -233,7 +233,7 @@ test_expect_success 'push with receive.fsck.skipList' ' test_grep "could not open.*: does-not-exist" err && git --git-dir=dst/.git config receive.fsck.skipList config && test_must_fail git push --porcelain dst bogus 2>err && - test_grep "invalid object name: \[core\]" err && + test_grep "invalid object name: " err && git --git-dir=dst/.git config receive.fsck.skipList SKIP && git push --porcelain dst bogus @@ -262,7 +262,7 @@ test_expect_success 'fetch with fetch.fsck.skipList' ' test_grep "could not open.*: does-not-exist" err && git --git-dir=dst/.git config fetch.fsck.skipList dst/.git/config && test_must_fail git --git-dir=dst/.git fetch "file://$(pwd)" $refspec 2>err && - test_grep "invalid object name: \[core\]" err && + test_grep "invalid object name: " err && git --git-dir=dst/.git config fetch.fsck.skipList dst/.git/SKIP && git --git-dir=dst/.git fetch "file://$(pwd)" $refspec diff --git a/worktree.c b/worktree.c index 77ff484d3e..af68b24f9d 100644 --- a/worktree.c +++ b/worktree.c @@ -111,9 +111,9 @@ struct worktree *get_linked_worktree(const char *id, strbuf_strip_suffix(&worktree_path, "/.git"); if (!is_absolute_path(worktree_path.buf)) { - strbuf_strip_suffix(&path, "gitdir"); - strbuf_addbuf(&path, &worktree_path); - strbuf_realpath_forgiving(&worktree_path, path.buf, 0); + strbuf_strip_suffix(&path, "gitdir"); + strbuf_addbuf(&path, &worktree_path); + strbuf_realpath_forgiving(&worktree_path, path.buf, 0); } CALLOC_ARRAY(worktree, 1); @@ -376,32 +376,28 @@ done: return ret; } -void update_worktree_location(struct worktree *wt, const char *path_) +void update_worktree_location(struct worktree *wt, const char *path_, + int use_relative_paths) { struct strbuf path = STRBUF_INIT; - struct strbuf repo = STRBUF_INIT; - struct strbuf file = STRBUF_INIT; - struct strbuf tmp = STRBUF_INIT; + struct strbuf dotgit = STRBUF_INIT; + struct strbuf gitdir = STRBUF_INIT; if (is_main_worktree(wt)) BUG("can't relocate main worktree"); - strbuf_realpath(&repo, git_common_path("worktrees/%s", wt->id), 1); + strbuf_realpath(&gitdir, git_common_path("worktrees/%s/gitdir", wt->id), 1); strbuf_realpath(&path, path_, 1); + strbuf_addf(&dotgit, "%s/.git", path.buf); if (fspathcmp(wt->path, path.buf)) { - strbuf_addf(&file, "%s/gitdir", repo.buf); - write_file(file.buf, "%s/.git", relative_path(path.buf, repo.buf, &tmp)); - strbuf_reset(&file); - strbuf_addf(&file, "%s/.git", path.buf); - write_file(file.buf, "gitdir: %s", relative_path(repo.buf, path.buf, &tmp)); + write_worktree_linking_files(dotgit, gitdir, use_relative_paths); free(wt->path); wt->path = strbuf_detach(&path, NULL); } strbuf_release(&path); - strbuf_release(&repo); - strbuf_release(&file); - strbuf_release(&tmp); + strbuf_release(&dotgit); + strbuf_release(&gitdir); } int is_worktree_being_rebased(const struct worktree *wt, @@ -577,12 +573,13 @@ int other_head_refs(each_ref_fn fn, void *cb_data) * pointing at <repo>/worktrees/<id>. */ static void repair_gitfile(struct worktree *wt, - worktree_repair_fn fn, void *cb_data) + worktree_repair_fn fn, void *cb_data, + int use_relative_paths) { struct strbuf dotgit = STRBUF_INIT; + struct strbuf gitdir = STRBUF_INIT; struct strbuf repo = STRBUF_INIT; struct strbuf backlink = STRBUF_INIT; - struct strbuf tmp = STRBUF_INIT; char *dotgit_contents = NULL; const char *repair = NULL; int err; @@ -598,6 +595,7 @@ static void repair_gitfile(struct worktree *wt, strbuf_realpath(&repo, git_common_path("worktrees/%s", wt->id), 1); strbuf_addf(&dotgit, "%s/.git", wt->path); + strbuf_addf(&gitdir, "%s/gitdir", repo.buf); dotgit_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err)); if (dotgit_contents) { @@ -615,18 +613,20 @@ static void repair_gitfile(struct worktree *wt, repair = _(".git file broken"); else if (fspathcmp(backlink.buf, repo.buf)) repair = _(".git file incorrect"); + else if (use_relative_paths == is_absolute_path(dotgit_contents)) + repair = _(".git file absolute/relative path mismatch"); if (repair) { fn(0, wt->path, repair, cb_data); - write_file(dotgit.buf, "gitdir: %s", relative_path(repo.buf, wt->path, &tmp)); + write_worktree_linking_files(dotgit, gitdir, use_relative_paths); } done: free(dotgit_contents); strbuf_release(&repo); strbuf_release(&dotgit); + strbuf_release(&gitdir); strbuf_release(&backlink); - strbuf_release(&tmp); } static void repair_noop(int iserr UNUSED, @@ -637,7 +637,7 @@ static void repair_noop(int iserr UNUSED, /* nothing */ } -void repair_worktrees(worktree_repair_fn fn, void *cb_data) +void repair_worktrees(worktree_repair_fn fn, void *cb_data, int use_relative_paths) { struct worktree **worktrees = get_worktrees_internal(1); struct worktree **wt = worktrees + 1; /* +1 skips main worktree */ @@ -645,51 +645,38 @@ void repair_worktrees(worktree_repair_fn fn, void *cb_data) if (!fn) fn = repair_noop; for (; *wt; wt++) - repair_gitfile(*wt, fn, cb_data); + repair_gitfile(*wt, fn, cb_data, use_relative_paths); free_worktrees(worktrees); } void repair_worktree_after_gitdir_move(struct worktree *wt, const char *old_path) { - struct strbuf path = STRBUF_INIT; - struct strbuf repo = STRBUF_INIT; struct strbuf gitdir = STRBUF_INIT; struct strbuf dotgit = STRBUF_INIT; - struct strbuf olddotgit = STRBUF_INIT; - struct strbuf tmp = STRBUF_INIT; + int is_relative_path; if (is_main_worktree(wt)) goto done; - strbuf_realpath(&repo, git_common_path("worktrees/%s", wt->id), 1); - strbuf_addf(&gitdir, "%s/gitdir", repo.buf); + strbuf_realpath(&gitdir, git_common_path("worktrees/%s/gitdir", wt->id), 1); - if (strbuf_read_file(&olddotgit, gitdir.buf, 0) < 0) + if (strbuf_read_file(&dotgit, gitdir.buf, 0) < 0) goto done; - strbuf_rtrim(&olddotgit); - if (is_absolute_path(olddotgit.buf)) { - strbuf_addbuf(&dotgit, &olddotgit); - } else { - strbuf_addf(&dotgit, "%s/worktrees/%s/%s", old_path, wt->id, olddotgit.buf); + strbuf_rtrim(&dotgit); + is_relative_path = ! is_absolute_path(dotgit.buf); + if (is_relative_path) { + strbuf_insertf(&dotgit, 0, "%s/worktrees/%s/", old_path, wt->id); strbuf_realpath_forgiving(&dotgit, dotgit.buf, 0); } if (!file_exists(dotgit.buf)) goto done; - strbuf_addbuf(&path, &dotgit); - strbuf_strip_suffix(&path, "/.git"); - - write_file(dotgit.buf, "gitdir: %s", relative_path(repo.buf, path.buf, &tmp)); - write_file(gitdir.buf, "%s", relative_path(dotgit.buf, repo.buf, &tmp)); + write_worktree_linking_files(dotgit, gitdir, is_relative_path); done: - strbuf_release(&path); - strbuf_release(&repo); strbuf_release(&gitdir); strbuf_release(&dotgit); - strbuf_release(&olddotgit); - strbuf_release(&tmp); } void repair_worktrees_after_gitdir_move(const char *old_path) @@ -725,8 +712,10 @@ static int is_main_worktree_path(const char *path) * won't know which <repo>/worktrees/<id>/gitdir to repair. However, we may * be able to infer the gitdir by manually reading /path/to/worktree/.git, * extracting the <id>, and checking if <repo>/worktrees/<id> exists. + * + * Returns -1 on failure and strbuf.len on success. */ -static int infer_backlink(const char *gitfile, struct strbuf *inferred) +static ssize_t infer_backlink(const char *gitfile, struct strbuf *inferred) { struct strbuf actual = STRBUF_INIT; const char *id; @@ -747,12 +736,11 @@ static int infer_backlink(const char *gitfile, struct strbuf *inferred) goto error; strbuf_release(&actual); - return 1; - + return inferred->len; error: strbuf_release(&actual); strbuf_reset(inferred); /* clear invalid path */ - return 0; + return -1; } /* @@ -760,16 +748,14 @@ error: * the worktree's path. */ void repair_worktree_at_path(const char *path, - worktree_repair_fn fn, void *cb_data) + worktree_repair_fn fn, void *cb_data, + int use_relative_paths) { struct strbuf dotgit = STRBUF_INIT; - struct strbuf realdotgit = STRBUF_INIT; struct strbuf backlink = STRBUF_INIT; struct strbuf inferred_backlink = STRBUF_INIT; struct strbuf gitdir = STRBUF_INIT; struct strbuf olddotgit = STRBUF_INIT; - struct strbuf realolddotgit = STRBUF_INIT; - struct strbuf tmp = STRBUF_INIT; char *dotgit_contents = NULL; const char *repair = NULL; int err; @@ -781,25 +767,25 @@ void repair_worktree_at_path(const char *path, goto done; strbuf_addf(&dotgit, "%s/.git", path); - if (!strbuf_realpath(&realdotgit, dotgit.buf, 0)) { + if (!strbuf_realpath(&dotgit, dotgit.buf, 0)) { fn(1, path, _("not a valid path"), cb_data); goto done; } - infer_backlink(realdotgit.buf, &inferred_backlink); + infer_backlink(dotgit.buf, &inferred_backlink); strbuf_realpath_forgiving(&inferred_backlink, inferred_backlink.buf, 0); - dotgit_contents = xstrdup_or_null(read_gitfile_gently(realdotgit.buf, &err)); + dotgit_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err)); if (dotgit_contents) { if (is_absolute_path(dotgit_contents)) { strbuf_addstr(&backlink, dotgit_contents); } else { - strbuf_addbuf(&backlink, &realdotgit); + strbuf_addbuf(&backlink, &dotgit); strbuf_strip_suffix(&backlink, ".git"); strbuf_addstr(&backlink, dotgit_contents); strbuf_realpath_forgiving(&backlink, backlink.buf, 0); } } else if (err == READ_GITFILE_ERR_NOT_A_FILE) { - fn(1, realdotgit.buf, _("unable to locate repository; .git is not a file"), cb_data); + fn(1, dotgit.buf, _("unable to locate repository; .git is not a file"), cb_data); goto done; } else if (err == READ_GITFILE_ERR_NOT_A_REPO) { if (inferred_backlink.len) { @@ -812,11 +798,11 @@ void repair_worktree_at_path(const char *path, */ strbuf_swap(&backlink, &inferred_backlink); } else { - fn(1, realdotgit.buf, _("unable to locate repository; .git file does not reference a repository"), cb_data); + fn(1, dotgit.buf, _("unable to locate repository; .git file does not reference a repository"), cb_data); goto done; } } else { - fn(1, realdotgit.buf, _("unable to locate repository; .git file broken"), cb_data); + fn(1, dotgit.buf, _("unable to locate repository; .git file broken"), cb_data); goto done; } @@ -838,39 +824,35 @@ void repair_worktree_at_path(const char *path, * in the "copy" repository. In this case, point the "copy" worktree's * .git file at the "copy" repository. */ - if (inferred_backlink.len && fspathcmp(backlink.buf, inferred_backlink.buf)) { + if (inferred_backlink.len && fspathcmp(backlink.buf, inferred_backlink.buf)) strbuf_swap(&backlink, &inferred_backlink); - } strbuf_addf(&gitdir, "%s/gitdir", backlink.buf); if (strbuf_read_file(&olddotgit, gitdir.buf, 0) < 0) repair = _("gitdir unreadable"); + else if (use_relative_paths == is_absolute_path(olddotgit.buf)) + repair = _("gitdir absolute/relative path mismatch"); else { strbuf_rtrim(&olddotgit); - if (is_absolute_path(olddotgit.buf)) { - strbuf_addbuf(&realolddotgit, &olddotgit); - } else { - strbuf_addf(&realolddotgit, "%s/%s", backlink.buf, olddotgit.buf); - strbuf_realpath_forgiving(&realolddotgit, realolddotgit.buf, 0); + if (!is_absolute_path(olddotgit.buf)) { + strbuf_insertf(&olddotgit, 0, "%s/", backlink.buf); + strbuf_realpath_forgiving(&olddotgit, olddotgit.buf, 0); } - if (fspathcmp(realolddotgit.buf, realdotgit.buf)) + if (fspathcmp(olddotgit.buf, dotgit.buf)) repair = _("gitdir incorrect"); } if (repair) { fn(0, gitdir.buf, repair, cb_data); - write_file(gitdir.buf, "%s", relative_path(realdotgit.buf, backlink.buf, &tmp)); + write_worktree_linking_files(dotgit, gitdir, use_relative_paths); } done: free(dotgit_contents); strbuf_release(&olddotgit); - strbuf_release(&realolddotgit); strbuf_release(&backlink); strbuf_release(&inferred_backlink); strbuf_release(&gitdir); - strbuf_release(&realdotgit); strbuf_release(&dotgit); - strbuf_release(&tmp); } int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath, timestamp_t expire) @@ -1031,3 +1013,38 @@ cleanup: free(main_worktree_file); return res; } + +void write_worktree_linking_files(struct strbuf dotgit, struct strbuf gitdir, + int use_relative_paths) +{ + struct strbuf path = STRBUF_INIT; + struct strbuf repo = STRBUF_INIT; + struct strbuf tmp = STRBUF_INIT; + + strbuf_addbuf(&path, &dotgit); + strbuf_strip_suffix(&path, "/.git"); + strbuf_realpath(&path, path.buf, 1); + strbuf_addbuf(&repo, &gitdir); + strbuf_strip_suffix(&repo, "/gitdir"); + strbuf_realpath(&repo, repo.buf, 1); + + if (use_relative_paths && !the_repository->repository_format_relative_worktrees) { + if (upgrade_repository_format(1) < 0) + die(_("unable to upgrade repository format to support relative worktrees")); + if (git_config_set_gently("extensions.relativeWorktrees", "true")) + die(_("unable to set extensions.relativeWorktrees setting")); + the_repository->repository_format_relative_worktrees = 1; + } + + if (use_relative_paths) { + write_file(gitdir.buf, "%s/.git", relative_path(path.buf, repo.buf, &tmp)); + write_file(dotgit.buf, "gitdir: %s", relative_path(repo.buf, path.buf, &tmp)); + } else { + write_file(gitdir.buf, "%s/.git", path.buf); + write_file(dotgit.buf, "gitdir: %s", repo.buf); + } + + strbuf_release(&path); + strbuf_release(&repo); + strbuf_release(&tmp); +} diff --git a/worktree.h b/worktree.h index e961186216..38145df80f 100644 --- a/worktree.h +++ b/worktree.h @@ -117,8 +117,8 @@ int validate_worktree(const struct worktree *wt, /* * Update worktrees/xxx/gitdir with the new path. */ -void update_worktree_location(struct worktree *wt, - const char *path_); +void update_worktree_location(struct worktree *wt, const char *path_, + int use_relative_paths); typedef void (* worktree_repair_fn)(int iserr, const char *path, const char *msg, void *cb_data); @@ -129,7 +129,7 @@ typedef void (* worktree_repair_fn)(int iserr, const char *path, * function, if non-NULL, is called with the path of the worktree and a * description of the repair or error, along with the callback user-data. */ -void repair_worktrees(worktree_repair_fn, void *cb_data); +void repair_worktrees(worktree_repair_fn, void *cb_data, int use_relative_paths); /* * Repair the linked worktrees after the gitdir has been moved. @@ -151,7 +151,8 @@ void repair_worktree_after_gitdir_move(struct worktree *wt, const char *old_path * worktree and a description of the repair or error, along with the callback * user-data. */ -void repair_worktree_at_path(const char *, worktree_repair_fn, void *cb_data); +void repair_worktree_at_path(const char *, worktree_repair_fn, + void *cb_data, int use_relative_paths); /* * Free up the memory for a worktree. @@ -215,4 +216,17 @@ void strbuf_worktree_ref(const struct worktree *wt, */ int init_worktree_config(struct repository *r); +/** + * Write the .git file and gitdir file that links the worktree to the repository. + * + * The `dotgit` parameter is the path to the worktree's .git file, and `gitdir` + * is the path to the repository's `gitdir` file. + * + * Example + * dotgit: "/path/to/foo/.git" + * gitdir: "/path/to/repo/worktrees/foo/gitdir" + */ +void write_worktree_linking_files(struct strbuf dotgit, struct strbuf gitdir, + int use_relative_paths); + #endif |