Merge branch 'es/worktree-add'
Update to the "linked checkout" in 2.5.0-rc1. Instead of "checkout --to" that does not do what "checkout" normally does, move the functionality to "git worktree add". * es/worktree-add: (24 commits) Revert "checkout: retire --ignore-other-worktrees in favor of --force" checkout: retire --ignore-other-worktrees in favor of --force worktree: add: auto-vivify new branch when <branch> is omitted worktree: add: make -b/-B default to HEAD when <branch> is omitted worktree: extract basename computation to new function checkout: require worktree unconditionally checkout: retire --to option tests: worktree: retrofit "checkout --to" tests for "worktree add" worktree: add -b/-B options worktree: add --detach option worktree: add --force option worktree: introduce "add" command checkout: drop 'checkout_opts' dependency from prepare_linked_checkout checkout: make --to unconditionally verbose checkout: prepare_linked_checkout: drop now-unused 'new' argument checkout: relocate --to's "no branch specified" check checkout: fix bug with --to and relative HEAD Documentation/git-worktree: add EXAMPLES section Documentation/git-worktree: add high-level 'lock' overview Documentation/git-worktree: split technical info from general description ...
This commit is contained in:
commit
799767cc98
@ -229,13 +229,6 @@ 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.
|
||||
|
||||
--to=<path>::
|
||||
Check out a branch in a separate working directory at
|
||||
`<path>`. A new working directory is linked to the current
|
||||
repository, sharing everything except working directory
|
||||
specific files such as HEAD, index... See "MULTIPLE WORKING
|
||||
TREES" section for more information.
|
||||
|
||||
--ignore-other-worktrees::
|
||||
`git checkout` refuses when the wanted ref is already checked
|
||||
out by another worktree. This option makes it check the ref
|
||||
@ -405,71 +398,6 @@ $ git reflog -2 HEAD # or
|
||||
$ git log -g -2 HEAD
|
||||
------------
|
||||
|
||||
MULTIPLE WORKING TREES
|
||||
----------------------
|
||||
|
||||
A git repository can support multiple working trees, allowing you to check
|
||||
out more than one branch at a time. With `git checkout --to` a new working
|
||||
tree is associated with the repository. This new working tree is called a
|
||||
"linked working tree" as opposed to the "main working tree" prepared by "git
|
||||
init" or "git clone". A repository has one main working tree (if it's not a
|
||||
bare repository) and zero or more linked working trees.
|
||||
|
||||
Each linked working tree has a private sub-directory in the repository's
|
||||
$GIT_DIR/worktrees directory. The private sub-directory's name is usually
|
||||
the base name of the linked working tree's path, possibly appended with a
|
||||
number to make it unique. For example, when `$GIT_DIR=/path/main/.git` the
|
||||
command `git checkout --to /path/other/test-next next` creates the linked
|
||||
working tree in `/path/other/test-next` and also creates a
|
||||
`$GIT_DIR/worktrees/test-next` directory (or `$GIT_DIR/worktrees/test-next1`
|
||||
if `test-next` is already taken).
|
||||
|
||||
Within a linked working tree, $GIT_DIR is set to point to this private
|
||||
directory (e.g. `/path/main/.git/worktrees/test-next` in the example) and
|
||||
$GIT_COMMON_DIR is set to point back to the main working tree's $GIT_DIR
|
||||
(e.g. `/path/main/.git`). These settings are made in a `.git` file located at
|
||||
the top directory of the linked working tree.
|
||||
|
||||
Path resolution via `git rev-parse --git-path` uses either
|
||||
$GIT_DIR or $GIT_COMMON_DIR depending on the path. For example, in the
|
||||
linked working tree `git rev-parse --git-path HEAD` returns
|
||||
`/path/main/.git/worktrees/test-next/HEAD` (not
|
||||
`/path/other/test-next/.git/HEAD` or `/path/main/.git/HEAD`) while `git
|
||||
rev-parse --git-path refs/heads/master` uses
|
||||
$GIT_COMMON_DIR and returns `/path/main/.git/refs/heads/master`,
|
||||
since refs are shared across all working trees.
|
||||
|
||||
See linkgit:gitrepository-layout[5] for more information. The rule of
|
||||
thumb is do not make any assumption about whether a path belongs to
|
||||
$GIT_DIR or $GIT_COMMON_DIR when you need to directly access something
|
||||
inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.
|
||||
|
||||
When you are done with a linked working tree you can simply delete it.
|
||||
The working tree's entry in the repository's $GIT_DIR/worktrees
|
||||
directory will eventually be removed automatically (see
|
||||
`gc.pruneworktreesexpire` in linkgit::git-config[1]), or you can run
|
||||
`git prune --worktrees` in the main or any linked working tree to
|
||||
clean up any stale entries in $GIT_DIR/worktrees.
|
||||
|
||||
If you move a linked working directory to another file system, or
|
||||
within a file system that does not support hard links, you need to run
|
||||
at least one git command inside the linked working directory
|
||||
(e.g. `git status`) in order to update its entry in $GIT_DIR/worktrees
|
||||
so that it does not get automatically removed.
|
||||
|
||||
To prevent a $GIT_DIR/worktrees entry from from being pruned (which
|
||||
can be useful in some situations, such as when the
|
||||
entry's working tree is stored on a portable device), add a file named
|
||||
'locked' to the entry's directory. The file contains the reason in
|
||||
plain text. For example, if a linked working tree's `.git` file points
|
||||
to `/path/main/.git/worktrees/test-next` then a file named
|
||||
`/path/main/.git/worktrees/test-next/locked` will prevent the
|
||||
`test-next` entry from being pruned. See
|
||||
linkgit:gitrepository-layout[5] for details.
|
||||
|
||||
Multiple checkout support for submodules is incomplete. It is NOT
|
||||
recommended to make multiple checkouts of a superproject.
|
||||
|
||||
EXAMPLES
|
||||
--------
|
||||
|
||||
|
@ -9,16 +9,52 @@ git-worktree - Manage multiple worktrees
|
||||
SYNOPSIS
|
||||
--------
|
||||
[verse]
|
||||
'git worktree add' [-f] [--detach] [-b <new-branch>] <path> [<branch>]
|
||||
'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`.
|
||||
Manage multiple worktrees attached to the same repository.
|
||||
|
||||
A git repository can support multiple working trees, allowing you to check
|
||||
out more than one branch at a time. With `git checkout --to` a new working
|
||||
tree is associated with the repository. This new working tree is called a
|
||||
"linked working tree" as opposed to the "main working tree" prepared by "git
|
||||
init" or "git clone". A repository has one main working tree (if it's not a
|
||||
bare repository) and zero or more linked working trees.
|
||||
|
||||
When you are done with a linked working tree you can simply delete it.
|
||||
The working tree's administrative files in the repository (see
|
||||
"DETAILS" below) will eventually be removed automatically (see
|
||||
`gc.pruneworktreesexpire` in linkgit::git-config[1]), or you can run
|
||||
`git worktree prune` in the main or any linked working tree to
|
||||
clean up any stale administrative files.
|
||||
|
||||
If you move a linked working directory to another file system, or
|
||||
within a file system that does not support hard links, you need to run
|
||||
at least one git command inside the linked working directory
|
||||
(e.g. `git status`) in order to update its administrative files in the
|
||||
repository so that they do not get automatically pruned.
|
||||
|
||||
If a linked working tree is stored on a portable device or network share
|
||||
which is not always mounted, you can prevent its administrative files from
|
||||
being pruned by creating a file named 'lock' alongside the other
|
||||
administrative files, optionally containing a plain text reason that
|
||||
pruning should be suppressed. See section "DETAILS" for more information.
|
||||
|
||||
COMMANDS
|
||||
--------
|
||||
add <path> [<branch>]::
|
||||
|
||||
Create `<path>` and checkout `<branch>` into it. The new working directory
|
||||
is linked to the current repository, sharing everything except working
|
||||
directory specific files such as HEAD, index, etc.
|
||||
+
|
||||
If `<branch>` is omitted and neither `-b` nor `-B` is used, then, as a
|
||||
convenience, a new branch based at HEAD is created automatically, as if
|
||||
`-b $(basename <path>)` was specified.
|
||||
|
||||
prune::
|
||||
|
||||
Prune working tree information in $GIT_DIR/worktrees.
|
||||
@ -26,22 +62,113 @@ Prune working tree information in $GIT_DIR/worktrees.
|
||||
OPTIONS
|
||||
-------
|
||||
|
||||
-f::
|
||||
--force::
|
||||
By default, `add` refuses to create a new worktree when `<branch>`
|
||||
is already checked out by another worktree. This option overrides
|
||||
that safeguard.
|
||||
|
||||
-b <new-branch>::
|
||||
-B <new-branch>::
|
||||
With `add`, create a new branch named `<new-branch>` starting at
|
||||
`<branch>`, and check out `<new-branch>` into the new worktree.
|
||||
If `<branch>` is omitted, it defaults to HEAD.
|
||||
By default, `-b` refuses to create a new branch if it already
|
||||
exists. `-B` overrides this safeguard, resetting `<new-branch>` to
|
||||
`<branch>`.
|
||||
|
||||
--detach::
|
||||
With `add`, detach HEAD in the new worktree. See "DETACHED HEAD" in
|
||||
linkgit:git-checkout[1].
|
||||
|
||||
-n::
|
||||
--dry-run::
|
||||
Do not remove anything; just report what it would
|
||||
With `prune`, do not remove anything; just report what it would
|
||||
remove.
|
||||
|
||||
-v::
|
||||
--verbose::
|
||||
Report all removals.
|
||||
With `prune`, report all removals.
|
||||
|
||||
--expire <time>::
|
||||
Only expire unused worktrees older than <time>.
|
||||
With `prune`, only expire unused worktrees older than <time>.
|
||||
|
||||
SEE ALSO
|
||||
DETAILS
|
||||
-------
|
||||
Each linked working tree has a private sub-directory in the repository's
|
||||
$GIT_DIR/worktrees directory. The private sub-directory's name is usually
|
||||
the base name of the linked working tree's path, possibly appended with a
|
||||
number to make it unique. For example, when `$GIT_DIR=/path/main/.git` the
|
||||
command `git checkout --to /path/other/test-next next` creates the linked
|
||||
working tree in `/path/other/test-next` and also creates a
|
||||
`$GIT_DIR/worktrees/test-next` directory (or `$GIT_DIR/worktrees/test-next1`
|
||||
if `test-next` is already taken).
|
||||
|
||||
Within a linked working tree, $GIT_DIR is set to point to this private
|
||||
directory (e.g. `/path/main/.git/worktrees/test-next` in the example) and
|
||||
$GIT_COMMON_DIR is set to point back to the main working tree's $GIT_DIR
|
||||
(e.g. `/path/main/.git`). These settings are made in a `.git` file located at
|
||||
the top directory of the linked working tree.
|
||||
|
||||
Path resolution via `git rev-parse --git-path` uses either
|
||||
$GIT_DIR or $GIT_COMMON_DIR depending on the path. For example, in the
|
||||
linked working tree `git rev-parse --git-path HEAD` returns
|
||||
`/path/main/.git/worktrees/test-next/HEAD` (not
|
||||
`/path/other/test-next/.git/HEAD` or `/path/main/.git/HEAD`) while `git
|
||||
rev-parse --git-path refs/heads/master` uses
|
||||
$GIT_COMMON_DIR and returns `/path/main/.git/refs/heads/master`,
|
||||
since refs are shared across all working trees.
|
||||
|
||||
See linkgit:gitrepository-layout[5] for more information. The rule of
|
||||
thumb is do not make any assumption about whether a path belongs to
|
||||
$GIT_DIR or $GIT_COMMON_DIR when you need to directly access something
|
||||
inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.
|
||||
|
||||
To prevent a $GIT_DIR/worktrees entry from from being pruned (which
|
||||
can be useful in some situations, such as when the
|
||||
entry's working tree is stored on a portable device), add a file named
|
||||
'locked' to the entry's directory. The file contains the reason in
|
||||
plain text. For example, if a linked working tree's `.git` file points
|
||||
to `/path/main/.git/worktrees/test-next` then a file named
|
||||
`/path/main/.git/worktrees/test-next/locked` will prevent the
|
||||
`test-next` entry from being pruned. See
|
||||
linkgit:gitrepository-layout[5] for details.
|
||||
|
||||
EXAMPLES
|
||||
--------
|
||||
You are in the middle of a refactoring session and your boss comes in and
|
||||
demands that you fix something immediately. You might typically use
|
||||
linkgit:git-stash[1] to store your changes away temporarily, however, your
|
||||
worktree is in such a state of disarray (with new, moved, and removed files,
|
||||
and other bits and pieces strewn around) that you don't want to risk
|
||||
disturbing any of it. Instead, you create a temporary linked worktree to
|
||||
make the emergency fix, remove it when done, and then resume your earlier
|
||||
refactoring session.
|
||||
|
||||
linkgit:git-checkout[1]
|
||||
------------
|
||||
$ git worktree add -b emergency-fix ../temp master
|
||||
$ pushd ../temp
|
||||
# ... hack hack hack ...
|
||||
$ git commit -a -m 'emergency fix for boss'
|
||||
$ popd
|
||||
$ rm -rf ../temp
|
||||
$ git worktree prune
|
||||
------------
|
||||
|
||||
BUGS
|
||||
----
|
||||
Multiple checkout support for submodules is incomplete. It is NOT
|
||||
recommended to make multiple checkouts of a superproject.
|
||||
|
||||
git-worktree could provide more automation for tasks currently
|
||||
performed manually, such as:
|
||||
|
||||
- `remove` to remove a linked worktree and its administrative files (and
|
||||
warn if the worktree is dirty)
|
||||
- `mv` to move or rename a worktree and update its administrative files
|
||||
- `list` to list linked worktrees
|
||||
- `lock` to prevent automatic pruning of administrative files (for instance,
|
||||
for a worktree on a portable device)
|
||||
|
||||
GIT
|
||||
---
|
||||
|
@ -19,8 +19,6 @@
|
||||
#include "ll-merge.h"
|
||||
#include "resolve-undo.h"
|
||||
#include "submodule.h"
|
||||
#include "argv-array.h"
|
||||
#include "sigchain.h"
|
||||
|
||||
static const char * const checkout_usage[] = {
|
||||
N_("git checkout [<options>] <branch>"),
|
||||
@ -51,8 +49,6 @@ struct checkout_opts {
|
||||
struct pathspec pathspec;
|
||||
struct tree *source_tree;
|
||||
|
||||
const char *new_worktree;
|
||||
const char **saved_argv;
|
||||
int new_worktree_mode;
|
||||
};
|
||||
|
||||
@ -273,9 +269,6 @@ static int checkout_paths(const struct checkout_opts *opts,
|
||||
die(_("Cannot update paths and switch to branch '%s' at the same time."),
|
||||
opts->new_branch);
|
||||
|
||||
if (opts->new_worktree)
|
||||
die(_("'%s' cannot be used with updating paths"), "--to");
|
||||
|
||||
if (opts->patch_mode)
|
||||
return run_add_interactive(revision, "--patch=checkout",
|
||||
&opts->pathspec);
|
||||
@ -850,138 +843,6 @@ static int switch_branches(const struct checkout_opts *opts,
|
||||
return ret || writeout_error;
|
||||
}
|
||||
|
||||
static char *junk_work_tree;
|
||||
static char *junk_git_dir;
|
||||
static int is_junk;
|
||||
static pid_t junk_pid;
|
||||
|
||||
static void remove_junk(void)
|
||||
{
|
||||
struct strbuf sb = STRBUF_INIT;
|
||||
if (!is_junk || getpid() != junk_pid)
|
||||
return;
|
||||
if (junk_git_dir) {
|
||||
strbuf_addstr(&sb, junk_git_dir);
|
||||
remove_dir_recursively(&sb, 0);
|
||||
strbuf_reset(&sb);
|
||||
}
|
||||
if (junk_work_tree) {
|
||||
strbuf_addstr(&sb, junk_work_tree);
|
||||
remove_dir_recursively(&sb, 0);
|
||||
}
|
||||
strbuf_release(&sb);
|
||||
}
|
||||
|
||||
static void remove_junk_on_signal(int signo)
|
||||
{
|
||||
remove_junk();
|
||||
sigchain_pop(signo);
|
||||
raise(signo);
|
||||
}
|
||||
|
||||
static int prepare_linked_checkout(const struct checkout_opts *opts,
|
||||
struct branch_info *new)
|
||||
{
|
||||
struct strbuf sb_git = STRBUF_INIT, sb_repo = STRBUF_INIT;
|
||||
struct strbuf sb = STRBUF_INIT;
|
||||
const char *path = opts->new_worktree, *name;
|
||||
struct stat st;
|
||||
struct child_process cp;
|
||||
int counter = 0, len, ret;
|
||||
|
||||
if (!new->commit)
|
||||
die(_("no branch specified"));
|
||||
if (file_exists(path) && !is_empty_dir(path))
|
||||
die(_("'%s' already exists"), path);
|
||||
|
||||
len = strlen(path);
|
||||
while (len && is_dir_sep(path[len - 1]))
|
||||
len--;
|
||||
|
||||
for (name = path + len - 1; name > path; name--)
|
||||
if (is_dir_sep(*name)) {
|
||||
name++;
|
||||
break;
|
||||
}
|
||||
strbuf_addstr(&sb_repo,
|
||||
git_path("worktrees/%.*s", (int)(path + len - name), name));
|
||||
len = sb_repo.len;
|
||||
if (safe_create_leading_directories_const(sb_repo.buf))
|
||||
die_errno(_("could not create leading directories of '%s'"),
|
||||
sb_repo.buf);
|
||||
while (!stat(sb_repo.buf, &st)) {
|
||||
counter++;
|
||||
strbuf_setlen(&sb_repo, len);
|
||||
strbuf_addf(&sb_repo, "%d", counter);
|
||||
}
|
||||
name = strrchr(sb_repo.buf, '/') + 1;
|
||||
|
||||
junk_pid = getpid();
|
||||
atexit(remove_junk);
|
||||
sigchain_push_common(remove_junk_on_signal);
|
||||
|
||||
if (mkdir(sb_repo.buf, 0777))
|
||||
die_errno(_("could not create directory of '%s'"), sb_repo.buf);
|
||||
junk_git_dir = xstrdup(sb_repo.buf);
|
||||
is_junk = 1;
|
||||
|
||||
/*
|
||||
* lock the incomplete repo so prune won't delete it, unlock
|
||||
* after the preparation is over.
|
||||
*/
|
||||
strbuf_addf(&sb, "%s/locked", sb_repo.buf);
|
||||
write_file(sb.buf, 1, "initializing\n");
|
||||
|
||||
strbuf_addf(&sb_git, "%s/.git", path);
|
||||
if (safe_create_leading_directories_const(sb_git.buf))
|
||||
die_errno(_("could not create leading directories of '%s'"),
|
||||
sb_git.buf);
|
||||
junk_work_tree = xstrdup(path);
|
||||
|
||||
strbuf_reset(&sb);
|
||||
strbuf_addf(&sb, "%s/gitdir", sb_repo.buf);
|
||||
write_file(sb.buf, 1, "%s\n", real_path(sb_git.buf));
|
||||
write_file(sb_git.buf, 1, "gitdir: %s/worktrees/%s\n",
|
||||
real_path(get_git_common_dir()), name);
|
||||
/*
|
||||
* This is to keep resolve_ref() happy. We need a valid HEAD
|
||||
* or is_git_directory() will reject the directory. Any valid
|
||||
* value would do because this value will be ignored and
|
||||
* replaced at the next (real) checkout.
|
||||
*/
|
||||
strbuf_reset(&sb);
|
||||
strbuf_addf(&sb, "%s/HEAD", sb_repo.buf);
|
||||
write_file(sb.buf, 1, "%s\n", sha1_to_hex(new->commit->object.sha1));
|
||||
strbuf_reset(&sb);
|
||||
strbuf_addf(&sb, "%s/commondir", sb_repo.buf);
|
||||
write_file(sb.buf, 1, "../..\n");
|
||||
|
||||
if (!opts->quiet)
|
||||
fprintf_ln(stderr, _("Enter %s (identifier %s)"), path, name);
|
||||
|
||||
setenv("GIT_CHECKOUT_NEW_WORKTREE", "1", 1);
|
||||
setenv(GIT_DIR_ENVIRONMENT, sb_git.buf, 1);
|
||||
setenv(GIT_WORK_TREE_ENVIRONMENT, path, 1);
|
||||
memset(&cp, 0, sizeof(cp));
|
||||
cp.git_cmd = 1;
|
||||
cp.argv = opts->saved_argv;
|
||||
ret = run_command(&cp);
|
||||
if (!ret) {
|
||||
is_junk = 0;
|
||||
free(junk_work_tree);
|
||||
free(junk_git_dir);
|
||||
junk_work_tree = NULL;
|
||||
junk_git_dir = NULL;
|
||||
}
|
||||
strbuf_reset(&sb);
|
||||
strbuf_addf(&sb, "%s/locked", sb_repo.buf);
|
||||
unlink_or_warn(sb.buf);
|
||||
strbuf_release(&sb);
|
||||
strbuf_release(&sb_repo);
|
||||
strbuf_release(&sb_git);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int git_checkout_config(const char *var, const char *value, void *cb)
|
||||
{
|
||||
if (!strcmp(var, "diff.ignoresubmodules")) {
|
||||
@ -1320,9 +1181,6 @@ static int checkout_branch(struct checkout_opts *opts,
|
||||
free(head_ref);
|
||||
}
|
||||
|
||||
if (opts->new_worktree)
|
||||
return prepare_linked_checkout(opts, new);
|
||||
|
||||
if (!new->commit && opts->new_branch) {
|
||||
unsigned char rev[20];
|
||||
int flag;
|
||||
@ -1365,8 +1223,6 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
|
||||
N_("do not limit pathspecs to sparse entries only")),
|
||||
OPT_HIDDEN_BOOL(0, "guess", &dwim_new_local_branch,
|
||||
N_("second guess 'git checkout <no-such-branch>'")),
|
||||
OPT_FILENAME(0, "to", &opts.new_worktree,
|
||||
N_("check a branch out in a separate working directory")),
|
||||
OPT_BOOL(0, "ignore-other-worktrees", &opts.ignore_other_worktrees,
|
||||
N_("do not check if another worktree is holding the given ref")),
|
||||
OPT_END(),
|
||||
@ -1377,9 +1233,6 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
|
||||
opts.overwrite_ignore = 1;
|
||||
opts.prefix = prefix;
|
||||
|
||||
opts.saved_argv = xmalloc(sizeof(const char *) * (argc + 2));
|
||||
memcpy(opts.saved_argv, argv, sizeof(const char *) * (argc + 1));
|
||||
|
||||
gitmodules_config();
|
||||
git_config(git_checkout_config, &opts);
|
||||
|
||||
@ -1388,13 +1241,7 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
|
||||
argc = parse_options(argc, argv, prefix, options, checkout_usage,
|
||||
PARSE_OPT_KEEP_DASHDASH);
|
||||
|
||||
/* recursive execution from checkout_new_worktree() */
|
||||
opts.new_worktree_mode = getenv("GIT_CHECKOUT_NEW_WORKTREE") != NULL;
|
||||
if (opts.new_worktree_mode)
|
||||
opts.new_worktree = NULL;
|
||||
|
||||
if (!opts.new_worktree)
|
||||
setup_work_tree();
|
||||
|
||||
if (conflict_style) {
|
||||
opts.merge = 1; /* implied */
|
||||
|
@ -2,8 +2,13 @@
|
||||
#include "builtin.h"
|
||||
#include "dir.h"
|
||||
#include "parse-options.h"
|
||||
#include "argv-array.h"
|
||||
#include "run-command.h"
|
||||
#include "sigchain.h"
|
||||
#include "refs.h"
|
||||
|
||||
static const char * const worktree_usage[] = {
|
||||
N_("git worktree add [<options>] <path> <branch>"),
|
||||
N_("git worktree prune [<options>]"),
|
||||
NULL
|
||||
};
|
||||
@ -119,6 +124,198 @@ static int prune(int ac, const char **av, const char *prefix)
|
||||
return 0;
|
||||
}
|
||||
|
||||
static char *junk_work_tree;
|
||||
static char *junk_git_dir;
|
||||
static int is_junk;
|
||||
static pid_t junk_pid;
|
||||
|
||||
static void remove_junk(void)
|
||||
{
|
||||
struct strbuf sb = STRBUF_INIT;
|
||||
if (!is_junk || getpid() != junk_pid)
|
||||
return;
|
||||
if (junk_git_dir) {
|
||||
strbuf_addstr(&sb, junk_git_dir);
|
||||
remove_dir_recursively(&sb, 0);
|
||||
strbuf_reset(&sb);
|
||||
}
|
||||
if (junk_work_tree) {
|
||||
strbuf_addstr(&sb, junk_work_tree);
|
||||
remove_dir_recursively(&sb, 0);
|
||||
}
|
||||
strbuf_release(&sb);
|
||||
}
|
||||
|
||||
static void remove_junk_on_signal(int signo)
|
||||
{
|
||||
remove_junk();
|
||||
sigchain_pop(signo);
|
||||
raise(signo);
|
||||
}
|
||||
|
||||
static const char *worktree_basename(const char *path, int *olen)
|
||||
{
|
||||
const char *name;
|
||||
int len;
|
||||
|
||||
len = strlen(path);
|
||||
while (len && is_dir_sep(path[len - 1]))
|
||||
len--;
|
||||
|
||||
for (name = path + len - 1; name > path; name--)
|
||||
if (is_dir_sep(*name)) {
|
||||
name++;
|
||||
break;
|
||||
}
|
||||
|
||||
*olen = len;
|
||||
return name;
|
||||
}
|
||||
|
||||
static int add_worktree(const char *path, const char **child_argv)
|
||||
{
|
||||
struct strbuf sb_git = STRBUF_INIT, sb_repo = STRBUF_INIT;
|
||||
struct strbuf sb = STRBUF_INIT;
|
||||
const char *name;
|
||||
struct stat st;
|
||||
struct child_process cp;
|
||||
int counter = 0, len, ret;
|
||||
unsigned char rev[20];
|
||||
|
||||
if (file_exists(path) && !is_empty_dir(path))
|
||||
die(_("'%s' already exists"), path);
|
||||
|
||||
name = worktree_basename(path, &len);
|
||||
strbuf_addstr(&sb_repo,
|
||||
git_path("worktrees/%.*s", (int)(path + len - name), name));
|
||||
len = sb_repo.len;
|
||||
if (safe_create_leading_directories_const(sb_repo.buf))
|
||||
die_errno(_("could not create leading directories of '%s'"),
|
||||
sb_repo.buf);
|
||||
while (!stat(sb_repo.buf, &st)) {
|
||||
counter++;
|
||||
strbuf_setlen(&sb_repo, len);
|
||||
strbuf_addf(&sb_repo, "%d", counter);
|
||||
}
|
||||
name = strrchr(sb_repo.buf, '/') + 1;
|
||||
|
||||
junk_pid = getpid();
|
||||
atexit(remove_junk);
|
||||
sigchain_push_common(remove_junk_on_signal);
|
||||
|
||||
if (mkdir(sb_repo.buf, 0777))
|
||||
die_errno(_("could not create directory of '%s'"), sb_repo.buf);
|
||||
junk_git_dir = xstrdup(sb_repo.buf);
|
||||
is_junk = 1;
|
||||
|
||||
/*
|
||||
* lock the incomplete repo so prune won't delete it, unlock
|
||||
* after the preparation is over.
|
||||
*/
|
||||
strbuf_addf(&sb, "%s/locked", sb_repo.buf);
|
||||
write_file(sb.buf, 1, "initializing\n");
|
||||
|
||||
strbuf_addf(&sb_git, "%s/.git", path);
|
||||
if (safe_create_leading_directories_const(sb_git.buf))
|
||||
die_errno(_("could not create leading directories of '%s'"),
|
||||
sb_git.buf);
|
||||
junk_work_tree = xstrdup(path);
|
||||
|
||||
strbuf_reset(&sb);
|
||||
strbuf_addf(&sb, "%s/gitdir", sb_repo.buf);
|
||||
write_file(sb.buf, 1, "%s\n", real_path(sb_git.buf));
|
||||
write_file(sb_git.buf, 1, "gitdir: %s/worktrees/%s\n",
|
||||
real_path(get_git_common_dir()), name);
|
||||
/*
|
||||
* This is to keep resolve_ref() happy. We need a valid HEAD
|
||||
* or is_git_directory() will reject the directory. Moreover, HEAD
|
||||
* in the new worktree must resolve to the same value as HEAD in
|
||||
* the current tree since the command invoked to populate the new
|
||||
* worktree will be handed the branch/ref specified by the user.
|
||||
* For instance, if the user asks for the new worktree to be based
|
||||
* at HEAD~5, then the resolved HEAD~5 in the new worktree must
|
||||
* match the resolved HEAD~5 in the current tree in order to match
|
||||
* the user's expectation.
|
||||
*/
|
||||
if (!resolve_ref_unsafe("HEAD", 0, rev, NULL))
|
||||
die(_("unable to resolve HEAD"));
|
||||
strbuf_reset(&sb);
|
||||
strbuf_addf(&sb, "%s/HEAD", sb_repo.buf);
|
||||
write_file(sb.buf, 1, "%s\n", sha1_to_hex(rev));
|
||||
strbuf_reset(&sb);
|
||||
strbuf_addf(&sb, "%s/commondir", sb_repo.buf);
|
||||
write_file(sb.buf, 1, "../..\n");
|
||||
|
||||
fprintf_ln(stderr, _("Enter %s (identifier %s)"), path, name);
|
||||
|
||||
setenv("GIT_CHECKOUT_NEW_WORKTREE", "1", 1);
|
||||
setenv(GIT_DIR_ENVIRONMENT, sb_git.buf, 1);
|
||||
setenv(GIT_WORK_TREE_ENVIRONMENT, path, 1);
|
||||
memset(&cp, 0, sizeof(cp));
|
||||
cp.git_cmd = 1;
|
||||
cp.argv = child_argv;
|
||||
ret = run_command(&cp);
|
||||
if (!ret) {
|
||||
is_junk = 0;
|
||||
free(junk_work_tree);
|
||||
free(junk_git_dir);
|
||||
junk_work_tree = NULL;
|
||||
junk_git_dir = NULL;
|
||||
}
|
||||
strbuf_reset(&sb);
|
||||
strbuf_addf(&sb, "%s/locked", sb_repo.buf);
|
||||
unlink_or_warn(sb.buf);
|
||||
strbuf_release(&sb);
|
||||
strbuf_release(&sb_repo);
|
||||
strbuf_release(&sb_git);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int add(int ac, const char **av, const char *prefix)
|
||||
{
|
||||
int force = 0, detach = 0;
|
||||
const char *new_branch = NULL, *new_branch_force = NULL;
|
||||
const char *path, *branch;
|
||||
struct argv_array cmd = ARGV_ARRAY_INIT;
|
||||
struct option options[] = {
|
||||
OPT__FORCE(&force, N_("checkout <branch> even if already checked out in other worktree")),
|
||||
OPT_STRING('b', NULL, &new_branch, N_("branch"),
|
||||
N_("create a new branch")),
|
||||
OPT_STRING('B', NULL, &new_branch_force, N_("branch"),
|
||||
N_("create or reset a branch")),
|
||||
OPT_BOOL(0, "detach", &detach, N_("detach HEAD at named commit")),
|
||||
OPT_END()
|
||||
};
|
||||
|
||||
ac = parse_options(ac, av, prefix, options, worktree_usage, 0);
|
||||
if (new_branch && new_branch_force)
|
||||
die(_("-b and -B are mutually exclusive"));
|
||||
if (ac < 1 || ac > 2)
|
||||
usage_with_options(worktree_usage, options);
|
||||
|
||||
path = prefix ? prefix_filename(prefix, strlen(prefix), av[0]) : av[0];
|
||||
branch = ac < 2 ? "HEAD" : av[1];
|
||||
|
||||
if (ac < 2 && !new_branch && !new_branch_force) {
|
||||
int n;
|
||||
const char *s = worktree_basename(path, &n);
|
||||
new_branch = xstrndup(s, n);
|
||||
}
|
||||
|
||||
argv_array_push(&cmd, "checkout");
|
||||
if (force)
|
||||
argv_array_push(&cmd, "--ignore-other-worktrees");
|
||||
if (new_branch)
|
||||
argv_array_pushl(&cmd, "-b", new_branch, NULL);
|
||||
if (new_branch_force)
|
||||
argv_array_pushl(&cmd, "-B", new_branch_force, NULL);
|
||||
if (detach)
|
||||
argv_array_push(&cmd, "--detach");
|
||||
argv_array_push(&cmd, branch);
|
||||
|
||||
return add_worktree(path, cmd.argv);
|
||||
}
|
||||
|
||||
int cmd_worktree(int ac, const char **av, const char *prefix)
|
||||
{
|
||||
struct option options[] = {
|
||||
@ -127,6 +324,8 @@ int cmd_worktree(int ac, const char **av, const char *prefix)
|
||||
|
||||
if (ac < 2)
|
||||
usage_with_options(worktree_usage, options);
|
||||
if (!strcmp(av[1], "add"))
|
||||
return add(ac - 1, av + 1, prefix);
|
||||
if (!strcmp(av[1], "prune"))
|
||||
return prune(ac - 1, av + 1, prefix);
|
||||
usage_with_options(worktree_usage, options);
|
||||
|
2
git.c
2
git.c
@ -382,7 +382,7 @@ static struct cmd_struct commands[] = {
|
||||
{ "check-ignore", cmd_check_ignore, RUN_SETUP | NEED_WORK_TREE },
|
||||
{ "check-mailmap", cmd_check_mailmap, RUN_SETUP },
|
||||
{ "check-ref-format", cmd_check_ref_format },
|
||||
{ "checkout", cmd_checkout, RUN_SETUP },
|
||||
{ "checkout", cmd_checkout, RUN_SETUP | NEED_WORK_TREE },
|
||||
{ "checkout-index", cmd_checkout_index,
|
||||
RUN_SETUP | NEED_WORK_TREE},
|
||||
{ "cherry", cmd_cherry, RUN_SETUP },
|
||||
|
@ -1,6 +1,6 @@
|
||||
#!/bin/sh
|
||||
|
||||
test_description='test git checkout --to'
|
||||
test_description='test git worktree add'
|
||||
|
||||
. ./test-lib.sh
|
||||
|
||||
@ -8,22 +8,18 @@ test_expect_success 'setup' '
|
||||
test_commit init
|
||||
'
|
||||
|
||||
test_expect_success 'checkout --to not updating paths' '
|
||||
test_must_fail git checkout --to -- init.t
|
||||
'
|
||||
|
||||
test_expect_success 'checkout --to an existing worktree' '
|
||||
test_expect_success '"add" an existing worktree' '
|
||||
mkdir -p existing/subtree &&
|
||||
test_must_fail git checkout --detach --to existing master
|
||||
test_must_fail git worktree add --detach existing master
|
||||
'
|
||||
|
||||
test_expect_success 'checkout --to an existing empty worktree' '
|
||||
test_expect_success '"add" an existing empty worktree' '
|
||||
mkdir existing_empty &&
|
||||
git checkout --detach --to existing_empty master
|
||||
git worktree add --detach existing_empty master
|
||||
'
|
||||
|
||||
test_expect_success 'checkout --to refuses to checkout locked branch' '
|
||||
test_must_fail git checkout --to zere master &&
|
||||
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
|
||||
'
|
||||
@ -36,9 +32,9 @@ test_expect_success 'checking out paths not complaining about linked checkouts'
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'checkout --to a new worktree' '
|
||||
test_expect_success '"add" worktree' '
|
||||
git rev-parse HEAD >expect &&
|
||||
git checkout --detach --to here master &&
|
||||
git worktree add --detach here master &&
|
||||
(
|
||||
cd here &&
|
||||
test_cmp ../init.t init.t &&
|
||||
@ -49,27 +45,27 @@ test_expect_success 'checkout --to a new worktree' '
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'checkout --to a new worktree from a subdir' '
|
||||
test_expect_success '"add" worktree from a subdir' '
|
||||
(
|
||||
mkdir sub &&
|
||||
cd sub &&
|
||||
git checkout --detach --to here master &&
|
||||
git worktree add --detach here master &&
|
||||
cd here &&
|
||||
test_cmp ../../init.t init.t
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'checkout --to from a linked checkout' '
|
||||
test_expect_success '"add" from a linked checkout' '
|
||||
(
|
||||
cd here &&
|
||||
git checkout --detach --to nested-here master &&
|
||||
git worktree add --detach nested-here master &&
|
||||
cd nested-here &&
|
||||
git fsck
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'checkout --to a new worktree creating new branch' '
|
||||
git checkout --to there -b newmaster master &&
|
||||
test_expect_success '"add" worktree creating new branch' '
|
||||
git worktree add -b newmaster there master &&
|
||||
(
|
||||
cd there &&
|
||||
test_cmp ../init.t init.t &&
|
||||
@ -90,7 +86,7 @@ test_expect_success 'die the same branch is already checked out' '
|
||||
test_expect_success 'not die the same branch is already checked out' '
|
||||
(
|
||||
cd here &&
|
||||
git checkout --ignore-other-worktrees --to anothernewmaster newmaster
|
||||
git worktree add --force anothernewmaster newmaster
|
||||
)
|
||||
'
|
||||
|
||||
@ -101,15 +97,15 @@ test_expect_success 'not die on re-checking out current branch' '
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'checkout --to from a bare repo' '
|
||||
test_expect_success '"add" from a bare repo' '
|
||||
(
|
||||
git clone --bare . bare &&
|
||||
cd bare &&
|
||||
git checkout --to ../there2 -b bare-master master
|
||||
git worktree add -b bare-master ../there2 master
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'checkout from a bare repo without --to' '
|
||||
test_expect_success 'checkout from a bare repo without "add"' '
|
||||
(
|
||||
cd bare &&
|
||||
test_must_fail git checkout master
|
||||
@ -129,9 +125,38 @@ test_expect_success 'checkout with grafts' '
|
||||
EOF
|
||||
git log --format=%s -2 >actual &&
|
||||
test_cmp expected actual &&
|
||||
git checkout --detach --to grafted master &&
|
||||
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 <branch> omitted' '
|
||||
git worktree add -b burble flornk &&
|
||||
test_cmp_rev HEAD burble
|
||||
'
|
||||
|
||||
test_expect_success '"add" with <branch> omitted' '
|
||||
git worktree add wiffle/bat &&
|
||||
test_cmp_rev HEAD bat
|
||||
'
|
||||
|
||||
test_expect_success '"add" auto-vivify does not clobber existing branch' '
|
||||
test_commit c1 &&
|
||||
test_commit c2 &&
|
||||
git branch precious HEAD~1 &&
|
||||
test_must_fail git worktree add precious &&
|
||||
test_cmp_rev HEAD~1 precious &&
|
||||
test_path_is_missing precious
|
||||
'
|
||||
|
||||
test_done
|
@ -88,7 +88,7 @@ test_expect_success 'not prune recent checkouts' '
|
||||
|
||||
test_expect_success 'not prune proper checkouts' '
|
||||
test_when_finished rm -r .git/worktrees &&
|
||||
git checkout "--to=$PWD/nop" --detach master &&
|
||||
git worktree add --detach "$PWD/nop" master &&
|
||||
git worktree prune &&
|
||||
test -d .git/worktrees/nop
|
||||
'
|
||||
|
@ -33,7 +33,7 @@ rev1_hash_sub=$(git --git-dir=origin/sub/.git show --pretty=format:%h -q "HEAD~1
|
||||
test_expect_success 'checkout main' \
|
||||
'mkdir default_checkout &&
|
||||
(cd clone/main &&
|
||||
git checkout --to "$base_path/default_checkout/main" "$rev1_hash_main")'
|
||||
git worktree add "$base_path/default_checkout/main" "$rev1_hash_main")'
|
||||
|
||||
test_expect_failure 'can see submodule diffs just after checkout' \
|
||||
'(cd default_checkout/main && git diff --submodule master"^!" | grep "file1 updated")'
|
||||
@ -41,7 +41,7 @@ test_expect_failure 'can see submodule diffs just after checkout' \
|
||||
test_expect_success 'checkout main and initialize independed clones' \
|
||||
'mkdir fully_cloned_submodule &&
|
||||
(cd clone/main &&
|
||||
git checkout --to "$base_path/fully_cloned_submodule/main" "$rev1_hash_main") &&
|
||||
git worktree add "$base_path/fully_cloned_submodule/main" "$rev1_hash_main") &&
|
||||
(cd fully_cloned_submodule/main && git submodule update)'
|
||||
|
||||
test_expect_success 'can see submodule diffs after independed cloning' \
|
||||
|
Loading…
Reference in New Issue
Block a user