 2c608e0f7c
			
		
	
	2c608e0f7c
	
	
	
		
			
			"git worktree prune" protected worktrees that are marked as "locked" by creating a file in a known location. "git worktree" command learned a dedicated command pair to create and remove such a file, so that the users do not have to do this with editor. * nd/worktree-lock: worktree.c: find_worktree() search by path suffix worktree: add "unlock" command worktree: add "lock" command worktree.c: add is_worktree_locked() worktree.c: add is_main_worktree() worktree.c: add find_worktree()
		
			
				
	
	
		
			381 lines
		
	
	
		
			9.1 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			381 lines
		
	
	
		
			9.1 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
| #include "cache.h"
 | |
| #include "refs.h"
 | |
| #include "strbuf.h"
 | |
| #include "worktree.h"
 | |
| #include "dir.h"
 | |
| #include "wt-status.h"
 | |
| 
 | |
| 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]);
 | |
| 	}
 | |
| 	free (worktrees);
 | |
| }
 | |
| 
 | |
| /*
 | |
|  * read 'path_to_ref' into 'ref'.  Also if is_detached is not NULL,
 | |
|  * set is_detached to 1 (0) if the ref is detached (is not detached).
 | |
|  *
 | |
|  * $GIT_COMMON_DIR/$symref (e.g. HEAD) is practically outside $GIT_DIR so
 | |
|  * for linked worktrees, `resolve_ref_unsafe()` won't work (it uses
 | |
|  * git_path). Parse the ref ourselves.
 | |
|  *
 | |
|  * return -1 if the ref is not a proper ref, 0 otherwise (success)
 | |
|  */
 | |
| static int parse_ref(char *path_to_ref, struct strbuf *ref, int *is_detached)
 | |
