Merge branch 'nd/multiple-work-trees'

"git checkout [<tree-ish>] <paths>" spent unnecessary cycles
checking if the current branch was checked out elsewhere, when we
know we are not switching the branches ourselves.

* nd/multiple-work-trees:
  worktree: new place for "git prune --worktrees"
  checkout: don't check worktrees when not necessary
This commit is contained in:
Junio C Hamano 2015-07-13 14:02:02 -07:00
Родитель 721f5bb896 df0b6cfbda
Коммит 7783eb2e59
13 изменённых файлов: 217 добавлений и 126 удалений

1
.gitignore поставляемый
Просмотреть файл

@ -171,6 +171,7 @@
/git-verify-tag
/git-web--browse
/git-whatchanged
/git-worktree
/git-write-tree
/git-core-*/?*
/gitweb/GITWEB-BUILD-OPTIONS

Просмотреть файл

@ -48,9 +48,6 @@ OPTIONS
--expire <time>::
Only expire loose objects older than <time>.
--worktrees::
Prune dead working tree information in $GIT_DIR/worktrees.
<head>...::
In addition to objects
reachable from any of our references, keep objects

Просмотреть файл

@ -0,0 +1,48 @@
git-worktree(1)
===============
NAME
----
git-worktree - Manage multiple worktrees
SYNOPSIS
--------
[verse]
'git worktree prune' [-n] [-v] [--expire <expire>]
DESCRIPTION
-----------
Manage multiple worktrees attached to the same repository. These are
created by the command `git checkout --to`.
COMMANDS
--------
prune::
Prune working tree information in $GIT_DIR/worktrees.
OPTIONS
-------
-n::
--dry-run::
Do not remove anything; just report what it would
remove.
-v::
--verbose::
Report all removals.
--expire <time>::
Only expire unused worktrees older than <time>.
SEE ALSO
--------
linkgit:git-checkout[1]
GIT
---
Part of the linkgit:git[1] suite

Просмотреть файл

@ -910,6 +910,7 @@ BUILTIN_OBJS += builtin/var.o
BUILTIN_OBJS += builtin/verify-commit.o
BUILTIN_OBJS += builtin/verify-pack.o
BUILTIN_OBJS += builtin/verify-tag.o
BUILTIN_OBJS += builtin/worktree.o
BUILTIN_OBJS += builtin/write-tree.o
GITLIBS = $(LIB_FILE) $(XDIFF_LIB)

Просмотреть файл

@ -133,6 +133,7 @@ extern int cmd_verify_commit(int argc, const char **argv, const char *prefix);
extern int cmd_verify_tag(int argc, const char **argv, const char *prefix);
extern int cmd_version(int argc, const char **argv, const char *prefix);
extern int cmd_whatchanged(int argc, const char **argv, const char *prefix);
extern int cmd_worktree(int argc, const char **argv, const char *prefix);
extern int cmd_write_tree(int argc, const char **argv, const char *prefix);
extern int cmd_verify_pack(int argc, const char **argv, const char *prefix);
extern int cmd_show_ref(int argc, const char **argv, const char *prefix);

Просмотреть файл

