diff options
Diffstat (limited to 'worktree.c')
-rw-r--r-- | worktree.c | 384 |
1 files changed, 281 insertions, 103 deletions
diff --git a/worktree.c b/worktree.c index b8cf29e6a1..77ff484d3e 100644 --- a/worktree.c +++ b/worktree.c @@ -1,3 +1,5 @@ +#define USE_THE_REPOSITORY_VARIABLE + #include "git-compat-util.h" #include "abspath.h" #include "environment.h" @@ -12,18 +14,23 @@ #include "wt-status.h" #include "config.h" +void free_worktree(struct worktree *worktree) +{ + if (!worktree) + return; + free(worktree->path); + free(worktree->id); + free(worktree->head_ref); + free(worktree->lock_reason); + free(worktree->prune_reason); + free(worktree); +} + void free_worktrees(struct worktree **worktrees) { int i = 0; - - for (i = 0; worktrees[i]; i++) { - free(worktrees[i]->path); - free(worktrees[i]->id); - free(worktrees[i]->head_ref); - free(worktrees[i]->lock_reason); - free(worktrees[i]->prune_reason); - free(worktrees[i]); - } + for (i = 0; worktrees[i]; i++) + free_worktree(worktrees[i]); free (worktrees); } @@ -48,18 +55,28 @@ static void add_head_info(struct worktree *wt) wt->is_detached = 1; } +static int is_current_worktree(struct worktree *wt) +{ + char *git_dir = absolute_pathdup(repo_get_git_dir(the_repository)); + const char *wt_git_dir = get_worktree_git_dir(wt); + int is_current = !fspathcmp(git_dir, absolute_path(wt_git_dir)); + free(git_dir); + return is_current; +} + /** * get the main worktree */ -static struct worktree *get_main_worktree(void) +static struct worktree *get_main_worktree(int skip_reading_head) { struct worktree *worktree = NULL; struct strbuf worktree_path = STRBUF_INIT; - strbuf_add_real_path(&worktree_path, get_git_common_dir()); + strbuf_add_real_path(&worktree_path, repo_get_common_dir(the_repository)); strbuf_strip_suffix(&worktree_path, "/.git"); CALLOC_ARRAY(worktree, 1); + worktree->repo = the_repository; worktree->path = strbuf_detach(&worktree_path, NULL); /* * NEEDSWORK: If this function is called from a secondary worktree and @@ -70,11 +87,14 @@ static struct worktree *get_main_worktree(void) */ worktree->is_bare = (is_bare_repository_cfg == 1) || is_bare_repository(); - add_head_info(worktree); + worktree->is_current = is_current_worktree(worktree); + if (!skip_reading_head) + add_head_info(worktree); return worktree; } -static struct worktree *get_linked_worktree(const char *id) +struct worktree *get_linked_worktree(const char *id, + int skip_reading_head) { struct worktree *worktree = NULL; struct strbuf path = STRBUF_INIT; @@ -90,10 +110,19 @@ static struct worktree *get_linked_worktree(const char *id) strbuf_rtrim(&worktree_path); 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); + } + CALLOC_ARRAY(worktree, 1); + worktree->repo = the_repository; worktree->path = strbuf_detach(&worktree_path, NULL); worktree->id = xstrdup(id); - add_head_info(worktree); + worktree->is_current = is_current_worktree(worktree); + if (!skip_reading_head) + add_head_info(worktree); done: strbuf_release(&path); @@ -101,24 +130,14 @@ done: return worktree; } -static void mark_current_worktree(struct worktree **worktrees) -{ - char *git_dir = absolute_pathdup(get_git_dir()); - int i; - - for (i = 0; worktrees[i]; i++) { - struct worktree *wt = worktrees[i]; - const char *wt_git_dir = get_worktree_git_dir(wt); - - if (!fspathcmp(git_dir, absolute_path(wt_git_dir))) { - wt->is_current = 1; - break; - } - } - free(git_dir); -} - -struct worktree **get_worktrees(void) +/* + * NEEDSWORK: This function exists so that we can look up metadata of a + * worktree without trying to access any of its internals like the refdb. It + * would be preferable to instead have a corruption-tolerant function for + * retrieving worktree metadata that could be used when the worktree is known + * to not be in a healthy state, e.g. when creating or repairing it. + */ +static struct worktree **get_worktrees_internal(int skip_reading_head) { struct worktree **list = NULL; struct strbuf path = STRBUF_INIT; @@ -128,16 +147,16 @@ struct worktree **get_worktrees(void) ALLOC_ARRAY(list, alloc); - list[counter++] = get_main_worktree(); + list[counter++] = get_main_worktree(skip_reading_head); - strbuf_addf(&path, "%s/worktrees", get_git_common_dir()); + strbuf_addf(&path, "%s/worktrees", repo_get_common_dir(the_repository)); dir = opendir(path.buf); strbuf_release(&path); if (dir) { while ((d = readdir_skip_dot_and_dotdot(dir)) != NULL) { struct worktree *linked = NULL; - if ((linked = get_linked_worktree(d->d_name))) { + if ((linked = get_linked_worktree(d->d_name, skip_reading_head))) { ALLOC_GROW(list, counter + 1, alloc); list[counter++] = linked; } @@ -147,16 +166,20 @@ struct worktree **get_worktrees(void) ALLOC_GROW(list, counter + 1, alloc); list[counter] = NULL; - mark_current_worktree(list); return list; } +struct worktree **get_worktrees(void) +{ + return get_worktrees_internal(0); +} + const char *get_worktree_git_dir(const struct worktree *wt) { if (!wt) - return get_git_dir(); + return repo_get_git_dir(the_repository); else if (!wt->id) - return get_git_common_dir(); + return repo_get_common_dir(the_repository); else return git_common_path("worktrees/%s", wt->id); } @@ -235,7 +258,7 @@ const char *worktree_lock_reason(struct worktree *wt) if (!wt->lock_reason_valid) { struct strbuf path = STRBUF_INIT; - strbuf_addstr(&path, worktree_git_path(wt, "locked")); + strbuf_addstr(&path, worktree_git_path(the_repository, wt, "locked")); if (file_exists(path.buf)) { struct strbuf lock_reason = STRBUF_INIT; if (strbuf_read_file(&lock_reason, path.buf, 0) < 0) @@ -356,18 +379,29 @@ done: void update_worktree_location(struct worktree *wt, const char *path_) { struct strbuf path = STRBUF_INIT; + struct strbuf repo = STRBUF_INIT; + struct strbuf file = STRBUF_INIT; + struct strbuf tmp = 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(&path, path_, 1); if (fspathcmp(wt->path, path.buf)) { - write_file(git_common_path("worktrees/%s/gitdir", wt->id), - "%s/.git", 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)); + free(wt->path); wt->path = strbuf_detach(&path, NULL); } strbuf_release(&path); + strbuf_release(&repo); + strbuf_release(&file); + strbuf_release(&tmp); } int is_worktree_being_rebased(const struct worktree *wt, @@ -395,9 +429,9 @@ int is_worktree_being_bisected(const struct worktree *wt, memset(&state, 0, sizeof(state)); found_bisect = wt_status_check_bisect(wt, &state) && - state.branch && + state.bisecting_from && skip_prefix(target, "refs/heads/", &target) && - !strcmp(state.branch, target); + !strcmp(state.bisecting_from, target); wt_status_state_free_buffers(&state); return found_bisect; } @@ -529,7 +563,7 @@ int other_head_refs(each_ref_fn fn, void *cb_data) refname.buf, RESOLVE_REF_READING, &oid, &flag)) - ret = fn(refname.buf, &oid, flag, cb_data); + ret = fn(refname.buf, NULL, &oid, flag, cb_data); if (ret) break; } @@ -547,49 +581,65 @@ static void repair_gitfile(struct worktree *wt, { struct strbuf dotgit = STRBUF_INIT; struct strbuf repo = STRBUF_INIT; - char *backlink; + struct strbuf backlink = STRBUF_INIT; + struct strbuf tmp = STRBUF_INIT; + char *dotgit_contents = NULL; const char *repair = NULL; int err; /* missing worktree can't be repaired */ if (!file_exists(wt->path)) - return; + goto done; if (!is_directory(wt->path)) { fn(1, wt->path, _("not a directory"), cb_data); - return; + goto done; } strbuf_realpath(&repo, git_common_path("worktrees/%s", wt->id), 1); strbuf_addf(&dotgit, "%s/.git", wt->path); - backlink = xstrdup_or_null(read_gitfile_gently(dotgit.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_addf(&backlink, "%s/%s", wt->path, dotgit_contents); + strbuf_realpath_forgiving(&backlink, backlink.buf, 0); + } + } if (err == READ_GITFILE_ERR_NOT_A_FILE) fn(1, wt->path, _(".git is not a file"), cb_data); else if (err) repair = _(".git file broken"); - else if (fspathcmp(backlink, repo.buf)) + else if (fspathcmp(backlink.buf, repo.buf)) repair = _(".git file incorrect"); if (repair) { fn(0, wt->path, repair, cb_data); - write_file(dotgit.buf, "gitdir: %s", repo.buf); + write_file(dotgit.buf, "gitdir: %s", relative_path(repo.buf, wt->path, &tmp)); } - free(backlink); +done: + free(dotgit_contents); strbuf_release(&repo); strbuf_release(&dotgit); + strbuf_release(&backlink); + strbuf_release(&tmp); } -static void repair_noop(int iserr, const char *path, const char *msg, - void *cb_data) +static void repair_noop(int iserr UNUSED, + const char *path UNUSED, + const char *msg UNUSED, + void *cb_data UNUSED) { /* nothing */ } void repair_worktrees(worktree_repair_fn fn, void *cb_data) { - struct worktree **worktrees = get_worktrees(); + struct worktree **worktrees = get_worktrees_internal(1); struct worktree **wt = worktrees + 1; /* +1 skips main worktree */ if (!fn) @@ -599,6 +649,59 @@ void repair_worktrees(worktree_repair_fn fn, void *cb_data) 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; + + 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); + + if (strbuf_read_file(&olddotgit, 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_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)); +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) +{ + struct worktree **worktrees = get_worktrees_internal(1); + struct worktree **wt = worktrees + 1; /* +1 skips main worktree */ + + for (; *wt; wt++) + repair_worktree_after_gitdir_move(*wt, old_path); + free_worktrees(worktrees); +} + static int is_main_worktree_path(const char *path) { struct strbuf target = STRBUF_INIT; @@ -607,7 +710,7 @@ static int is_main_worktree_path(const char *path) strbuf_add_real_path(&target, path); strbuf_strip_suffix(&target, "/.git"); - strbuf_add_real_path(&maindir, get_git_common_dir()); + strbuf_add_real_path(&maindir, repo_get_common_dir(the_repository)); strbuf_strip_suffix(&maindir, "/.git"); cmp = fspathcmp(maindir.buf, target.buf); @@ -623,10 +726,9 @@ static int is_main_worktree_path(const char *path) * be able to infer the gitdir by manually reading /path/to/worktree/.git, * extracting the <id>, and checking if <repo>/worktrees/<id> exists. */ -static char *infer_backlink(const char *gitfile) +static int infer_backlink(const char *gitfile, struct strbuf *inferred) { struct strbuf actual = STRBUF_INIT; - struct strbuf inferred = STRBUF_INIT; const char *id; if (strbuf_read_file(&actual, gitfile, 0) < 0) @@ -639,17 +741,18 @@ static char *infer_backlink(const char *gitfile) id++; /* advance past '/' to point at <id> */ if (!*id) goto error; - strbuf_git_common_path(&inferred, the_repository, "worktrees/%s", id); - if (!is_directory(inferred.buf)) + strbuf_reset(inferred); + strbuf_git_common_path(inferred, the_repository, "worktrees/%s", id); + if (!is_directory(inferred->buf)) goto error; strbuf_release(&actual); - return strbuf_detach(&inferred, NULL); + return 1; error: strbuf_release(&actual); - strbuf_release(&inferred); - return NULL; + strbuf_reset(inferred); /* clear invalid path */ + return 0; } /* @@ -661,9 +764,13 @@ void repair_worktree_at_path(const char *path, { 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; - char *backlink = NULL; + struct strbuf realolddotgit = STRBUF_INIT; + struct strbuf tmp = STRBUF_INIT; + char *dotgit_contents = NULL; const char *repair = NULL; int err; @@ -679,115 +786,186 @@ void repair_worktree_at_path(const char *path, goto done; } - backlink = xstrdup_or_null(read_gitfile_gently(realdotgit.buf, &err)); - if (err == READ_GITFILE_ERR_NOT_A_FILE) { + infer_backlink(realdotgit.buf, &inferred_backlink); + strbuf_realpath_forgiving(&inferred_backlink, inferred_backlink.buf, 0); + dotgit_contents = xstrdup_or_null(read_gitfile_gently(realdotgit.buf, &err)); + if (dotgit_contents) { + if (is_absolute_path(dotgit_contents)) { + strbuf_addstr(&backlink, dotgit_contents); + } else { + strbuf_addbuf(&backlink, &realdotgit); + 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); goto done; } else if (err == READ_GITFILE_ERR_NOT_A_REPO) { - if (!(backlink = infer_backlink(realdotgit.buf))) { + if (inferred_backlink.len) { + /* + * Worktree's .git file does not point at a repository + * but we found a .git/worktrees/<id> in this + * repository with the same <id> as recorded in the + * worktree's .git file so make the worktree point at + * the discovered .git/worktrees/<id>. + */ + strbuf_swap(&backlink, &inferred_backlink); + } else { fn(1, realdotgit.buf, _("unable to locate repository; .git file does not reference a repository"), cb_data); goto done; } - } else if (err) { + } else { fn(1, realdotgit.buf, _("unable to locate repository; .git file broken"), cb_data); goto done; } - strbuf_addf(&gitdir, "%s/gitdir", backlink); + /* + * If we got this far, either the worktree's .git file pointed at a + * valid repository (i.e. read_gitfile_gently() returned success) or + * the .git file did not point at a repository but we were able to + * infer a suitable new value for the .git file by locating a + * .git/worktrees/<id> in *this* repository corresponding to the <id> + * recorded in the worktree's .git file. + * + * However, if, at this point, inferred_backlink is non-NULL (i.e. we + * found a suitable .git/worktrees/<id> in *this* repository) *and* the + * worktree's .git file points at a valid repository *and* those two + * paths differ, then that indicates that the user probably *copied* + * the main and linked worktrees to a new location as a unit rather + * than *moving* them. Thus, the copied worktree's .git file actually + * points at the .git/worktrees/<id> in the *original* repository, not + * 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)) { + 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 { strbuf_rtrim(&olddotgit); - if (fspathcmp(olddotgit.buf, realdotgit.buf)) + 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 (fspathcmp(realolddotgit.buf, realdotgit.buf)) repair = _("gitdir incorrect"); } if (repair) { fn(0, gitdir.buf, repair, cb_data); - write_file(gitdir.buf, "%s", realdotgit.buf); + write_file(gitdir.buf, "%s", relative_path(realdotgit.buf, backlink.buf, &tmp)); } done: - free(backlink); + 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) { struct stat st; - char *path; + struct strbuf dotgit = STRBUF_INIT; + struct strbuf gitdir = STRBUF_INIT; + struct strbuf repo = STRBUF_INIT; + struct strbuf file = STRBUF_INIT; + char *path = NULL; + int rc = 0; int fd; size_t len; ssize_t read_result; *wtpath = NULL; - if (!is_directory(git_path("worktrees/%s", id))) { + strbuf_realpath(&repo, git_common_path("worktrees/%s", id), 1); + strbuf_addf(&gitdir, "%s/gitdir", repo.buf); + if (!is_directory(repo.buf)) { strbuf_addstr(reason, _("not a valid directory")); - return 1; + rc = 1; + goto done; } - if (file_exists(git_path("worktrees/%s/locked", id))) - return 0; - if (stat(git_path("worktrees/%s/gitdir", id), &st)) { + strbuf_addf(&file, "%s/locked", repo.buf); + if (file_exists(file.buf)) { + goto done; + } + if (stat(gitdir.buf, &st)) { strbuf_addstr(reason, _("gitdir file does not exist")); - return 1; + rc = 1; + goto done; } - fd = open(git_path("worktrees/%s/gitdir", id), O_RDONLY); + fd = open(gitdir.buf, O_RDONLY); if (fd < 0) { strbuf_addf(reason, _("unable to read gitdir file (%s)"), strerror(errno)); - return 1; + rc = 1; + goto done; } len = xsize_t(st.st_size); path = xmallocz(len); read_result = read_in_full(fd, path, len); + close(fd); if (read_result < 0) { strbuf_addf(reason, _("unable to read gitdir file (%s)"), strerror(errno)); - close(fd); - free(path); - return 1; - } - close(fd); - - if (read_result != len) { + rc = 1; + goto done; + } else if (read_result != len) { strbuf_addf(reason, _("short read (expected %"PRIuMAX" bytes, read %"PRIuMAX")"), (uintmax_t)len, (uintmax_t)read_result); - free(path); - return 1; + rc = 1; + goto done; } while (len && (path[len - 1] == '\n' || path[len - 1] == '\r')) len--; if (!len) { strbuf_addstr(reason, _("invalid gitdir file")); - free(path); - return 1; + rc = 1; + goto done; } path[len] = '\0'; - if (!file_exists(path)) { - if (stat(git_path("worktrees/%s/index", id), &st) || - st.st_mtime <= expire) { + if (is_absolute_path(path)) { + strbuf_addstr(&dotgit, path); + } else { + strbuf_addf(&dotgit, "%s/%s", repo.buf, path); + strbuf_realpath_forgiving(&dotgit, dotgit.buf, 0); + } + if (!file_exists(dotgit.buf)) { + strbuf_reset(&file); + strbuf_addf(&file, "%s/index", repo.buf); + if (stat(file.buf, &st) || st.st_mtime <= expire) { strbuf_addstr(reason, _("gitdir file points to non-existent location")); - free(path); - return 1; - } else { - *wtpath = path; - return 0; + rc = 1; + goto done; } } - *wtpath = path; - return 0; + *wtpath = strbuf_detach(&dotgit, NULL); +done: + free(path); + strbuf_release(&dotgit); + strbuf_release(&gitdir); + strbuf_release(&repo); + strbuf_release(&file); + return rc; } static int move_config_setting(const char *key, const char *value, const char *from_file, const char *to_file) { - if (git_config_set_in_file_gently(to_file, key, value)) + if (git_config_set_in_file_gently(to_file, key, NULL, value)) return error(_("unable to set %s in '%s'"), key, to_file); - if (git_config_set_in_file_gently(from_file, key, NULL)) + if (git_config_set_in_file_gently(from_file, key, NULL, NULL)) return error(_("unable to unset %s in '%s'"), key, from_file); return 0; } |