| {
 | |
| 	if (is_detached)
 | |
| 		*is_detached = 0;
 | |
| 	if (!strbuf_readlink(ref, path_to_ref, 0)) {
 | |
| 		/* HEAD is symbolic link */
 | |
| 		if (!starts_with(ref->buf, "refs/") ||
 | |
| 				check_refname_format(ref->buf, 0))
 | |
| 			return -1;
 | |
| 	} else if (strbuf_read_file(ref, path_to_ref, 0) >= 0) {
 | |
| 		/* textual symref or detached */
 | |
| 		if (!starts_with(ref->buf, "ref:")) {
 | |
| 			if (is_detached)
 | |
| 				*is_detached = 1;
 | |
| 		} else {
 | |
| 			strbuf_remove(ref, 0, strlen("ref:"));
 | |
| 			strbuf_trim(ref);
 | |
| 			if (check_refname_format(ref->buf, 0))
 | |
| 				return -1;
 | |
| 		}
 | |
| 	} else
 | |
| 		return -1;
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Add the head_sha1 and head_ref (if not detached) to the given worktree
 | |
|  */
 | |
| static void add_head_info(struct strbuf *head_ref, struct worktree *worktree)
 | |
| {
 | |
| 	if (head_ref->len) {
 | |
| 		if (worktree->is_detached) {
 | |
| 			get_sha1_hex(head_ref->buf, worktree->head_sha1);
 | |
| 		} else {
 | |
| 			resolve_ref_unsafe(head_ref->buf, 0, worktree->head_sha1, NULL);
 | |
| 			worktree->head_ref = strbuf_detach(head_ref, NULL);
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * get the main worktree
 | |
|  */
 | |
| static struct worktree *get_main_worktree(void)
 | |
| {
 | |
| 	struct worktree *worktree = NULL;
 | |
| 	struct strbuf path = STRBUF_INIT;
 | |
| 	struct strbuf worktree_path = STRBUF_INIT;
 | |
| 	struct strbuf head_ref = STRBUF_INIT;
 | |
| 	int is_bare = 0;
 | |
| 	int is_detached = 0;
 | |
| 
 | |
| 	strbuf_add_absolute_path(&worktree_path, get_git_common_dir());
 | |
| 	is_bare = !strbuf_strip_suffix(&worktree_path, "/.git");
 | |
| 	if (is_bare)
 | |
| 		strbuf_strip_suffix(&worktree_path, "/.");
 | |
| 
 | |
| 	strbuf_addf(&path, "%s/HEAD", get_git_common_dir());
 | |
| 
 | |
| 	if (parse_ref(path.buf, &head_ref, &is_detached) < 0)
 | |
| 		goto done;
 | |
| 
 | |
| 	worktree = xmalloc(sizeof(struct worktree));
 | |
| 	worktree->path = strbuf_detach(&worktree_path, NULL);
 | |
| 	worktree->id = NULL;
 | |
| 	worktree->is_bare = is_bare;
 | |
| 	worktree->head_ref = NULL;
 | |
| 	worktree->is_detached = is_detached;
 | |
| 	worktree->is_current = 0;
 | |
| 	add_head_info(&head_ref, worktree);
 | |
| 	worktree->lock_reason = NULL;
 | |
| 	worktree->lock_reason_valid = 0;
 | |
| 
 | |
| done:
 | |
| 	strbuf_release(&path);
 | |
| 	strbuf_release(&worktree_path);
 | |
| 	strbuf_release(&head_ref);
 | |
| 	return worktree;
 | |
| }
 | |
| 
 | |
| static struct worktree *get_linked_worktree(const char *id)
 | |
| {
 | |
| 	struct worktree *worktree = NULL;
 | |
| 	struct strbuf path = STRBUF_INIT;
 | |
| 	struct strbuf worktree_path = STRBUF_INIT;
 | |
| 	struct strbuf head_ref = STRBUF_INIT;
 | |
| 	int is_detached = 0;
 | |
| 
 | |
| 	if (!id)
 | |
| 		die("Missing linked worktree name");
 | |
| 
 | |
| 	strbuf_git_common_path(&path, "worktrees/%s/gitdir", id);
 | |
| 	if (strbuf_read_file(&worktree_path, path.buf, 0) <= 0)
 | |
| 		/* invalid gitdir file */
 | |
| 		goto done;
 | |
| 
 | |
| 	strbuf_rtrim(&worktree_path);
 | |
| 	if (!strbuf_strip_suffix(&worktree_path, "/.git")) {
 | |
| 		strbuf_reset(&worktree_path);
 | |
| 		strbuf_add_absolute_path(&worktree_path, ".");
 | |
| 		strbuf_strip_suffix(&worktree_path, "/.");
 | |
| 	}
 | |
| 
 | |
| 	strbuf_reset(&path);
 | |
| 	strbuf_addf(&path, "%s/worktrees/%s/HEAD", get_git_common_dir(), id);
 | |
| 
 | |
| 	if (parse_ref(path.buf, &head_ref, &is_detached) < 0)
 | |
| 		goto done;
 | |
| 
 | |
| 	worktree = xmalloc(sizeof(struct worktree));
 | |
| 	worktree->path = strbuf_detach(&worktree_path, NULL);
 | |
| 	worktree->id = xstrdup(id);
 | |
| 	worktree->is_bare = 0;
 | |
| 	worktree->head_ref = NULL;
 | |
| 	worktree->is_detached = is_detached;
 | |
| 	worktree->is_current = 0;
 | |
| 	add_head_info(&head_ref, worktree);
 | |
| 	worktree->lock_reason = NULL;
 | |
| 	worktree->lock_reason_valid = 0;
 | |
| 
 | |
| done:
 | |
| 	strbuf_release(&path);
 | |
| 	strbuf_release(&worktree_path);
 | |
| 	strbuf_release(&head_ref);
 | |
| 	return worktree;
 | |
| }
 | |
| 
 | |
| static void mark_current_worktree(struct worktree **worktrees)
 | |
| {
 | |
| 	char *git_dir = xstrdup(absolute_path(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)
 | |
| {
 | |
| 	struct worktree **list = NULL;
 | |
| 	struct strbuf path = STRBUF_INIT;
 | |
| 	DIR *dir;
 | |
| 	struct dirent *d;
 | |
| 	int counter = 0, alloc = 2;
 | |
| 
 | |
| 	list = xmalloc(alloc * sizeof(struct worktree *));
 | |
| 
 | |
| 	if ((list[counter] = get_main_worktree()))
 | |
| 		counter++;
 | |
| 
 | |
| 	strbuf_addf(&path, "%s/worktrees", get_git_common_dir());
 | |
| 	dir = opendir(path.buf);
 | |
| 	strbuf_release(&path);
 | |
| 	if (dir) {
 | |
| 		while ((d = readdir(dir)) != NULL) {
 | |
| 			struct worktree *linked = NULL;
 | |
| 			if (is_dot_or_dotdot(d->d_name))
 | |
| 				continue;
 | |
| 
 | |
| 			if ((linked = get_linked_worktree(d->d_name))) {
 | |
| 				ALLOC_GROW(list, counter + 1, alloc);
 | |
| 				list[counter++] = linked;
 | |
| 			}
 | |
| 		}
 | |
| 		closedir(dir);
 | |
| 	}
 | |
| 	ALLOC_GROW(list, counter + 1, alloc);
 | |
| 	list[counter] = NULL;
 | |
| 
 | |
| 	mark_current_worktree(list);
 | |
| 	return list;
 | |
| }
 | |
| 
 | |
| const char *get_worktree_git_dir(const struct worktree *wt)
 | |
| {
 | |
| 	if (!wt)
 | |
| 		return get_git_dir();
 | |
| 	else if (!wt->id)
 | |
| 		return get_git_common_dir();
 | |
| 	else
 | |
| 		return git_common_path("worktrees/%s", wt->id);
 | |
| }
 | |
| 
 | |
| static struct worktree *find_worktree_by_suffix(struct worktree **list,
 | |
| 						const char *suffix)
 | |
| {
 | |
| 	struct worktree *found = NULL;
 | |
| 	int nr_found = 0, suffixlen;
 | |
| 
 | |
| 	suffixlen = strlen(suffix);
 | |
| 	if (!suffixlen)
 | |
| 		return NULL;
 | |
| 
 | |
| 	for (; *list && nr_found < 2; list++) {
 | |
| 		const char	*path	 = (*list)->path;
 | |
| 		int		 pathlen = strlen(path);
 | |
| 		int		 start	 = pathlen - suffixlen;
 | |
| 
 | |
| 		/* suffix must start at directory boundary */
 | |
| 		if ((!start || (start > 0 && is_dir_sep(path[start - 1]))) &&
 | |
| 		    !fspathcmp(suffix, path + start)) {
 | |
| 			found = *list;
 | |
| 			nr_found++;
 | |
| 		}
 | |
| 	}
 | |
| 	return nr_found == 1 ? found : NULL;
 | |
| }
 | |
| 
 | |
| struct worktree *find_worktree(struct worktree **list,
 | |
| 			       const char *prefix,
 | |
| 			       const char *arg)
 | |
| {
 | |
| 	struct worktree *wt;
 | |
| 	char *path;
 | |
| 
 | |
| 	if ((wt = find_worktree_by_suffix(list, arg)))
 | |
| 		return wt;
 | |
| 
 | |
| 	arg = prefix_filename(prefix, strlen(prefix), arg);
 | |
| 	path = xstrdup(real_path(arg));
 | |
| 	for (; *list; list++)
 | |
| 		if (!fspathcmp(path, real_path((*list)->path)))
 | |
| 			break;
 | |
| 	free(path);
 | |
| 	return *list;
 | |
| }
 | |
| 
 | |
| int is_main_worktree(const struct worktree *wt)
 | |
| {
 | |
| 	return !wt->id;
 | |
| }
 | |
| 
 | |
| const char *is_worktree_locked(struct worktree *wt)
 | |
| {
 | |
| 	assert(!is_main_worktree(wt));
 | |
| 
 | |
| 	if (!wt->lock_reason_valid) {
 | |
| 		struct strbuf path = STRBUF_INIT;
 | |
| 
 | |
| 		strbuf_addstr(&path, worktree_git_path(wt, "locked"));
 | |
| 		if (file_exists(path.buf)) {
 | |
| 			struct strbuf lock_reason = STRBUF_INIT;
 | |
| 			if (strbuf_read_file(&lock_reason, path.buf, 0) < 0)
 | |
| 				die_errno(_("failed to read '%s'"), path.buf);
 | |
| 			strbuf_trim(&lock_reason);
 | |
| 			wt->lock_reason = strbuf_detach(&lock_reason, NULL);
 | |
| 		} else
 | |
| 			wt->lock_reason = NULL;
 | |
| 		wt->lock_reason_valid = 1;
 | |
| 		strbuf_release(&path);
 | |
| 	}
 | |
| 
 | |
| 	return wt->lock_reason;
 | |
| }
 | |
| 
 | |
| int is_worktree_being_rebased(const struct worktree *wt,
 | |
| 			      const char *target)
 | |
| {
 | |
| 	struct wt_status_state state;
 | |
| 	int found_rebase;
 | |
| 
 | |
| 	memset(&state, 0, sizeof(state));
 | |
| 	found_rebase = wt_status_check_rebase(wt, &state) &&
 | |
| 		((state.rebase_in_progress ||
 | |
| 		  state.rebase_interactive_in_progress) &&
 | |
| 		 state.branch &&
 | |
| 		 starts_with(target, "refs/heads/") &&
 | |
| 		 !strcmp(state.branch, target + strlen("refs/heads/")));
 | |
| 	free(state.branch);
 | |
| 	free(state.onto);
 | |
| 	return found_rebase;
 | |
| }
 | |
| 
 | |
| int is_worktree_being_bisected(const struct worktree *wt,
 | |
| 			       const char *target)
 | |
| {
 | |
| 	struct wt_status_state state;
 | |
| 	int found_rebase;
 | |
| 
 | |
| 	memset(&state, 0, sizeof(state));
 | |
| 	found_rebase = wt_status_check_bisect(wt, &state) &&
 | |
| 		state.branch &&
 | |
| 		starts_with(target, "refs/heads/") &&
 | |
| 		!strcmp(state.branch, target + strlen("refs/heads/"));
 | |
| 	free(state.branch);
 | |
| 	return found_rebase;
 | |
| }
 | |
| 
 | |
| /*
 | |
|  * note: this function should be able to detect shared symref even if
 | |
|  * HEAD is temporarily detached (e.g. in the middle of rebase or
 | |
|  * bisect). New commands that do similar things should update this
 | |
|  * function as well.
 | |
|  */
 | |
| const struct worktree *find_shared_symref(const char *symref,
 | |
| 					  const char *target)
 | |
| {
 | |
| 	const struct worktree *existing = NULL;
 | |
| 	struct strbuf path = STRBUF_INIT;
 | |
| 	struct strbuf sb = STRBUF_INIT;
 | |
| 	static struct worktree **worktrees;
 | |
| 	int i = 0;
 | |
| 
 | |
| 	if (worktrees)
 | |
| 		free_worktrees(worktrees);
 | |
| 	worktrees = get_worktrees();
 | |
| 
 | |
| 	for (i = 0; worktrees[i]; i++) {
 | |
| 		struct worktree *wt = worktrees[i];
 | |
| 
 | |
| 		if (wt->is_detached && !strcmp(symref, "HEAD")) {
 | |
| 			if (is_worktree_being_rebased(wt, target)) {
 | |
| 				existing = wt;
 | |
| 				break;
 | |
| 			}
 | |
| 			if (is_worktree_being_bisected(wt, target)) {
 | |
| 				existing = wt;
 | |
| 				break;
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		strbuf_reset(&path);
 | |
| 		strbuf_reset(&sb);
 | |
| 		strbuf_addf(&path, "%s/%s",
 | |
| 			    get_worktree_git_dir(wt),
 | |
| 			    symref);
 | |
| 
 | |
| 		if (parse_ref(path.buf, &sb, NULL)) {
 | |
| 			continue;
 | |
| 		}
 | |
| 
 | |
| 		if (!strcmp(sb.buf, target)) {
 | |
| 			existing = wt;
 | |
| 			break;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	strbuf_release(&path);
 | |
| 	strbuf_release(&sb);
 | |
| 
 | |
| 	return existing;
 | |
| }
 |