@ -1110,7 +1110,6 @@ static int parse_branchname_arg(int argc, const char **argv,
{
struct tree **source_tree = &opts->source_tree;
const char **new_branch = &opts->new_branch;
int force_detach = opts->force_detach;
int argcount = 0;
unsigned char branch_rev[20];
const char *arg;
@ -1231,17 +1230,6 @@ static int parse_branchname_arg(int argc, const char **argv,
else
new->path = NULL; /* not an existing branch */
if (new->path && !force_detach && !*new_branch) {
unsigned char sha1[20];
int flag;
char *head_ref = resolve_refdup("HEAD", 0, sha1, &flag);
if (head_ref &&
(!(flag & REF_ISSYMREF) || strcmp(head_ref, new->path)) &&
!opts->ignore_other_worktrees)
check_linked_checkouts(new);
free(head_ref);
}
new->commit = lookup_commit_reference_gently(rev, 1);
if (!new->commit) {
/* not a commit */
@ -1321,6 +1309,17 @@ static int checkout_branch(struct checkout_opts *opts,
die(_("Cannot switch branch to a non-commit '%s'"),
new->name);
if (new->path && !opts->force_detach && !opts->new_branch) {
unsigned char sha1[20];
int flag;
char *head_ref = resolve_refdup("HEAD", 0, sha1, &flag);
if (head_ref &&
(!(flag & REF_ISSYMREF) || strcmp(head_ref, new->path)) &&
!opts->ignore_other_worktrees)
check_linked_checkouts(new);
free(head_ref);
}
if (opts->new_worktree)
return prepare_linked_checkout(opts, new);

Просмотреть файл

@ -293,7 +293,7 @@ int cmd_gc(int argc, const char **argv, const char *prefix)
argv_array_pushl(&reflog, "reflog", "expire", "--all", NULL);
argv_array_pushl(&repack, "repack", "-d", "-l", NULL);
argv_array_pushl(&prune, "prune", "--expire", NULL);
argv_array_pushl(&prune_worktrees, "prune", "--worktrees", "--expire", NULL);
argv_array_pushl(&prune_worktrees, "worktree", "prune", "--expire", NULL);
argv_array_pushl(&rerere, "rerere", "gc", NULL);
gc_config();

Просмотреть файл

@ -6,7 +6,6 @@
#include "reachable.h"
#include "parse-options.h"
#include "progress.h"
#include "dir.h"
static const char * const prune_usage[] = {
N_("git prune [-n] [-v] [--expire <time>] [--] [<head>...]"),
@ -76,95 +75,6 @@ static int prune_subdir(int nr, const char *path, void *data)
return 0;
}
static int prune_worktree(const char *id, struct strbuf *reason)
{
struct stat st;
char *path;
int fd, len;
if (!is_directory(git_path("worktrees/%s", id))) {
strbuf_addf(reason, _("Removing worktrees/%s: not a valid directory"), id);
return 1;
}
if (file_exists(git_path("worktrees/%s/locked", id)))
return 0;
if (stat(git_path("worktrees/%s/gitdir", id), &st)) {
strbuf_addf(reason, _("Removing worktrees/%s: gitdir file does not exist"), id);
return 1;
}
fd = open(git_path("worktrees/%s/gitdir", id), O_RDONLY);
if (fd < 0) {
strbuf_addf(reason, _("Removing worktrees/%s: unable to read gitdir file (%s)"),
id, strerror(errno));
return 1;
}
len = st.st_size;
path = xmalloc(len + 1);
read_in_full(fd, path, len);
close(fd);
while (len && (path[len - 1] == '\n' || path[len - 1] == '\r'))
len--;
if (!len) {
strbuf_addf(reason, _("Removing worktrees/%s: invalid gitdir file"), id);
free(path);
return 1;
}
path[len] = '\0';
if (!file_exists(path)) {
struct stat st_link;
free(path);
/*
* the repo is moved manually and has not been
* accessed since?
*/
if (!stat(git_path("worktrees/%s/link", id), &st_link) &&
st_link.st_nlink > 1)
return 0;
if (st.st_mtime <= expire) {
strbuf_addf(reason, _("Removing worktrees/%s: gitdir file points to non-existent location"), id);
return 1;
} else {
return 0;
}
}
free(path);
return 0;
}
static void prune_worktrees(void)
{
struct strbuf reason = STRBUF_INIT;
struct strbuf path = STRBUF_INIT;
DIR *dir = opendir(git_path("worktrees"));
struct dirent *d;
int ret;
if (!dir)
return;
while ((d = readdir(dir)) != NULL) {
if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, ".."))
continue;
strbuf_reset(&reason);
if (!prune_worktree(d->d_name, &reason))
continue;
if (show_only || verbose)
printf("%s\n", reason.buf);
if (show_only)
continue;
strbuf_reset(&path);
strbuf_addstr(&path, git_path("worktrees/%s", d->d_name));
ret = remove_dir_recursively(&path, 0);
if (ret < 0 && errno == ENOTDIR)
ret = unlink(path.buf);
if (ret)
error(_("failed to remove: %s"), strerror(errno));
}
closedir(dir);
if (!show_only)
rmdir(git_path("worktrees"));
strbuf_release(&reason);
strbuf_release(&path);
}
/*
* Write errors (particularly out of space) can result in
* failed temporary packs (and more rarely indexes and other
@ -191,12 +101,10 @@ int cmd_prune(int argc, const char **argv, const char *prefix)
{
struct rev_info revs;
struct progress *progress = NULL;
int do_prune_worktrees = 0;
const struct option options[] = {
OPT__DRY_RUN(&show_only, N_("do not remove, show only")),
OPT__VERBOSE(&verbose, N_("report pruned objects")),
OPT_BOOL(0, "progress", &show_progress, N_("show progress")),
OPT_BOOL(0, "worktrees", &do_prune_worktrees, N_("prune .git/worktrees")),
OPT_EXPIRY_DATE(0, "expire", &expire,
N_("expire objects older than <time>")),
OPT_END()
@ -211,13 +119,6 @@ int cmd_prune(int argc, const char **argv, const char *prefix)
argc = parse_options(argc, argv, prefix, options, prune_usage, 0);
if (do_prune_worktrees) {
if (argc)
die(_("--worktrees does not take extra arguments"));
prune_worktrees();
return 0;
}
while (argc--) {
unsigned char sha1[20];
const char *name = *argv++;

133
builtin/worktree.c Normal file
Просмотреть файл

@ -0,0 +1,133 @@
#include "cache.h"
#include "builtin.h"
#include "dir.h"
#include "parse-options.h"
static const char * const worktree_usage[] = {
N_("git worktree prune [<options>]"),
NULL
};
static int show_only;
static int verbose;
static unsigned long expire;
static int prune_worktree(const char *id, struct strbuf *reason)
{
struct stat st;
char *path;
int fd, len;
if (!is_directory(git_path("worktrees/%s", id))) {
strbuf_addf(reason, _("Removing worktrees/%s: not a valid directory"), id);
return 1;
}
if (file_exists(git_path("worktrees/%s/locked", id)))
return 0;
if (stat(git_path("worktrees/%s/gitdir", id), &st)) {
strbuf_addf(reason, _("Removing worktrees/%s: gitdir file does not exist"), id);
return 1;
}
fd = open(git_path("worktrees/%s/gitdir", id), O_RDONLY);
if (fd < 0) {
strbuf_addf(reason, _("Removing worktrees/%s: unable to read gitdir file (%s)"),
id, strerror(errno));
return 1;
}
len = st.st_size;
path = xmalloc(len + 1);
read_in_full(fd, path, len);
close(fd);
while (len && (path[len - 1] == '\n' || path[len - 1] == '\r'))
len--;
if (!len) {
strbuf_addf(reason, _("Removing worktrees/%s: invalid gitdir file"), id);
free(path);
return 1;
}
path[len] = '\0';
if (!file_exists(path)) {
struct stat st_link;
free(path);
/*
* the repo is moved manually and has not been
* accessed since?
*/
if (!stat(git_path("worktrees/%s/link", id), &st_link) &&
st_link.st_nlink > 1)
return 0;
if (st.st_mtime <= expire) {
strbuf_addf(reason, _("Removing worktrees/%s: gitdir file points to non-existent location"), id);
return 1;
} else {
return 0;
}
}
free(path);
return 0;
}
static void prune_worktrees(void)
{
struct strbuf reason = STRBUF_INIT;
struct strbuf path = STRBUF_INIT;
DIR *dir = opendir(git_path("worktrees"));
struct dirent *d;
int ret;
if (!dir)
return;
while ((d = readdir(dir)) != NULL) {
if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, ".."))
continue;
strbuf_reset(&reason);
if (!prune_worktree(d->d_name, &reason))
continue;
if (show_only || verbose)
printf("%s\n", reason.buf);
if (show_only)
continue;
strbuf_reset(&path);
strbuf_addstr(&path, git_path("worktrees/%s", d->d_name));
ret = remove_dir_recursively(&path, 0);
if (ret < 0 && errno == ENOTDIR)
ret = unlink(path.buf);
if (ret)
error(_("failed to remove: %s"), strerror(errno));
}
closedir(dir);
if (!show_only)
rmdir(git_path("worktrees"));
strbuf_release(&reason);
strbuf_release(&path);
}
static int prune(int ac, const char **av, const char *prefix)
{
struct option options[] = {
OPT__DRY_RUN(&show_only, N_("do not remove, show only")),
OPT__VERBOSE(&verbose, N_("report pruned objects")),
OPT_EXPIRY_DATE(0, "expire", &expire,
N_("expire objects older than <time>")),
OPT_END()
};
expire = ULONG_MAX;
ac = parse_options(ac, av, prefix, options, worktree_usage, 0);
if (ac)
usage_with_options(worktree_usage, options);
prune_worktrees();
return 0;
}
int cmd_worktree(int ac, const char **av, const char *prefix)
{
struct option options[] = {
OPT_END()
};
if (ac < 2)
usage_with_options(worktree_usage, options);
if (!strcmp(av[1], "prune"))
return prune(ac - 1, av + 1, prefix);
usage_with_options(worktree_usage, options);
}

Просмотреть файл

@ -148,4 +148,5 @@ git-verify-pack plumbinginterrogators
git-verify-tag ancillaryinterrogators
gitweb ancillaryinterrogators
git-whatchanged ancillaryinterrogators
git-worktree mainporcelain
git-write-tree plumbingmanipulators

1
git.c
Просмотреть файл

@ -483,6 +483,7 @@ static struct cmd_struct commands[] = {
{ "verify-tag", cmd_verify_tag, RUN_SETUP },
{ "version", cmd_version },
{ "whatchanged", cmd_whatchanged, RUN_SETUP },
{ "worktree", cmd_worktree, RUN_SETUP },
{ "write-tree", cmd_write_tree, RUN_SETUP },
};

Просмотреть файл

@ -28,6 +28,14 @@ test_expect_success 'checkout --to refuses to checkout locked branch' '
! 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 'checkout --to a new worktree' '
git rev-parse HEAD >expect &&
git checkout --detach --to here master &&

Просмотреть файл

@ -8,15 +8,15 @@ test_expect_success initialize '
git commit --allow-empty -m init
'
test_expect_success 'prune --worktrees on normal repo' '
git prune --worktrees &&
test_must_fail git prune --worktrees abc
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 prune --worktrees --verbose >actual &&
git worktree prune --verbose >actual &&
cat >expect <<EOF &&
Removing worktrees/abc: not a valid directory
EOF
@ -31,7 +31,7 @@ test_expect_success 'prune directories without gitdir' '
cat >expect <<EOF &&
Removing worktrees/def: gitdir file does not exist
EOF
git prune --worktrees --verbose >actual &&
git worktree prune --verbose >actual &&
test_i18ncmp expect actual &&
! test -d .git/worktrees/def &&
! test -d .git/worktrees
@ -42,7 +42,7 @@ test_expect_success SANITY 'prune directories with unreadable gitdir' '
: >.git/worktrees/def/def &&
: >.git/worktrees/def/gitdir &&
chmod u-r .git/worktrees/def/gitdir &&
git prune --worktrees --verbose >actual &&
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
@ -52,7 +52,7 @@ test_expect_success 'prune directories with invalid gitdir' '
mkdir -p .git/worktrees/def/abc &&
: >.git/worktrees/def/def &&
: >.git/worktrees/def/gitdir &&
git prune --worktrees --verbose >actual &&
git worktree prune --verbose >actual &&
test_i18ngrep "Removing worktrees/def: invalid gitdir file" actual &&
! test -d .git/worktrees/def &&
! test -d .git/worktrees
@ -62,7 +62,7 @@ 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 prune --worktrees --verbose >actual &&
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
@ -72,7 +72,7 @@ test_expect_success 'not prune locked checkout' '
test_when_finished rm -r .git/worktrees &&
mkdir -p .git/worktrees/ghi &&
: >.git/worktrees/ghi/locked &&
git prune --worktrees &&
git worktree prune &&
test -d .git/worktrees/ghi
'
@ -82,14 +82,14 @@ test_expect_success 'not prune recent checkouts' '
mkdir -p .git/worktrees/jlm &&
echo "$(pwd)"/zz >.git/worktrees/jlm/gitdir &&
rmdir zz &&
git prune --worktrees --verbose --expire=2.days.ago &&
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 checkout "--to=$PWD/nop" --detach master &&
git prune --worktrees &&
git worktree prune &&
test -d .git/worktrees/nop
'