aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Documentation/config.txt9
-rw-r--r--Documentation/git-checkout.txt78
-rw-r--r--Documentation/git-prune.txt3
-rw-r--r--Documentation/git-rev-parse.txt10
-rw-r--r--Documentation/git.txt9
-rw-r--r--Documentation/gitrepository-layout.txt74
-rw-r--r--builtin/branch.c4
-rw-r--r--builtin/checkout.c269
-rw-r--r--builtin/clone.c9
-rw-r--r--builtin/commit.c2
-rw-r--r--builtin/count-objects.c4
-rw-r--r--builtin/fetch.c5
-rw-r--r--builtin/fsck.c4
-rw-r--r--builtin/gc.c34
-rw-r--r--builtin/init-db.c7
-rw-r--r--builtin/prune.c99
-rw-r--r--builtin/receive-pack.c2
-rw-r--r--builtin/remote.c2
-rw-r--r--builtin/repack.c8
-rw-r--r--builtin/rev-parse.c11
-rw-r--r--cache.h17
-rw-r--r--daemon.c11
-rw-r--r--environment.c34
-rw-r--r--fast-import.c7
-rwxr-xr-xgit-am.sh22
-rwxr-xr-xgit-pull.sh2
-rw-r--r--git-rebase--interactive.sh6
-rw-r--r--git-rebase--merge.sh6
-rwxr-xr-xgit-rebase.sh4
-rw-r--r--git-sh-setup.sh2
-rwxr-xr-xgit-stash.sh6
-rw-r--r--git.c2
-rw-r--r--notes-merge.c6
-rw-r--r--path.c234
-rw-r--r--refs.c59
-rw-r--r--refs.h2
-rw-r--r--run-command.c4
-rw-r--r--run-command.h2
-rw-r--r--setup.c124
-rw-r--r--sha1_file.c2
-rw-r--r--submodule.c9
-rwxr-xr-xt/t0060-path-utils.sh36
-rwxr-xr-xt/t1501-worktree.sh77
-rwxr-xr-xt/t1510-repo-setup.sh1
-rwxr-xr-xt/t2025-checkout-to.sh129
-rwxr-xr-xt/t2026-prune-linked-checkouts.sh96
-rwxr-xr-xt/t7410-submodule-checkout-to.sh50
-rwxr-xr-xtemplates/hooks--applypatch-msg.sample4
-rwxr-xr-xtemplates/hooks--pre-applypatch.sample4
-rw-r--r--trace.c1
-rw-r--r--transport.c8
-rw-r--r--wrapper.c31
52 files changed, 1391 insertions, 250 deletions
diff --git a/Documentation/config.txt b/Documentation/config.txt
index 2e5ceaf71..948b8b0e5 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -453,6 +453,8 @@ false), while all other repositories are assumed to be bare (bare
core.worktree::
Set the path to the root of the working tree.
+ If GIT_COMMON_DIR environment variable is set, core.worktree
+ is ignored and not used for determining the root of working tree.
This can be overridden by the GIT_WORK_TREE environment
variable and the '--work-tree' command-line option.
The value can be an absolute path or relative to the path to
@@ -1274,6 +1276,13 @@ gc.pruneExpire::
"now" may be used to disable this grace period and always prune
unreachable objects immediately.
+gc.pruneWorktreesExpire::
+ When 'git gc' is run, it will call
+ 'prune --worktrees --expire 3.months.ago'.
+ Override the grace period with this config variable. The value
+ "now" may be used to disable the grace period and prune
+ $GIT_DIR/worktrees immediately.
+
gc.reflogExpire::
gc.<pattern>.reflogExpire::
'git reflog expire' removes reflog entries older than
diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt
index d5041082e..d263a5652 100644
--- a/Documentation/git-checkout.txt
+++ b/Documentation/git-checkout.txt
@@ -225,6 +225,19 @@ 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
+ out anyway. In other words, the ref can be held by more than one
+ worktree.
+
<branch>::
Branch to checkout; if it refers to a branch (i.e., a name that,
when prepended with "refs/heads/", is a valid ref), then that
@@ -388,6 +401,71 @@ $ 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
--------
diff --git a/Documentation/git-prune.txt b/Documentation/git-prune.txt
index 7a493c80f..1cf3bed4a 100644
--- a/Documentation/git-prune.txt
+++ b/Documentation/git-prune.txt
@@ -48,6 +48,9 @@ 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
diff --git a/Documentation/git-rev-parse.txt b/Documentation/git-rev-parse.txt
index d6de42f74..97fc703f4 100644
--- a/Documentation/git-rev-parse.txt
+++ b/Documentation/git-rev-parse.txt
@@ -216,6 +216,9 @@ If `$GIT_DIR` is not defined and the current directory
is not detected to lie in a Git repository or work tree
print a message to stderr and exit with nonzero status.
+--git-common-dir::
+ Show `$GIT_COMMON_DIR` if defined, else `$GIT_DIR`.
+
--is-inside-git-dir::
When the current working directory is below the repository
directory print "true", otherwise "false".
@@ -233,6 +236,13 @@ print a message to stderr and exit with nonzero status.
repository. If <path> is a gitfile then the resolved path
to the real repository is printed.
+--git-path <path>::
+ Resolve "$GIT_DIR/<path>" and takes other path relocation
+ variables such as $GIT_OBJECT_DIRECTORY,
+ $GIT_INDEX_FILE... into account. For example, if
+ $GIT_OBJECT_DIRECTORY is set to /foo/bar then "git rev-parse
+ --git-path objects/abc" returns /foo/bar/abc.
+
--show-cdup::
When the command is invoked from a subdirectory, show the
path of the top-level directory relative to the current
diff --git a/Documentation/git.txt b/Documentation/git.txt
index c71e818cf..2789da4f8 100644
--- a/Documentation/git.txt
+++ b/Documentation/git.txt
@@ -833,6 +833,15 @@ Git so take care if using Cogito etc.
an explicit repository directory set via 'GIT_DIR' or on the
command line.
+'GIT_COMMON_DIR'::
+ If this variable is set to a path, non-worktree files that are
+ normally in $GIT_DIR will be taken from this path
+ instead. Worktree-specific files such as HEAD or index are
+ taken from $GIT_DIR. See linkgit:gitrepository-layout[5] and
+ the section 'MULTIPLE CHECKOUT MODE' in linkgit:checkout[1]
+ details. This variable has lower precedence than other path
+ variables such as GIT_INDEX_FILE, GIT_OBJECT_DIRECTORY...
+
Git Commits
~~~~~~~~~~~
'GIT_AUTHOR_NAME'::
diff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt
index 79653f313..7173b3883 100644
--- a/Documentation/gitrepository-layout.txt
+++ b/Documentation/gitrepository-layout.txt
@@ -46,6 +46,9 @@ of incomplete object store is not suitable to be published for
use with dumb transports but otherwise is OK as long as
`objects/info/alternates` points at the object stores it
borrows from.
++
+This directory is ignored if $GIT_COMMON_DIR is set and
+"$GIT_COMMON_DIR/objects" will be used instead.
objects/[0-9a-f][0-9a-f]::
A newly created object is stored in its own file.
@@ -92,7 +95,8 @@ refs::
References are stored in subdirectories of this
directory. The 'git prune' command knows to preserve
objects reachable from refs found in this directory and
- its subdirectories.
+ its subdirectories. This directory is ignored if $GIT_COMMON_DIR
+ is set and "$GIT_COMMON_DIR/refs" will be used instead.
refs/heads/`name`::
records tip-of-the-tree commit objects of branch `name`
@@ -114,7 +118,8 @@ refs/replace/`<obj-sha1>`::
packed-refs::
records the same information as refs/heads/, refs/tags/,
and friends record in a more efficient way. See
- linkgit:git-pack-refs[1].
+ linkgit:git-pack-refs[1]. This file is ignored if $GIT_COMMON_DIR
+ is set and "$GIT_COMMON_DIR/packed-refs" will be used instead.
HEAD::
A symref (see glossary) to the `refs/heads/` namespace
@@ -133,6 +138,11 @@ being a symref to point at the current branch. Such a state
is often called 'detached HEAD.' See linkgit:git-checkout[1]
for details.
+config::
+ Repository specific configuration file. This file is ignored
+ if $GIT_COMMON_DIR is set and "$GIT_COMMON_DIR/config" will be
+ used instead.
+
branches::
A slightly deprecated way to store shorthands to be used
to specify a URL to 'git fetch', 'git pull' and 'git push'.
@@ -140,7 +150,10 @@ branches::
'name' can be given to these commands in place of
'repository' argument. See the REMOTES section in
linkgit:git-fetch[1] for details. This mechanism is legacy
- and not likely to be found in modern repositories.
+ and not likely to be found in modern repositories. This
+ directory is ignored if $GIT_COMMON_DIR is set and
+ "$GIT_COMMON_DIR/branches" will be used instead.
+
hooks::
Hooks are customization scripts used by various Git
@@ -149,7 +162,9 @@ hooks::
default. To enable, the `.sample` suffix has to be
removed from the filename by renaming.
Read linkgit:githooks[5] for more details about
- each hook.
+ each hook. This directory is ignored if $GIT_COMMON_DIR is set
+ and "$GIT_COMMON_DIR/hooks" will be used instead.
+
index::
The current index file for the repository. It is
@@ -161,7 +176,8 @@ sharedindex.<SHA-1>::
info::
Additional information about the repository is recorded
- in this directory.
+ in this directory. This directory is ignored if $GIT_COMMON_DIR
+ is set and "$GIT_COMMON_DIR/index" will be used instead.
info/refs::
This file helps dumb transports discover what refs are
@@ -201,12 +217,15 @@ remotes::
when interacting with remote repositories via 'git fetch',
'git pull' and 'git push' commands. See the REMOTES section
in linkgit:git-fetch[1] for details. This mechanism is legacy
- and not likely to be found in modern repositories.
+ and not likely to be found in modern repositories. This
+ directory is ignored if $GIT_COMMON_DIR is set and
+ "$GIT_COMMON_DIR/remotes" will be used instead.
logs::
- Records of changes made to refs are stored in this
- directory. See linkgit:git-update-ref[1]
- for more information.
+ Records of changes made to refs are stored in this directory.
+ See linkgit:git-update-ref[1] for more information. This
+ directory is ignored if $GIT_COMMON_DIR is set and
+ "$GIT_COMMON_DIR/logs" will be used instead.
logs/refs/heads/`name`::
Records all changes made to the branch tip named `name`.
@@ -217,11 +236,46 @@ logs/refs/tags/`name`::
shallow::
This is similar to `info/grafts` but is internally used
and maintained by shallow clone mechanism. See `--depth`
- option to linkgit:git-clone[1] and linkgit:git-fetch[1].
+ option to linkgit:git-clone[1] and linkgit:git-fetch[1]. This
+ file is ignored if $GIT_COMMON_DIR is set and
+ "$GIT_COMMON_DIR/shallow" will be used instead.
+
+commondir::
+ If this file exists, $GIT_COMMON_DIR (see linkgit:git[1]) will
+ be set to the path specified in this file if it is not
+ explicitly set. If the specified path is relative, it is
+ relative to $GIT_DIR. The repository with commondir is
+ incomplete without the repository pointed by "commondir".
modules::
Contains the git-repositories of the submodules.
+worktrees::
+ Contains worktree specific information of linked
+ checkouts. Each subdirectory contains the worktree-related
+ part of a linked checkout. This directory is ignored if
+ $GIT_COMMON_DIR is set and "$GIT_COMMON_DIR/worktrees" will be
+ used instead.
+
+worktrees/<id>/gitdir::
+ A text file containing the absolute path back to the .git file
+ that points to here. This is used to check if the linked
+ repository has been manually removed and there is no need to
+ keep this directory any more. mtime of this file should be
+ updated every time the linked repository is accessed.
+
+worktrees/<id>/locked::
+ If this file exists, the linked repository may be on a
+ portable device and not available. It does not mean that the
+ linked repository is gone and `worktrees/<id>` could be
+ removed. The file's content contains a reason string on why
+ the repository is locked.
+
+worktrees/<id>/link::
+ If this file exists, it is a hard link to the linked .git
+ file. It is used to detect if the linked repository is
+ manually removed.
+
SEE ALSO
--------
linkgit:git-init[1],
diff --git a/builtin/branch.c b/builtin/branch.c
index 1d150378e..258fe2ff9 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -771,7 +771,6 @@ static const char edit_description[] = "BRANCH_DESCRIPTION";
static int edit_branch_description(const char *branch_name)
{
- FILE *fp;
int status;
struct strbuf buf = STRBUF_INIT;
struct strbuf name = STRBUF_INIT;
@@ -784,8 +783,7 @@ static int edit_branch_description(const char *branch_name)
" %s\n"
"Lines starting with '%c' will be stripped.\n",
branch_name, comment_line_char);
- fp = fopen(git_path(edit_description), "w");
- if ((fwrite(buf.buf, 1, buf.len, fp) < buf.len) || fclose(fp)) {
+ if (write_file(git_path(edit_description), 0, "%s", buf.buf)) {
strbuf_release(&buf);
return error(_("could not write branch description template: %s"),
strerror(errno));
diff --git a/builtin/checkout.c b/builtin/checkout.c
index 4aad49aaa..2f92328db 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -20,6 +20,7 @@
#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>"),
@@ -36,6 +37,7 @@ struct checkout_opts {
int writeout_stage;
int overwrite_ignore;
int ignore_skipworktree;
+ int ignore_other_worktrees;
const char *new_branch;
const char *new_branch_force;
@@ -48,6 +50,10 @@ struct checkout_opts {
const char *prefix;
struct pathspec pathspec;
struct tree *source_tree;
+
+ const char *new_worktree;
+ const char **saved_argv;
+ int new_worktree_mode;
};
static int post_checkout_hook(struct commit *old, struct commit *new,
@@ -267,6 +273,9 @@ 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);
@@ -441,6 +450,11 @@ struct branch_info {
const char *name; /* The short name used */
const char *path; /* The full name of a real branch */
struct commit *commit; /* The named commit */
+ /*
+ * if not null the branch is detached because it's already
+ * checked out in this checkout
+ */
+ char *checkout;
};
static void setup_branch_path(struct branch_info *branch)
@@ -502,7 +516,7 @@ static int merge_working_tree(const struct checkout_opts *opts,
topts.dir->flags |= DIR_SHOW_IGNORED;
setup_standard_excludes(topts.dir);
}
- tree = parse_tree_indirect(old->commit ?
+ tree = parse_tree_indirect(old->commit && !opts->new_worktree_mode ?
old->commit->object.sha1 :
EMPTY_TREE_SHA1_BIN);
init_tree_desc(&trees[0], tree->buffer, tree->size);
@@ -606,18 +620,21 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
if (opts->new_orphan_branch) {
if (opts->new_branch_log && !log_all_ref_updates) {
int temp;
- char log_file[PATH_MAX];
- char *ref_name = mkpath("refs/heads/%s", opts->new_orphan_branch);
+ struct strbuf log_file = STRBUF_INIT;
+ int ret;
+ const char *ref_name;
+ ref_name = mkpath("refs/heads/%s", opts->new_orphan_branch);
temp = log_all_ref_updates;
log_all_ref_updates = 1;
- if (log_ref_setup(ref_name, log_file, sizeof(log_file))) {
+ ret = log_ref_setup(ref_name, &log_file);
+ log_all_ref_updates = temp;
+ strbuf_release(&log_file);
+ if (ret) {
fprintf(stderr, _("Can not do reflog for '%s'\n"),
opts->new_orphan_branch);
- log_all_ref_updates = temp;
return;
}
- log_all_ref_updates = temp;
}
}
else
@@ -822,7 +839,8 @@ static int switch_branches(const struct checkout_opts *opts,
return ret;
}
- if (!opts->quiet && !old.path && old.commit && new->commit != old.commit)
+ if (!opts->quiet && !old.path && old.commit &&
+ new->commit != old.commit && !opts->new_worktree_mode)
orphaned_commit_warning(old.commit, new->commit);
update_refs_for_switch(opts, &old, new);
@@ -832,6 +850,138 @@ 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")) {
@@ -887,13 +1037,80 @@ static const char *unique_tracking_name(const char *name, unsigned char *sha1)
return NULL;
}
+static void check_linked_checkout(struct branch_info *new, const char *id)
+{
+ struct strbuf sb = STRBUF_INIT;
+ struct strbuf path = STRBUF_INIT;
+ struct strbuf gitdir = STRBUF_INIT;
+ const char *start, *end;
+
+ if (id)
+ strbuf_addf(&path, "%s/worktrees/%s/HEAD", get_git_common_dir(), id);
+ else
+ strbuf_addf(&path, "%s/HEAD", get_git_common_dir());
+
+ if (strbuf_read_file(&sb, path.buf, 0) < 0 ||
+ !skip_prefix(sb.buf, "ref:", &start))
+ goto done;
+ while (isspace(*start))
+ start++;
+ end = start;
+ while (*end && !isspace(*end))
+ end++;
+ if (strncmp(start, new->path, end - start) || new->path[end - start] != '\0')
+ goto done;
+ if (id) {
+ strbuf_reset(&path);
+ strbuf_addf(&path, "%s/worktrees/%s/gitdir", get_git_common_dir(), id);
+ if (strbuf_read_file(&gitdir, path.buf, 0) <= 0)
+ goto done;
+ strbuf_rtrim(&gitdir);
+ } else
+ strbuf_addstr(&gitdir, get_git_common_dir());
+ die(_("'%s' is already checked out at '%s'"), new->name, gitdir.buf);
+done:
+ strbuf_release(&path);
+ strbuf_release(&sb);
+ strbuf_release(&gitdir);
+}
+
+static void check_linked_checkouts(struct branch_info *new)
+{
+ struct strbuf path = STRBUF_INIT;
+ DIR *dir;
+ struct dirent *d;
+
+ strbuf_addf(&path, "%s/worktrees", get_git_common_dir());
+ if ((dir = opendir(path.buf)) == NULL) {
+ strbuf_release(&path);
+ return;
+ }
+
+ /*
+ * $GIT_COMMON_DIR/HEAD is practically outside
+ * $GIT_DIR so resolve_ref_unsafe() won't work (it
+ * uses git_path). Parse the ref ourselves.
+ */
+ check_linked_checkout(new, NULL);
+
+ while ((d = readdir(dir)) != NULL) {
+ if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, ".."))
+ continue;
+ check_linked_checkout(new, d->d_name);
+ }
+ strbuf_release(&path);
+ closedir(dir);
+}
+
static int parse_branchname_arg(int argc, const char **argv,
int dwim_new_local_branch_ok,
struct branch_info *new,
- struct tree **source_tree,
- unsigned char rev[20],
- const char **new_branch)
+ struct checkout_opts *opts,
+ unsigned char rev[20])
{
+ 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;
@@ -1014,6 +1231,17 @@ 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 */
@@ -1093,6 +1321,9 @@ static int checkout_branch(struct checkout_opts *opts,
die(_("Cannot switch branch to a non-commit '%s'"),
new->name);
+ if (opts->new_worktree)
+ return prepare_linked_checkout(opts, new);
+
if (!new->commit && opts->new_branch) {
unsigned char rev[20];
int flag;
@@ -1135,6 +1366,10 @@ 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(),
};
@@ -1143,6 +1378,9 @@ 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);
@@ -1151,6 +1389,14 @@ 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 */
git_xmerge_config("merge.conflictstyle", conflict_style, NULL);
@@ -1204,8 +1450,7 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
opts.track == BRANCH_TRACK_UNSPECIFIED &&
!opts.new_branch;
int n = parse_branchname_arg(argc, argv, dwim_ok,
- &new, &opts.source_tree,
- rev, &opts.new_branch);
+ &new, &opts, rev);
argv += n;
argc -= n;
}
diff --git a/builtin/clone.c b/builtin/clone.c
index 53a2e5af3..166a645e2 100644
--- a/builtin/clone.c
+++ b/builtin/clone.c
@@ -293,16 +293,17 @@ static void copy_alternates(struct strbuf *src, struct strbuf *dst,
struct strbuf line = STRBUF_INIT;
while (strbuf_getline(&line, in, '\n') != EOF) {
- char *abs_path, abs_buf[PATH_MAX];
+ char *abs_path;
if (!line.len || line.buf[0] == '#')
continue;
if (is_absolute_path(line.buf)) {
add_to_alternates_file(line.buf);
continue;
}
- abs_path = mkpath("%s/objects/%s", src_repo, line.buf);
- normalize_path_copy(abs_buf, abs_path);
- add_to_alternates_file(abs_buf);
+ abs_path = mkpathdup("%s/objects/%s", src_repo, line.buf);
+ normalize_path_copy(abs_path, abs_path);
+ add_to_alternates_file(abs_path);
+ free(abs_path);
}
strbuf_release(&line);
fclose(in);
diff --git a/builtin/commit.c b/builtin/commit.c
index da79ac4bc..310674cfd 100644
--- a/builtin/commit.c
+++ b/builtin/commit.c
@@ -170,7 +170,7 @@ static void determine_whence(struct wt_status *s)
whence = FROM_MERGE;
else if (file_exists(git_path("CHERRY_PICK_HEAD"))) {
whence = FROM_CHERRY_PICK;
- if (file_exists(git_path("sequencer")))
+ if (file_exists(git_path(SEQ_DIR)))
sequencer_in_use = 1;
}
else
diff --git a/builtin/count-objects.c b/builtin/count-objects.c
index e47ef0b1a..ad0c79954 100644
--- a/builtin/count-objects.c
+++ b/builtin/count-objects.c
@@ -70,8 +70,10 @@ int cmd_count_objects(int argc, const char **argv, const char *prefix)
/* we do not take arguments other than flags for now */
if (argc)
usage_with_options(count_objects_usage, opts);
- if (verbose)
+ if (verbose) {
report_garbage = real_report_garbage;
+ report_linked_checkout_garbage();
+ }
for_each_loose_file_in_objdir(get_object_directory(),
count_loose, count_cruft, NULL, NULL);
diff --git a/builtin/fetch.c b/builtin/fetch.c
index f9512652c..7910419c9 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -588,7 +588,8 @@ static int store_updated_refs(const char *raw_url, const char *remote_name,
struct strbuf note = STRBUF_INIT;
const char *what, *kind;
struct ref *rm;
- char *url, *filename = dry_run ? "/dev/null" : git_path("FETCH_HEAD");
+ char *url;
+ const char *filename = dry_run ? "/dev/null" : git_path("FETCH_HEAD");
int want_status;
fp = fopen(filename, "a");
@@ -822,7 +823,7 @@ static void check_not_current_branch(struct ref *ref_map)
static int truncate_fetch_head(void)
{
- char *filename = git_path("FETCH_HEAD");
+ const char *filename = git_path("FETCH_HEAD");
FILE *fp = fopen(filename, "w");
if (!fp)
diff --git a/builtin/fsck.c b/builtin/fsck.c
index 0c757862e..4783896fd 100644
--- a/builtin/fsck.c
+++ b/builtin/fsck.c
@@ -225,12 +225,12 @@ static void check_unreachable_object(struct object *obj)
printf("dangling %s %s\n", typename(obj->type),
sha1_to_hex(obj->sha1));
if (write_lost_and_found) {
- char *filename = git_path("lost-found/%s/%s",
+ const char *filename = git_path("lost-found/%s/%s",
obj->type == OBJ_COMMIT ? "commit" : "other",
sha1_to_hex(obj->sha1));
FILE *f;
- if (safe_create_leading_directories(filename)) {
+ if (safe_create_leading_directories_const(filename)) {
error("Could not create lost-found");
return;
}
diff --git a/builtin/gc.c b/builtin/gc.c
index 5c634afc0..36fe33300 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -33,11 +33,13 @@ static int gc_auto_threshold = 6700;
static int gc_auto_pack_limit = 50;
static int detach_auto = 1;
static const char *prune_expire = "2.weeks.ago";
+static const char *prune_worktrees_expire = "3.months.ago";
static struct argv_array pack_refs_cmd = ARGV_ARRAY_INIT;
static struct argv_array reflog = ARGV_ARRAY_INIT;
static struct argv_array repack = ARGV_ARRAY_INIT;
static struct argv_array prune = ARGV_ARRAY_INIT;
+static struct argv_array prune_worktrees = ARGV_ARRAY_INIT;
static struct argv_array rerere = ARGV_ARRAY_INIT;
static char *pidfile;
@@ -55,6 +57,17 @@ static void remove_pidfile_on_signal(int signo)
raise(signo);
}
+static void git_config_date_string(const char *key, const char **output)
+{
+ if (git_config_get_string_const(key, output))
+ return;
+ if (strcmp(*output, "now")) {
+ unsigned long now = approxidate("now");
+ if (approxidate(*output) >= now)
+ git_die_config(key, _("Invalid %s: '%s'"), key, *output);
+ }
+}
+
static void gc_config(void)
{
const char *value;
@@ -71,16 +84,8 @@ static void gc_config(void)
git_config_get_int("gc.auto", &gc_auto_threshold);
git_config_get_int("gc.autopacklimit", &gc_auto_pack_limit);
git_config_get_bool("gc.autodetach", &detach_auto);
-
- if (!git_config_get_string_const("gc.pruneexpire", &prune_expire)) {
- if (strcmp(prune_expire, "now")) {
- unsigned long now = approxidate("now");
- if (approxidate(prune_expire) >= now) {
- git_die_config("gc.pruneexpire", _("Invalid gc.pruneexpire: '%s'"),
- prune_expire);
- }
- }
- }
+ git_config_date_string("gc.pruneexpire", &prune_expire);
+ git_config_date_string("gc.pruneworktreesexpire", &prune_worktrees_expire);
git_config(git_default_config, NULL);
}
@@ -287,7 +292,8 @@ int cmd_gc(int argc, const char **argv, const char *prefix)
argv_array_pushl(&pack_refs_cmd, "pack-refs", "--all", "--prune", NULL);
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, "prune", "--expire", NULL);
+ argv_array_pushl(&prune_worktrees, "prune", "--worktrees", "--expire", NULL);
argv_array_pushl(&rerere, "rerere", "gc", NULL);
gc_config();
@@ -357,6 +363,12 @@ int cmd_gc(int argc, const char **argv, const char *prefix)
return error(FAILED_RUN, prune.argv[0]);
}
+ if (prune_worktrees_expire) {
+ argv_array_push(&prune_worktrees, prune_worktrees_expire);
+ if (run_command_v_opt(prune_worktrees.argv, RUN_GIT_CMD))
+ return error(FAILED_RUN, prune_worktrees.argv[0]);
+ }
+
if (run_command_v_opt(rerere.argv, RUN_GIT_CMD))
return error(FAILED_RUN, rerere.argv[0]);
diff --git a/builtin/init-db.c b/builtin/init-db.c
index ab9f86b88..433573813 100644
--- a/builtin/init-db.c
+++ b/builtin/init-db.c
@@ -362,7 +362,6 @@ int set_git_dir_init(const char *git_dir, const char *real_git_dir,
static void separate_git_dir(const char *git_dir)
{
struct stat st;
- FILE *fp;
if (!stat(git_link, &st)) {
const char *src;
@@ -378,11 +377,7 @@ static void separate_git_dir(const char *git_dir)
die_errno(_("unable to move %s to %s"), src, git_dir);
}
- fp = fopen(git_link, "w");
- if (!fp)
- die(_("Could not create git link %s"), git_link);
- fprintf(fp, "gitdir: %s\n", git_dir);
- fclose(fp);
+ write_file(git_link, 1, "gitdir: %s\n", git_dir);
}
int init_db(const char *template_dir, unsigned int flags)
diff --git a/builtin/prune.c b/builtin/prune.c
index 17094ad95..0c73246c7 100644
--- a/builtin/prune.c
+++ b/builtin/prune.c
@@ -76,6 +76,95 @@ 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
@@ -102,10 +191,12 @@ 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()
@@ -119,6 +210,14 @@ int cmd_prune(int argc, const char **argv, const char *prefix)
init_revisions(&revs, 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++;
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 5292bb5a5..d2ec52bca 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -1008,7 +1008,7 @@ static void run_update_post_hook(struct command *commands)
int argc;
const char **argv;
struct child_process proc = CHILD_PROCESS_INIT;
- char *hook;
+ const char *hook;
hook = find_hook("post-update");
for (argc = 0, cmd = commands; cmd; cmd = cmd->next) {
diff --git a/builtin/remote.c b/builtin/remote.c
index 5d3ab906b..ad57fc984 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -584,7 +584,7 @@ static int migrate_file(struct remote *remote)
{
struct strbuf buf = STRBUF_INIT;
int i;
- char *path = NULL;
+ const char *path = NULL;
strbuf_addf(&buf, "remote.%s.url", remote->name);
for (i = 0; i < remote->url_nr; i++)
diff --git a/builtin/repack.c b/builtin/repack.c
index f2edeb0f4..af7340c7b 100644
--- a/builtin/repack.c
+++ b/builtin/repack.c
@@ -285,7 +285,8 @@ int cmd_repack(int argc, const char **argv, const char *prefix)
failed = 0;
for_each_string_list_item(item, &names) {
for (ext = 0; ext < ARRAY_SIZE(exts); ext++) {
- char *fname, *fname_old;
+ const char *fname_old;
+ char *fname;
fname = mkpathdup("%s/pack-%s%s", packdir,
item->string, exts[ext].name);
if (!file_exists(fname)) {
@@ -313,7 +314,8 @@ int cmd_repack(int argc, const char **argv, const char *prefix)
if (failed) {
struct string_list rollback_failure = STRING_LIST_INIT_DUP;
for_each_string_list_item(item, &rollback) {
- char *fname, *fname_old;
+ const char *fname_old;
+ char *fname;
fname = mkpathdup("%s/%s", packdir, item->string);
fname_old = mkpath("%s/old-%s", packdir, item->string);
if (rename(fname_old, fname))
@@ -366,7 +368,7 @@ int cmd_repack(int argc, const char **argv, const char *prefix)
/* Remove the "old-" files */
for_each_string_list_item(item, &names) {
for (ext = 0; ext < ARRAY_SIZE(exts); ext++) {
- char *fname;
+ const char *fname;
fname = mkpath("%s/old-%s%s",
packdir,
item->string,
diff --git a/builtin/rev-parse.c b/builtin/rev-parse.c
index 3626c61da..4d10dd954 100644
--- a/builtin/rev-parse.c
+++ b/builtin/rev-parse.c
@@ -533,6 +533,13 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)
for (i = 1; i < argc; i++) {
const char *arg = argv[i];
+ if (!strcmp(arg, "--git-path")) {
+ if (!argv[i + 1])
+ die("--git-path requires an argument");
+ puts(git_path("%s", argv[i + 1]));
+ i++;
+ continue;
+ }
if (as_is) {
if (show_file(arg, output_prefix) && as_is < 2)
verify_filename(prefix, arg, 0);
@@ -755,6 +762,10 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)
free(cwd);
continue;
}
+ if (!strcmp(arg, "--git-common-dir")) {
+ puts(get_git_common_dir());
+ continue;
+ }
if (!strcmp(arg, "--resolve-git-dir")) {
const char *gitdir = argv[++i];
if (!gitdir)
diff --git a/cache.h b/cache.h
index b34447ffc..771b77562 100644
--- a/cache.h
+++ b/cache.h
@@ -378,6 +378,7 @@ static inline enum object_type object_type(unsigned int mode)
/* Double-check local_repo_env below if you add to this list. */
#define GIT_DIR_ENVIRONMENT "GIT_DIR"
+#define GIT_COMMON_DIR_ENVIRONMENT "GIT_COMMON_DIR"
#define GIT_NAMESPACE_ENVIRONMENT "GIT_NAMESPACE"
#define GIT_WORK_TREE_ENVIRONMENT "GIT_WORK_TREE"
#define GIT_PREFIX_ENVIRONMENT "GIT_PREFIX"
@@ -431,11 +432,13 @@ extern int is_inside_git_dir(void);
extern char *git_work_tree_cfg;
extern int is_inside_work_tree(void);
extern const char *get_git_dir(void);
+extern const char *get_git_common_dir(void);
extern int is_git_directory(const char *path);
extern char *get_object_directory(void);
extern char *get_index_file(void);
extern char *get_graft_file(void);
extern int set_git_dir(const char *path);
+extern int get_common_dir(struct strbuf *sb, const char *gitdir);
extern const char *get_git_namespace(void);
extern const char *strip_namespace(const char *namespaced_ref);
extern const char *get_git_work_tree(void);
@@ -620,6 +623,7 @@ extern int core_apply_sparse_checkout;
extern int precomposed_unicode;
extern int protect_hfs;
extern int protect_ntfs;
+extern int git_db_env, git_index_env, git_graft_env, git_common_dir_env;
/*
* Include broken refs in all ref iterations, which will
@@ -690,18 +694,19 @@ extern int check_repository_format(void);
extern char *mksnpath(char *buf, size_t n, const char *fmt, ...)
__attribute__((format (printf, 3, 4)));
-extern char *git_snpath(char *buf, size_t n, const char *fmt, ...)
- __attribute__((format (printf, 3, 4)));
+extern void strbuf_git_path(struct strbuf *sb, const char *fmt, ...)
+ __attribute__((format (printf, 2, 3)));
extern char *git_pathdup(const char *fmt, ...)
__attribute__((format (printf, 1, 2)));
extern char *mkpathdup(const char *fmt, ...)
__attribute__((format (printf, 1, 2)));
/* Return a statically allocated filename matching the sha1 signature */
-extern char *mkpath(const char *fmt, ...) __attribute__((format (printf, 1, 2)));
-extern char *git_path(const char *fmt, ...) __attribute__((format (printf, 1, 2)));
-extern char *git_path_submodule(const char *path, const char *fmt, ...)
+extern const char *mkpath(const char *fmt, ...) __attribute__((format (printf, 1, 2)));
+extern const char *git_path(const char *fmt, ...) __attribute__((format (printf, 1, 2)));
+extern const char *git_path_submodule(const char *path, const char *fmt, ...)
__attribute__((format (printf, 2, 3)));
+extern void report_linked_checkout_garbage(void);
/*
* Return the name of the file in the local object database that would
@@ -1543,6 +1548,8 @@ static inline ssize_t write_str_in_full(int fd, const char *str)
{
return write_in_full(fd, str, strlen(str));
}
+__attribute__((format (printf, 3, 4)))
+extern int write_file(const char *path, int fatal, const char *fmt, ...);
/* pager.c */
extern void setup_pager(void);
diff --git a/daemon.c b/daemon.c
index 9ee21877c..ac2bc852d 100644
--- a/daemon.c
+++ b/daemon.c
@@ -1166,15 +1166,6 @@ static struct credentials *prepare_credentials(const char *user_name,
}
#endif
-static void store_pid(const char *path)
-{
- FILE *f = fopen(path, "w");
- if (!f)
- die_errno("cannot open pid file '%s'", path);
- if (fprintf(f, "%"PRIuMAX"\n", (uintmax_t) getpid()) < 0 || fclose(f) != 0)
- die_errno("failed to write pid file '%s'", path);
-}
-
static int serve(struct string_list *listen_addr, int listen_port,
struct credentials *cred)
{
@@ -1385,7 +1376,7 @@ int main(int argc, char **argv)
sanitize_stdfds();
if (pid_file)
- store_pid(pid_file);
+ write_file(pid_file, 1, "%"PRIuMAX"\n", (uintmax_t) getpid());
/* prepare argv for serving-processes */
cld_argv = xmalloc(sizeof (char *) * (argc + 2));
diff --git a/environment.c b/environment.c
index a40044c3b..61c685b8d 100644
--- a/environment.c
+++ b/environment.c
@@ -92,8 +92,9 @@ static char *work_tree;
static const char *namespace;
static size_t namespace_len;
-static const char *git_dir;
+static const char *git_dir, *git_common_dir;
static char *git_object_dir, *git_index_file, *git_graft_file;
+int git_db_env, git_index_env, git_graft_env, git_common_dir_env;
/*
* Repository-local GIT_* environment variables; see cache.h for details.
@@ -111,6 +112,7 @@ const char * const local_repo_env[] = {
NO_REPLACE_OBJECTS_ENVIRONMENT,
GIT_PREFIX_ENVIRONMENT,
GIT_SHALLOW_FILE_ENVIRONMENT,
+ GIT_COMMON_DIR_ENVIRONMENT,
NULL
};
@@ -135,14 +137,23 @@ static char *expand_namespace(const char *raw_namespace)
return strbuf_detach(&buf, NULL);
}
-static char *git_path_from_env(const char *envvar, const char *path)
+static char *git_path_from_env(const char *envvar, const char *git_dir,
+ const char *path, int *fromenv)
{
const char *value = getenv(envvar);
- return value ? xstrdup(value) : git_pathdup("%s", path);
+ if (!value) {
+ char *buf = xmalloc(strlen(git_dir) + strlen(path) + 2);
+ sprintf(buf, "%s/%s", git_dir, path);
+ return buf;
+ }
+ if (fromenv)
+ *fromenv = 1;
+ return xstrdup(value);
}
static void setup_git_env(void)
{
+ struct strbuf sb = STRBUF_INIT;
const char *gitfile;
const char *shallow_file;
@@ -151,9 +162,15 @@ static void setup_git_env(void)
git_dir = DEFAULT_GIT_DIR_ENVIRONMENT;
gitfile = read_gitfile(git_dir);
git_dir = xstrdup(gitfile ? gitfile : git_dir);
- git_object_dir = git_path_from_env(DB_ENVIRONMENT, "objects");
- git_index_file = git_path_from_env(INDEX_ENVIRONMENT, "index");
- git_graft_file = git_path_from_env(GRAFT_ENVIRONMENT, "info/grafts");
+ if (get_common_dir(&sb, git_dir))
+ git_common_dir_env = 1;
+ git_common_dir = strbuf_detach(&sb, NULL);
+ git_object_dir = git_path_from_env(DB_ENVIRONMENT, git_common_dir,
+ "objects", &git_db_env);
+ git_index_file = git_path_from_env(INDEX_ENVIRONMENT, git_dir,
+ "index", &git_index_env);
+ git_graft_file = git_path_from_env(GRAFT_ENVIRONMENT, git_common_dir,
+ "info/grafts", &git_graft_env);
if (getenv(NO_REPLACE_OBJECTS_ENVIRONMENT))
check_replace_refs = 0;
namespace = expand_namespace(getenv(GIT_NAMESPACE_ENVIRONMENT));
@@ -176,6 +193,11 @@ const char *get_git_dir(void)
return git_dir;
}
+const char *get_git_common_dir(void)
+{
+ return git_common_dir;
+}
+
const char *get_git_namespace(void)
{
if (!namespace)
diff --git a/fast-import.c b/fast-import.c
index e78ca107b..637872699 100644
--- a/fast-import.c
+++ b/fast-import.c
@@ -405,7 +405,7 @@ static void dump_marks_helper(FILE *, uintmax_t, struct mark_set *);
static void write_crash_report(const char *err)
{
- char *loc = git_path("fast_import_crash_%"PRIuMAX, (uintmax_t) getpid());
+ const char *loc = git_path("fast_import_crash_%"PRIuMAX, (uintmax_t) getpid());
FILE *rpt = fopen(loc, "w");
struct branch *b;
unsigned long lu;
@@ -3113,12 +3113,9 @@ static void parse_progress(void)
static char* make_fast_import_path(const char *path)
{
- struct strbuf abs_path = STRBUF_INIT;
-
if (!relative_marks_paths || is_absolute_path(path))
return xstrdup(path);
- strbuf_addf(&abs_path, "%s/info/fast-import/%s", get_git_dir(), path);
- return strbuf_detach(&abs_path, NULL);
+ return xstrdup(git_path("info/fast-import/%s", path));
}
static void option_import_marks(const char *marks,
diff --git a/git-am.sh b/git-am.sh
index a67d0f989..761befbd3 100755
--- a/git-am.sh
+++ b/git-am.sh
@@ -827,10 +827,10 @@ To restore the original branch and stop patching run \"\$cmdline --abort\"."
continue
fi
- if test -x "$GIT_DIR"/hooks/applypatch-msg
+ hook="$(git rev-parse --git-path hooks/applypatch-msg)"
+ if test -x "$hook"
then
- "$GIT_DIR"/hooks/applypatch-msg "$dotest/final-commit" ||
- stop_here $this
+ "$hook" "$dotest/final-commit" || stop_here $this
fi
if test -f "$dotest/final-commit"
@@ -904,9 +904,10 @@ did you forget to use 'git add'?"
stop_here_user_resolve $this
fi
- if test -x "$GIT_DIR"/hooks/pre-applypatch
+ hook="$(git rev-parse --git-path hooks/pre-applypatch)"
+ if test -x "$hook"
then
- "$GIT_DIR"/hooks/pre-applypatch || stop_here $this
+ "$hook" || stop_here $this
fi
tree=$(git write-tree) &&
@@ -933,18 +934,17 @@ did you forget to use 'git add'?"
echo "$(cat "$dotest/original-commit") $commit" >> "$dotest/rewritten"
fi
- if test -x "$GIT_DIR"/hooks/post-applypatch
- then
- "$GIT_DIR"/hooks/post-applypatch
- fi
+ hook="$(git rev-parse --git-path hooks/post-applypatch)"
+ test -x "$hook" && "$hook"
go_next
done
if test -s "$dotest"/rewritten; then
git notes copy --for-rewrite=rebase < "$dotest"/rewritten
- if test -x "$GIT_DIR"/hooks/post-rewrite; then
- "$GIT_DIR"/hooks/post-rewrite rebase < "$dotest"/rewritten
+ hook="$(git rev-parse --git-path hooks/post-rewrite)"
+ if test -x "$hook"; then
+ "$hook" rebase < "$dotest"/rewritten
fi
fi
diff --git a/git-pull.sh b/git-pull.sh
index 4d4fc77b0..ad442264c 100755
--- a/git-pull.sh
+++ b/git-pull.sh
@@ -240,7 +240,7 @@ test true = "$rebase" && {
if ! git rev-parse -q --verify HEAD >/dev/null
then
# On an unborn branch
- if test -f "$GIT_DIR/index"
+ if test -f "$(git rev-parse --git-path index)"
then
die "$(gettext "updating an unborn branch with changes added to the index")"
fi
diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
index f7deeb096..08e5d86fe 100644
--- a/git-rebase--interactive.sh
+++ b/git-rebase--interactive.sh
@@ -642,9 +642,9 @@ do_next () {
git notes copy --for-rewrite=rebase < "$rewritten_list" ||
true # we don't care if this copying failed
} &&
- if test -x "$GIT_DIR"/hooks/post-rewrite &&
- test -s "$rewritten_list"; then
- "$GIT_DIR"/hooks/post-rewrite rebase < "$rewritten_list"
+ hook="$(git rev-parse --git-path hooks/post-rewrite)"
+ if test -x "$hook" && test -s "$rewritten_list"; then
+ "$hook" rebase < "$rewritten_list"
true # we don't care if this hook failed
fi &&
warn "Successfully rebased and updated $head_name."
diff --git a/git-rebase--merge.sh b/git-rebase--merge.sh
index d3fb67d75..2cc2a6d27 100644
--- a/git-rebase--merge.sh
+++ b/git-rebase--merge.sh
@@ -94,10 +94,8 @@ finish_rb_merge () {
if test -s "$state_dir"/rewritten
then
git notes copy --for-rewrite=rebase <"$state_dir"/rewritten
- if test -x "$GIT_DIR"/hooks/post-rewrite
- then
- "$GIT_DIR"/hooks/post-rewrite rebase <"$state_dir"/rewritten
- fi
+ hook="$(git rev-parse --git-path hooks/post-rewrite)"
+ test -x "$hook" && "$hook" rebase <"$state_dir"/rewritten
fi
say All done.
}
diff --git a/git-rebase.sh b/git-rebase.sh
index 55da9db81..fb935a03f 100755
--- a/git-rebase.sh
+++ b/git-rebase.sh
@@ -202,9 +202,9 @@ run_specific_rebase () {
run_pre_rebase_hook () {
if test -z "$ok_to_skip_pre_rebase" &&
- test -x "$GIT_DIR/hooks/pre-rebase"
+ test -x "$(git rev-parse --git-path hooks/pre-rebase)"
then
- "$GIT_DIR/hooks/pre-rebase" ${1+"$@"} ||
+ "$(git rev-parse --git-path hooks/pre-rebase)" ${1+"$@"} ||
die "$(gettext "The pre-rebase hook refused to rebase.")"
fi
}
diff --git a/git-sh-setup.sh b/git-sh-setup.sh
index c42c6e636..4691fbcb6 100644
--- a/git-sh-setup.sh
+++ b/git-sh-setup.sh
@@ -344,7 +344,7 @@ git_dir_init () {
echo >&2 "Unable to determine absolute path of git directory"
exit 1
}
- : ${GIT_OBJECT_DIRECTORY="$GIT_DIR/objects"}
+ : ${GIT_OBJECT_DIRECTORY="$(git rev-parse --git-path objects)"}
}
if test -z "$NONGIT_OK"
diff --git a/git-stash.sh b/git-stash.sh
index d4cf818be..6846b18db 100755
--- a/git-stash.sh
+++ b/git-stash.sh
@@ -20,7 +20,7 @@ require_work_tree
cd_to_toplevel
TMP="$GIT_DIR/.git-stash.$$"
-TMPindex=${GIT_INDEX_FILE-"$GIT_DIR/index"}.stash.$$
+TMPindex=${GIT_INDEX_FILE-"$(git rev-parse --git-path index)"}.stash.$$
trap 'rm -f "$TMP-"* "$TMPindex"' 0
ref_stash=refs/stash
@@ -184,7 +184,7 @@ store_stash () {
fi
# Make sure the reflog for stash is kept.
- : >>"$GIT_DIR/logs/$ref_stash"
+ : >>"$(git rev-parse --git-path logs/$ref_stash)"
git update-ref -m "$stash_msg" $ref_stash $w_commit
ret=$?
test $ret != 0 && test -z $quiet &&
@@ -259,7 +259,7 @@ save_stash () {
say "$(gettext "No local changes to save")"
exit 0
fi
- test -f "$GIT_DIR/logs/$ref_stash" ||
+ test -f "$(git rev-parse --git-path logs/$ref_stash)" ||
clear_stash || die "$(gettext "Cannot initialize stash")"
create_stash "$stash_msg" $untracked
diff --git a/git.c b/git.c
index 42a4ee578..44374b1d9 100644
--- a/git.c
+++ b/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 | NEED_WORK_TREE },
+ { "checkout", cmd_checkout, RUN_SETUP },
{ "checkout-index", cmd_checkout_index,
RUN_SETUP | NEED_WORK_TREE},
{ "cherry", cmd_cherry, RUN_SETUP },
diff --git a/notes-merge.c b/notes-merge.c
index 109ff4ef4..0b2b82c41 100644
--- a/notes-merge.c
+++ b/notes-merge.c
@@ -280,7 +280,7 @@ static void check_notes_merge_worktree(struct notes_merge_options *o)
"(%s exists).", git_path("NOTES_MERGE_*"));
}
- if (safe_create_leading_directories(git_path(
+ if (safe_create_leading_directories_const(git_path(
NOTES_MERGE_WORKTREE "/.test")))
die_errno("unable to create directory %s",
git_path(NOTES_MERGE_WORKTREE));
@@ -295,8 +295,8 @@ static void write_buf_to_worktree(const unsigned char *obj,
const char *buf, unsigned long size)
{
int fd;
- char *path = git_path(NOTES_MERGE_WORKTREE "/%s", sha1_to_hex(obj));
- if (safe_create_leading_directories(path))
+ const char *path = git_path(NOTES_MERGE_WORKTREE "/%s", sha1_to_hex(obj));
+ if (safe_create_leading_directories_const(path))
die_errno("unable to create directory for '%s'", path);
if (file_exists(path))
die("found existing file at '%s'", path);
diff --git a/path.c b/path.c
index 595da81ca..586f2c90a 100644
--- a/path.c
+++ b/path.c
@@ -4,6 +4,7 @@
#include "cache.h"
#include "strbuf.h"
#include "string-list.h"
+#include "dir.h"
static int get_st_mode_bits(const char *path, int *mode)
{
@@ -16,11 +17,15 @@ static int get_st_mode_bits(const char *path, int *mode)
static char bad_path[] = "/bad-path/";
-static char *get_pathname(void)
+static struct strbuf *get_pathname(void)
{
- static char pathname_array[4][PATH_MAX];
+ static struct strbuf pathname_array[4] = {
+ STRBUF_INIT, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT
+ };
static int index;
- return pathname_array[3 & ++index];
+ struct strbuf *sb = &pathname_array[3 & ++index];
+ strbuf_reset(sb);
+ return sb;
}
static char *cleanup_path(char *path)
@@ -34,6 +39,13 @@ static char *cleanup_path(char *path)
return path;
}
+static void strbuf_cleanup_path(struct strbuf *sb)
+{
+ char *path = cleanup_path(sb->buf);
+ if (path > sb->buf)
+ strbuf_remove(sb, 0, path - sb->buf);
+}
+
char *mksnpath(char *buf, size_t n, const char *fmt, ...)
{
va_list args;
@@ -49,85 +61,167 @@ char *mksnpath(char *buf, size_t n, const char *fmt, ...)
return cleanup_path(buf);
}
-static char *vsnpath(char *buf, size_t n, const char *fmt, va_list args)
+static int dir_prefix(const char *buf, const char *dir)
{
- const char *git_dir = get_git_dir();
- size_t len;
+ int len = strlen(dir);
+ return !strncmp(buf, dir, len) &&
+ (is_dir_sep(buf[len]) || buf[len] == '\0');
+}
- len = strlen(git_dir);
- if (n < len + 1)
- goto bad;
- memcpy(buf, git_dir, len);
- if (len && !is_dir_sep(git_dir[len-1]))
- buf[len++] = '/';
- len += vsnprintf(buf + len, n - len, fmt, args);
- if (len >= n)
- goto bad;
- return cleanup_path(buf);
-bad:
- strlcpy(buf, bad_path, n);
- return buf;
+/* $buf =~ m|$dir/+$file| but without regex */
+static int is_dir_file(const char *buf, const char *dir, const char *file)
+{
+ int len = strlen(dir);
+ if (strncmp(buf, dir, len) || !is_dir_sep(buf[len]))
+ return 0;
+ while (is_dir_sep(buf[len]))
+ len++;
+ return !strcmp(buf + len, file);
+}
+
+static void replace_dir(struct strbuf *buf, int len, const char *newdir)
+{
+ int newlen = strlen(newdir);
+ int need_sep = (buf->buf[len] && !is_dir_sep(buf->buf[len])) &&
+ !is_dir_sep(newdir[newlen - 1]);
+ if (need_sep)
+ len--; /* keep one char, to be replaced with '/' */
+ strbuf_splice(buf, 0, len, newdir, newlen);
+ if (need_sep)
+ buf->buf[newlen] = '/';
+}
+
+static const char *common_list[] = {
+ "/branches", "/hooks", "/info", "!/logs", "/lost-found",
+ "/objects", "/refs", "/remotes", "/worktrees", "/rr-cache", "/svn",
+ "config", "!gc.pid", "packed-refs", "shallow",
+ NULL
+};
+
+static void update_common_dir(struct strbuf *buf, int git_dir_len)
+{
+ char *base = buf->buf + git_dir_len;
+ const char **p;
+
+ if (is_dir_file(base, "logs", "HEAD") ||
+ is_dir_file(base, "info", "sparse-checkout"))
+ return; /* keep this in $GIT_DIR */
+ for (p = common_list; *p; p++) {
+ const char *path = *p;
+ int is_dir = 0;
+ if (*path == '!')
+ path++;
+ if (*path == '/') {
+ path++;
+ is_dir = 1;
+ }
+ if (is_dir && dir_prefix(base, path)) {
+ replace_dir(buf, git_dir_len, get_git_common_dir());
+ return;
+ }
+ if (!is_dir && !strcmp(base, path)) {
+ replace_dir(buf, git_dir_len, get_git_common_dir());
+ return;
+ }
+ }
+}
+
+void report_linked_checkout_garbage(void)
+{
+ struct strbuf sb = STRBUF_INIT;
+ const char **p;
+ int len;
+
+ if (!git_common_dir_env)
+ return;
+ strbuf_addf(&sb, "%s/", get_git_dir());
+ len = sb.len;
+ for (p = common_list; *p; p++) {
+ const char *path = *p;
+ if (*path == '!')
+ continue;
+ strbuf_setlen(&sb, len);
+ strbuf_addstr(&sb, path);
+ if (file_exists(sb.buf))
+ report_garbage("unused in linked checkout", sb.buf);
+ }
+ strbuf_release(&sb);
+}
+
+static void adjust_git_path(struct strbuf *buf, int git_dir_len)
+{
+ const char *base = buf->buf + git_dir_len;
+ if (git_graft_env && is_dir_file(base, "info", "grafts"))
+ strbuf_splice(buf, 0, buf->len,
+ get_graft_file(), strlen(get_graft_file()));
+ else if (git_index_env && !strcmp(base, "index"))
+ strbuf_splice(buf, 0, buf->len,
+ get_index_file(), strlen(get_index_file()));
+ else if (git_db_env && dir_prefix(base, "objects"))
+ replace_dir(buf, git_dir_len + 7, get_object_directory());
+ else if (git_common_dir_env)
+ update_common_dir(buf, git_dir_len);
+}
+
+static void do_git_path(struct strbuf *buf, const char *fmt, va_list args)
+{
+ int gitdir_len;
+ strbuf_addstr(buf, get_git_dir());
+ if (buf->len && !is_dir_sep(buf->buf[buf->len - 1]))
+ strbuf_addch(buf, '/');
+ gitdir_len = buf->len;
+ strbuf_vaddf(buf, fmt, args);
+ adjust_git_path(buf, gitdir_len);
+ strbuf_cleanup_path(buf);
}
-char *git_snpath(char *buf, size_t n, const char *fmt, ...)
+void strbuf_git_path(struct strbuf *sb, const char *fmt, ...)
{
- char *ret;
va_list args;
va_start(args, fmt);
- ret = vsnpath(buf, n, fmt, args);
+ do_git_path(sb, fmt, args);
va_end(args);
- return ret;
}
-char *git_pathdup(const char *fmt, ...)
+const char *git_path(const char *fmt, ...)
{
- char path[PATH_MAX], *ret;
+ struct strbuf *pathname = get_pathname();
va_list args;
va_start(args, fmt);
- ret = vsnpath(path, sizeof(path), fmt, args);
+ do_git_path(pathname, fmt, args);
va_end(args);
- return xstrdup(ret);
+ return pathname->buf;
}
-char *mkpathdup(const char *fmt, ...)
+char *git_pathdup(const char *fmt, ...)
{
- char *path;
- struct strbuf sb = STRBUF_INIT;
+ struct strbuf path = STRBUF_INIT;
va_list args;
-
va_start(args, fmt);
- strbuf_vaddf(&sb, fmt, args);
+ do_git_path(&path, fmt, args);
va_end(args);
- path = xstrdup(cleanup_path(sb.buf));
-
- strbuf_release(&sb);
- return path;
+ return strbuf_detach(&path, NULL);
}
-char *mkpath(const char *fmt, ...)
+char *mkpathdup(const char *fmt, ...)
{
+ struct strbuf sb = STRBUF_INIT;
va_list args;
- unsigned len;
- char *pathname = get_pathname();
-
va_start(args, fmt);
- len = vsnprintf(pathname, PATH_MAX, fmt, args);
+ strbuf_vaddf(&sb, fmt, args);
va_end(args);
- if (len >= PATH_MAX)
- return bad_path;
- return cleanup_path(pathname);
+ strbuf_cleanup_path(&sb);
+ return strbuf_detach(&sb, NULL);
}
-char *git_path(const char *fmt, ...)
+const char *mkpath(const char *fmt, ...)
{
- char *pathname = get_pathname();
va_list args;
- char *ret;
-
+ struct strbuf *pathname = get_pathname();
va_start(args, fmt);
- ret = vsnpath(pathname, PATH_MAX, fmt, args);
+ strbuf_vaddf(pathname, fmt, args);
va_end(args);
- return ret;
+ return cleanup_path(pathname->buf);
}
void home_config_paths(char **global, char **xdg, char *file)
@@ -158,43 +252,29 @@ void home_config_paths(char **global, char **xdg, char *file)
free(to_free);
}
-char *git_path_submodule(const char *path, const char *fmt, ...)
+const char *git_path_submodule(const char *path, const char *fmt, ...)
{
- char *pathname = get_pathname();
- struct strbuf buf = STRBUF_INIT;
+ struct strbuf *buf = get_pathname();
const char *git_dir;
va_list args;
- unsigned len;
- len = strlen(path);
- if (len > PATH_MAX-100)
- return bad_path;
+ strbuf_addstr(buf, path);
+ if (buf->len && buf->buf[buf->len - 1] != '/')
+ strbuf_addch(buf, '/');
+ strbuf_addstr(buf, ".git");
- strbuf_addstr(&buf, path);
- if (len && path[len-1] != '/')
- strbuf_addch(&buf, '/');
- strbuf_addstr(&buf, ".git");
-
- git_dir = read_gitfile(buf.buf);
+ git_dir = read_gitfile(buf->buf);
if (git_dir) {
- strbuf_reset(&buf);
- strbuf_addstr(&buf, git_dir);
+ strbuf_reset(buf);
+ strbuf_addstr(buf, git_dir);
}
- strbuf_addch(&buf, '/');
-
- if (buf.len >= PATH_MAX)
- return bad_path;
- memcpy(pathname, buf.buf, buf.len + 1);
-
- strbuf_release(&buf);
- len = strlen(pathname);
+ strbuf_addch(buf, '/');
va_start(args, fmt);
- len += vsnprintf(pathname + len, PATH_MAX - len, fmt, args);
+ strbuf_vaddf(buf, fmt, args);
va_end(args);
- if (len >= PATH_MAX)
- return bad_path;
- return cleanup_path(pathname);
+ strbuf_cleanup_path(buf);
+ return buf->buf;
}
int validate_headref(const char *path)
diff --git a/refs.c b/refs.c
index 47e4e5380..0312f0525 100644
--- a/refs.c
+++ b/refs.c
@@ -1382,7 +1382,7 @@ static int resolve_gitlink_ref_recursive(struct ref_cache *refs,
{
int fd, len;
char buffer[128], *p;
- char *path;
+ const char *path;
if (recursion > MAXDEPTH || strlen(refname) > MAXREFLEN)
return -1;
@@ -1475,7 +1475,11 @@ static int resolve_missing_loose_ref(const char *refname,
}
/* This function needs to return a meaningful errno on failure */
-const char *resolve_ref_unsafe(const char *refname, int resolve_flags, unsigned char *sha1, int *flags)
+static const char *resolve_ref_unsafe_1(const char *refname,
+ int resolve_flags,
+ unsigned char *sha1,
+ int *flags,
+ struct strbuf *sb_path)
{
int depth = MAXDEPTH;
ssize_t len;
@@ -1506,7 +1510,7 @@ const char *resolve_ref_unsafe(const char *refname, int resolve_flags, unsigned
bad_name = 1;
}
for (;;) {
- char path[PATH_MAX];
+ const char *path;
struct stat st;
char *buf;
int fd;
@@ -1516,7 +1520,9 @@ const char *resolve_ref_unsafe(const char *refname, int resolve_flags, unsigned
return NULL;
}
- git_snpath(path, sizeof(path), "%s", refname);
+ strbuf_reset(sb_path);
+ strbuf_git_path(sb_path, "%s", refname);
+ path = sb_path->buf;
/*
* We might have to loop back here to avoid a race
@@ -1643,6 +1649,16 @@ const char *resolve_ref_unsafe(const char *refname, int resolve_flags, unsigned
}
}
+const char *resolve_ref_unsafe(const char *refname, int resolve_flags,
+ unsigned char *sha1, int *flags)
+{
+ struct strbuf sb_path = STRBUF_INIT;
+ const char *ret = resolve_ref_unsafe_1(refname, resolve_flags,
+ sha1, flags, &sb_path);
+ strbuf_release(&sb_path);
+ return ret;
+}
+
char *resolve_refdup(const char *ref, int resolve_flags, unsigned char *sha1, int *flags)
{
return xstrdup_or_null(resolve_ref_unsafe(ref, resolve_flags, sha1, flags));
@@ -2274,7 +2290,7 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname,
const struct string_list *skip,
unsigned int flags, int *type_p)
{
- char *ref_file;
+ const char *ref_file;
const char *orig_refname = refname;
struct ref_lock *lock;
int last_errno = 0;
@@ -2343,7 +2359,7 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname,
ref_file = git_path("%s", refname);
retry:
- switch (safe_create_leading_directories(ref_file)) {
+ switch (safe_create_leading_directories_const(ref_file)) {
case SCLD_OK:
break; /* success */
case SCLD_VANISHED:
@@ -2721,7 +2737,7 @@ static int rename_tmp_log(const char *newrefname)
int attempts_remaining = 4;
retry:
- switch (safe_create_leading_directories(git_path("logs/%s", newrefname))) {
+ switch (safe_create_leading_directories_const(git_path("logs/%s", newrefname))) {
case SCLD_OK:
break; /* success */
case SCLD_VANISHED:
@@ -2907,11 +2923,15 @@ static int copy_msg(char *buf, const char *msg)
}
/* This function must set a meaningful errno on failure */
-int log_ref_setup(const char *refname, char *logfile, int bufsize)
+int log_ref_setup(const char *refname, struct strbuf *sb_logfile)
{
int logfd, oflags = O_APPEND | O_WRONLY;
+ char *logfile;
- git_snpath(logfile, bufsize, "logs/%s", refname);
+ strbuf_git_path(sb_logfile, "logs/%s", refname);
+ logfile = sb_logfile->buf;
+ /* make sure the rest of the function can't change "logfile" */
+ sb_logfile = NULL;
if (log_all_ref_updates &&
(starts_with(refname, "refs/heads/") ||
starts_with(refname, "refs/remotes/") ||
@@ -2982,18 +3002,22 @@ static int log_ref_write_fd(int fd, const unsigned char *old_sha1,
return 0;
}
-static int log_ref_write(const char *refname, const unsigned char *old_sha1,
- const unsigned char *new_sha1, const char *msg)
+static int log_ref_write_1(const char *refname, const unsigned char *old_sha1,
+ const unsigned char *new_sha1, const char *msg,
+ struct strbuf *sb_log_file)
{
int logfd, result, oflags = O_APPEND | O_WRONLY;
- char log_file[PATH_MAX];
+ char *log_file;
if (log_all_ref_updates < 0)
log_all_ref_updates = !is_bare_repository();
- result = log_ref_setup(refname, log_file, sizeof(log_file));
+ result = log_ref_setup(refname, sb_log_file);
if (result)
return result;
+ log_file = sb_log_file->buf;
+ /* make sure the rest of the function can't change "log_file" */
+ sb_log_file = NULL;
logfd = open(log_file, oflags);
if (logfd < 0)
@@ -3016,6 +3040,15 @@ static int log_ref_write(const char *refname, const unsigned char *old_sha1,
return 0;
}
+static int log_ref_write(const char *refname, const unsigned char *old_sha1,
+ const unsigned char *new_sha1, const char *msg)
+{
+ struct strbuf sb = STRBUF_INIT;
+ int ret = log_ref_write_1(refname, old_sha1, new_sha1, msg, &sb);
+ strbuf_release(&sb);
+ return ret;
+}
+
int is_branch(const char *refname)
{
return !strcmp(refname, "HEAD") || starts_with(refname, "refs/heads/");
diff --git a/refs.h b/refs.h
index cf642e6dd..6d7d9b40f 100644
--- a/refs.h
+++ b/refs.h
@@ -191,7 +191,7 @@ extern int peel_ref(const char *refname, unsigned char *sha1);
/*
* Setup reflog before using. Set errno to something meaningful on failure.
*/
-int log_ref_setup(const char *refname, char *logfile, int bufsize);
+int log_ref_setup(const char *refname, struct strbuf *logfile);
/** Reads log for the value of ref during at_time. **/
extern int read_ref_at(const char *refname, unsigned int flags,
diff --git a/run-command.c b/run-command.c
index aad03ab70..4d73e90fa 100644
--- a/run-command.c
+++ b/run-command.c
@@ -795,9 +795,9 @@ int finish_async(struct async *async)
#endif
}
-char *find_hook(const char *name)
+const char *find_hook(const char *name)
{
- char *path = git_path("hooks/%s", name);
+ const char *path = git_path("hooks/%s", name);
if (access(path, X_OK) < 0)
path = NULL;
diff --git a/run-command.h b/run-command.h
index 263b9662a..1103805af 100644
--- a/run-command.h
+++ b/run-command.h
@@ -52,7 +52,7 @@ int start_command(struct child_process *);
int finish_command(struct child_process *);
int run_command(struct child_process *);
-extern char *find_hook(const char *name);
+extern const char *find_hook(const char *name);
LAST_ARG_MUST_BE_NULL
extern int run_hook_le(const char *const *env, const char *name, ...);
extern int run_hook_ve(const char *const *env, const char *name, va_list args);
diff --git a/setup.c b/setup.c
index 979b13f0c..fb6186049 100644
--- a/setup.c
+++ b/setup.c
@@ -224,6 +224,36 @@ void verify_non_filename(const char *prefix, const char *arg)
"'git <command> [<revision>...] -- [<file>...]'", arg);
}
+int get_common_dir(struct strbuf *sb, const char *gitdir)
+{
+ struct strbuf data = STRBUF_INIT;
+ struct strbuf path = STRBUF_INIT;
+ const char *git_common_dir = getenv(GIT_COMMON_DIR_ENVIRONMENT);
+ int ret = 0;
+ if (git_common_dir) {
+ strbuf_addstr(sb, git_common_dir);
+ return 1;
+ }
+ strbuf_addf(&path, "%s/commondir", gitdir);
+ if (file_exists(path.buf)) {
+ if (strbuf_read_file(&data, path.buf, 0) <= 0)
+ die_errno(_("failed to read %s"), path.buf);
+ while (data.len && (data.buf[data.len - 1] == '\n' ||
+ data.buf[data.len - 1] == '\r'))
+ data.len--;
+ data.buf[data.len] = '\0';
+ strbuf_reset(&path);
+ if (!is_absolute_path(data.buf))
+ strbuf_addf(&path, "%s/", gitdir);
+ strbuf_addbuf(&path, &data);
+ strbuf_addstr(sb, real_path(path.buf));
+ ret = 1;
+ } else
+ strbuf_addstr(sb, gitdir);
+ strbuf_release(&data);
+ strbuf_release(&path);
+ return ret;
+}
/*
* Test if it looks like we're at a git directory.
@@ -238,31 +268,40 @@ void verify_non_filename(const char *prefix, const char *arg)
*/
int is_git_directory(const char *suspect)
{
- char path[PATH_MAX];
- size_t len = strlen(suspect);
+ struct strbuf path = STRBUF_INIT;
+ int ret = 0;
+ size_t len;
- if (PATH_MAX <= len + strlen("/objects"))
- die("Too long path: %.*s", 60, suspect);
- strcpy(path, suspect);
+ /* Check worktree-related signatures */
+ strbuf_addf(&path, "%s/HEAD", suspect);
+ if (validate_headref(path.buf))
+ goto done;
+
+ strbuf_reset(&path);
+ get_common_dir(&path, suspect);
+ len = path.len;
+
+ /* Check non-worktree-related signatures */
if (getenv(DB_ENVIRONMENT)) {
if (access(getenv(DB_ENVIRONMENT), X_OK))
- return 0;
+ goto done;
}
else {
- strcpy(path + len, "/objects");
- if (access(path, X_OK))
- return 0;
+ strbuf_setlen(&path, len);
+ strbuf_addstr(&path, "/objects");
+ if (access(path.buf, X_OK))
+ goto done;
}
- strcpy(path + len, "/refs");
- if (access(path, X_OK))
- return 0;
-
- strcpy(path + len, "/HEAD");
- if (validate_headref(path))
- return 0;
+ strbuf_setlen(&path, len);
+ strbuf_addstr(&path, "/refs");
+ if (access(path.buf, X_OK))
+ goto done;
- return 1;
+ ret = 1;
+done:
+ strbuf_release(&path);
+ return ret;
}
int is_inside_git_dir(void)
@@ -304,9 +343,28 @@ void setup_work_tree(void)
initialized = 1;
}
+static int check_repo_format(const char *var, const char *value, void *cb)
+{
+ if (strcmp(var, "core.repositoryformatversion") == 0)
+ repository_format_version = git_config_int(var, value);
+ else if (strcmp(var, "core.sharedrepository") == 0)
+ shared_repository = git_config_perm(var, value);
+ return 0;
+}
+
static int check_repository_format_gently(const char *gitdir, int *nongit_ok)
{
- char repo_config[PATH_MAX+1];
+ struct strbuf sb = STRBUF_INIT;
+ const char *repo_config;
+ config_fn_t fn;
+ int ret = 0;
+
+ if (get_common_dir(&sb, gitdir))
+ fn = check_repo_format;
+ else
+ fn = check_repository_format_version;
+ strbuf_addstr(&sb, "/config");
+ repo_config = sb.buf;
/*
* git_config() can't be used here because it calls git_pathdup()
@@ -317,8 +375,7 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)
* Use a gentler version of git_config() to check if this repo
* is a good one.
*/
- snprintf(repo_config, PATH_MAX, "%s/config", gitdir);
- git_config_early(check_repository_format_version, NULL, repo_config);
+ git_config_early(fn, NULL, repo_config);
if (GIT_REPO_VERSION < repository_format_version) {
if (!nongit_ok)
die ("Expected git repo version <= %d, found %d",
@@ -327,9 +384,21 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)
GIT_REPO_VERSION, repository_format_version);
warning("Please upgrade Git");
*nongit_ok = -1;
- return -1;
+ ret = -1;
}
- return 0;
+ strbuf_release(&sb);
+ return ret;
+}
+
+static void update_linked_gitdir(const char *gitfile, const char *gitdir)
+{
+ struct strbuf path = STRBUF_INIT;
+ struct stat st;
+
+ strbuf_addf(&path, "%s/gitfile", gitdir);
+ if (stat(path.buf, &st) || st.st_mtime + 24 * 3600 < time(NULL))
+ write_file(path.buf, 0, "%s\n", gitfile);
+ strbuf_release(&path);
}
/*
@@ -380,6 +449,8 @@ const char *read_gitfile(const char *path)
if (!is_git_directory(dir))
die("Not a git repository: %s", dir);
+
+ update_linked_gitdir(path, dir);
path = real_path(dir);
free(buf);
@@ -799,11 +870,10 @@ int git_config_perm(const char *var, const char *value)
int check_repository_format_version(const char *var, const char *value, void *cb)
{
- if (strcmp(var, "core.repositoryformatversion") == 0)
- repository_format_version = git_config_int(var, value);
- else if (strcmp(var, "core.sharedrepository") == 0)
- shared_repository = git_config_perm(var, value);
- else if (strcmp(var, "core.bare") == 0) {
+ int ret = check_repo_format(var, value, cb);
+ if (ret)
+ return ret;
+ if (strcmp(var, "core.bare") == 0) {
is_bare_repository_cfg = git_config_bool(var, value);
if (is_bare_repository_cfg == 1)
inside_work_tree = -1;
diff --git a/sha1_file.c b/sha1_file.c
index f860d6774..47c4929eb 100644
--- a/sha1_file.c
+++ b/sha1_file.c
@@ -405,7 +405,7 @@ void add_to_alternates_file(const char *reference)
{
struct lock_file *lock = xcalloc(1, sizeof(struct lock_file));
int fd = hold_lock_file_for_append(lock, git_path("objects/info/alternates"), LOCK_DIE_ON_ERROR);
- char *alt = mkpath("%s\n", reference);
+ const char *alt = mkpath("%s\n", reference);
write_or_die(fd, alt, strlen(alt));
if (commit_lock_file(lock))
die("could not close alternates file");
diff --git a/submodule.c b/submodule.c
index c0e6c81fc..d491e6a77 100644
--- a/submodule.c
+++ b/submodule.c
@@ -1100,16 +1100,11 @@ void connect_work_tree_and_git_dir(const char *work_tree, const char *git_dir)
struct strbuf file_name = STRBUF_INIT;
struct strbuf rel_path = STRBUF_INIT;
const char *real_work_tree = xstrdup(real_path(work_tree));
- FILE *fp;
/* Update gitfile */
strbuf_addf(&file_name, "%s/.git", work_tree);
- fp = fopen(file_name.buf, "w");
- if (!fp)
- die(_("Could not create git link %s"), file_name.buf);
- fprintf(fp, "gitdir: %s\n", relative_path(git_dir, real_work_tree,
- &rel_path));
- fclose(fp);
+ write_file(file_name.buf, 1, "gitdir: %s\n",
+ relative_path(git_dir, real_work_tree, &rel_path));
/* Update core.worktree setting */
strbuf_reset(&file_name);
diff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh
index c0143a0a7..93605f42f 100755
--- a/t/t0060-path-utils.sh
+++ b/t/t0060-path-utils.sh
@@ -19,6 +19,14 @@ relative_path() {
"test \"\$(test-path-utils relative_path '$1' '$2')\" = '$expected'"
}
+test_git_path() {
+ test_expect_success "git-path $1 $2 => $3" "
+ $1 git rev-parse --git-path $2 >actual &&
+ echo $3 >expect &&
+ test_cmp expect actual
+ "
+}
+
# On Windows, we are using MSYS's bash, which mangles the paths.
# Absolute paths are anchored at the MSYS installation directory,
# which means that the path / accounts for this many characters:
@@ -244,4 +252,32 @@ relative_path "<null>" "<empty>" ./
relative_path "<null>" "<null>" ./
relative_path "<null>" /foo/a/b ./
+test_git_path A=B info/grafts .git/info/grafts
+test_git_path GIT_GRAFT_FILE=foo info/grafts foo
+test_git_path GIT_GRAFT_FILE=foo info/////grafts foo
+test_git_path GIT_INDEX_FILE=foo index foo
+test_git_path GIT_INDEX_FILE=foo index/foo .git/index/foo
+test_git_path GIT_INDEX_FILE=foo index2 .git/index2
+test_expect_success 'setup fake objects directory foo' 'mkdir foo'
+test_git_path GIT_OBJECT_DIRECTORY=foo objects foo
+test_git_path GIT_OBJECT_DIRECTORY=foo objects/foo foo/foo
+test_git_path GIT_OBJECT_DIRECTORY=foo objects2 .git/objects2
+test_expect_success 'setup common repository' 'git --git-dir=bar init'
+test_git_path GIT_COMMON_DIR=bar index .git/index
+test_git_path GIT_COMMON_DIR=bar HEAD .git/HEAD
+test_git_path GIT_COMMON_DIR=bar logs/HEAD .git/logs/HEAD
+test_git_path GIT_COMMON_DIR=bar objects bar/objects
+test_git_path GIT_COMMON_DIR=bar objects/bar bar/objects/bar
+test_git_path GIT_COMMON_DIR=bar info/exclude bar/info/exclude
+test_git_path GIT_COMMON_DIR=bar info/grafts bar/info/grafts
+test_git_path GIT_COMMON_DIR=bar info/sparse-checkout .git/info/sparse-checkout
+test_git_path GIT_COMMON_DIR=bar remotes/bar bar/remotes/bar
+test_git_path GIT_COMMON_DIR=bar branches/bar bar/branches/bar
+test_git_path GIT_COMMON_DIR=bar logs/refs/heads/master bar/logs/refs/heads/master
+test_git_path GIT_COMMON_DIR=bar refs/heads/master bar/refs/heads/master
+test_git_path GIT_COMMON_DIR=bar hooks/me bar/hooks/me
+test_git_path GIT_COMMON_DIR=bar config bar/config
+test_git_path GIT_COMMON_DIR=bar packed-refs bar/packed-refs
+test_git_path GIT_COMMON_DIR=bar shallow bar/shallow
+
test_done
diff --git a/t/t1501-worktree.sh b/t/t1501-worktree.sh
index 8f36aa9fc..cc5b870e5 100755
--- a/t/t1501-worktree.sh
+++ b/t/t1501-worktree.sh
@@ -346,4 +346,81 @@ test_expect_success 'relative $GIT_WORK_TREE and git subprocesses' '
test_cmp expected actual
'
+test_expect_success 'Multi-worktree setup' '
+ mkdir work &&
+ mkdir -p repo.git/repos/foo &&
+ cp repo.git/HEAD repo.git/index repo.git/repos/foo &&
+ test_might_fail cp repo.git/sharedindex.* repo.git/repos/foo &&
+ sane_unset GIT_DIR GIT_CONFIG GIT_WORK_TREE
+'
+
+test_expect_success 'GIT_DIR set (1)' '
+ echo "gitdir: repo.git/repos/foo" >gitfile &&
+ echo ../.. >repo.git/repos/foo/commondir &&
+ (
+ cd work &&
+ GIT_DIR=../gitfile git rev-parse --git-common-dir >actual &&
+ test-path-utils real_path "$TRASH_DIRECTORY/repo.git" >expect &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'GIT_DIR set (2)' '
+ echo "gitdir: repo.git/repos/foo" >gitfile &&
+ echo "$(pwd)/repo.git" >repo.git/repos/foo/commondir &&
+ (
+ cd work &&
+ GIT_DIR=../gitfile git rev-parse --git-common-dir >actual &&
+ test-path-utils real_path "$TRASH_DIRECTORY/repo.git" >expect &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'Auto discovery' '
+ echo "gitdir: repo.git/repos/foo" >.git &&
+ echo ../.. >repo.git/repos/foo/commondir &&
+ (
+ cd work &&
+ git rev-parse --git-common-dir >actual &&
+ test-path-utils real_path "$TRASH_DIRECTORY/repo.git" >expect &&
+ test_cmp expect actual &&
+ echo haha >data1 &&
+ git add data1 &&
+ git ls-files --full-name :/ | grep data1 >actual &&
+ echo work/data1 >expect &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success '$GIT_DIR/common overrides core.worktree' '
+ mkdir elsewhere &&
+ git --git-dir=repo.git config core.worktree "$TRASH_DIRECTORY/elsewhere" &&
+ echo "gitdir: repo.git/repos/foo" >.git &&
+ echo ../.. >repo.git/repos/foo/commondir &&
+ (
+ cd work &&
+ git rev-parse --git-common-dir >actual &&
+ test-path-utils real_path "$TRASH_DIRECTORY/repo.git" >expect &&
+ test_cmp expect actual &&
+ echo haha >data2 &&
+ git add data2 &&
+ git ls-files --full-name :/ | grep data2 >actual &&
+ echo work/data2 >expect &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success '$GIT_WORK_TREE overrides $GIT_DIR/common' '
+ echo "gitdir: repo.git/repos/foo" >.git &&
+ echo ../.. >repo.git/repos/foo/commondir &&
+ (
+ cd work &&
+ echo haha >data3 &&
+ git --git-dir=../.git --work-tree=. add data3 &&
+ git ls-files --full-name -- :/ | grep data3 >actual &&
+ echo data3 >expect &&
+ test_cmp expect actual
+ )
+'
+
test_done
diff --git a/t/t1510-repo-setup.sh b/t/t1510-repo-setup.sh
index e1b2a99f1..33c1a587b 100755
--- a/t/t1510-repo-setup.sh
+++ b/t/t1510-repo-setup.sh
@@ -106,6 +106,7 @@ setup_env () {
expect () {
cat >"$1/expected" <<-EOF
setup: git_dir: $2
+ setup: git_common_dir: $2
setup: worktree: $3
setup: cwd: $4
setup: prefix: $5
diff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh
new file mode 100755
index 000000000..f8e4df481
--- /dev/null
+++ b/t/t2025-checkout-to.sh
@@ -0,0 +1,129 @@
+#!/bin/sh
+
+test_description='test git checkout --to'
+
+. ./test-lib.sh
+
+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' '
+ mkdir -p existing/subtree &&
+ test_must_fail git checkout --detach --to existing master
+'
+
+test_expect_success 'checkout --to an existing empty worktree' '
+ mkdir existing_empty &&
+ git checkout --detach --to existing_empty master
+'
+
+test_expect_success 'checkout --to refuses to checkout locked branch' '
+ test_must_fail git checkout --to zere master &&
+ ! test -d zere &&
+ ! test -d .git/worktrees/zere
+'
+
+test_expect_success 'checkout --to a new worktree' '
+ git rev-parse HEAD >expect &&
+ git checkout --detach --to here master &&
+ (
+ cd here &&
+ test_cmp ../init.t init.t &&
+ test_must_fail git symbolic-ref HEAD &&
+ git rev-parse HEAD >actual &&
+ test_cmp ../expect actual &&
+ git fsck
+ )
+'
+
+test_expect_success 'checkout --to a new worktree from a subdir' '
+ (
+ mkdir sub &&
+ cd sub &&
+ git checkout --detach --to here master &&
+ cd here &&
+ test_cmp ../../init.t init.t
+ )
+'
+
+test_expect_success 'checkout --to from a linked checkout' '
+ (
+ cd here &&
+ git checkout --detach --to 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 &&
+ (
+ cd there &&
+ test_cmp ../init.t init.t &&
+ git symbolic-ref HEAD >actual &&
+ echo refs/heads/newmaster >expect &&
+ test_cmp expect actual &&
+ git fsck
+ )
+'
+
+test_expect_success 'die the same branch is already checked out' '
+ (
+ cd here &&
+ test_must_fail git checkout newmaster
+ )
+'
+
+test_expect_success 'not die the same branch is already checked out' '
+ (
+ cd here &&
+ git checkout --ignore-other-worktrees --to anothernewmaster newmaster
+ )
+'
+
+test_expect_success 'not die on re-checking out current branch' '
+ (
+ cd there &&
+ git checkout newmaster
+ )
+'
+
+test_expect_success 'checkout --to from a bare repo' '
+ (
+ git clone --bare . bare &&
+ cd bare &&
+ git checkout --to ../there2 -b bare-master master
+ )
+'
+
+test_expect_success 'checkout from a bare repo without --to' '
+ (
+ cd bare &&
+ test_must_fail git checkout master
+ )
+'
+
+test_expect_success 'checkout with grafts' '
+ test_when_finished rm .git/info/grafts &&
+ test_commit abc &&
+ SHA1=`git rev-parse HEAD` &&
+ test_commit def &&
+ test_commit xyz &&
+ echo "`git rev-parse HEAD` $SHA1" >.git/info/grafts &&
+ cat >expected <<-\EOF &&
+ xyz
+ abc
+ EOF
+ git log --format=%s -2 >actual &&
+ test_cmp expected actual &&
+ git checkout --detach --to grafted master &&
+ git --git-dir=grafted/.git log --format=%s -2 >actual &&
+ test_cmp expected actual
+'
+
+test_done
diff --git a/t/t2026-prune-linked-checkouts.sh b/t/t2026-prune-linked-checkouts.sh
new file mode 100755
index 000000000..1821a480c
--- /dev/null
+++ b/t/t2026-prune-linked-checkouts.sh
@@ -0,0 +1,96 @@
+#!/bin/sh
+
+test_description='prune $GIT_DIR/worktrees'
+
+. ./test-lib.sh
+
+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 'prune files inside $GIT_DIR/worktrees' '
+ mkdir .git/worktrees &&
+ : >.git/worktrees/abc &&
+ git prune --worktrees --verbose >actual &&
+ cat >expect <<EOF &&
+Removing worktrees/abc: not a valid directory
+EOF
+ test_i18ncmp expect actual &&
+ ! test -f .git/worktrees/abc &&
+ ! test -d .git/worktrees
+'
+
+test_expect_success 'prune directories without gitdir' '
+ mkdir -p .git/worktrees/def/abc &&
+ : >.git/worktrees/def/def &&
+ cat >expect <<EOF &&
+Removing worktrees/def: gitdir file does not exist
+EOF
+ git prune --worktrees --verbose >actual &&
+ test_i18ncmp expect actual &&
+ ! test -d .git/worktrees/def &&
+ ! test -d .git/worktrees
+'
+
+test_expect_success SANITY 'prune directories with unreadable gitdir' '
+ mkdir -p .git/worktrees/def/abc &&
+ : >.git/worktrees/def/def &&
+ : >.git/worktrees/def/gitdir &&
+ chmod u-r .git/worktrees/def/gitdir &&
+ git prune --worktrees --verbose >actual &&
+ test_i18ngrep "Removing worktrees/def: unable to read gitdir file" actual &&
+ ! test -d .git/worktrees/def &&
+ ! test -d .git/worktrees
+'
+
+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 &&
+ test_i18ngrep "Removing worktrees/def: invalid gitdir file" actual &&
+ ! test -d .git/worktrees/def &&
+ ! test -d .git/worktrees
+'
+
+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 &&
+ test_i18ngrep "Removing worktrees/def: gitdir file points to non-existent location" actual &&
+ ! test -d .git/worktrees/def &&
+ ! test -d .git/worktrees
+'
+
+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 &&
+ test -d .git/worktrees/ghi
+'
+
+test_expect_success 'not prune recent checkouts' '
+ test_when_finished rm -r .git/worktrees &&
+ mkdir zz &&
+ mkdir -p .git/worktrees/jlm &&
+ echo "$(pwd)"/zz >.git/worktrees/jlm/gitdir &&
+ rmdir zz &&
+ git prune --worktrees --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 &&
+ test -d .git/worktrees/nop
+'
+
+test_done
diff --git a/t/t7410-submodule-checkout-to.sh b/t/t7410-submodule-checkout-to.sh
new file mode 100755
index 000000000..8f30aed6c
--- /dev/null
+++ b/t/t7410-submodule-checkout-to.sh
@@ -0,0 +1,50 @@
+#!/bin/sh
+
+test_description='Combination of submodules and multiple workdirs'
+
+. ./test-lib.sh
+
+base_path=$(pwd -P)
+
+test_expect_success 'setup: make origin' \
+ 'mkdir -p origin/sub && ( cd origin/sub && git init &&
+ echo file1 >file1 &&
+ git add file1 &&
+ git commit -m file1 ) &&
+ mkdir -p origin/main && ( cd origin/main && git init &&
+ git submodule add ../sub &&
+ git commit -m "add sub" ) &&
+ ( cd origin/sub &&
+ echo file1updated >file1 &&
+ git add file1 &&
+ git commit -m "file1 updated" ) &&
+ ( cd origin/main/sub && git pull ) &&
+ ( cd origin/main &&
+ git add sub &&
+ git commit -m "sub updated" )'
+
+test_expect_success 'setup: clone' \
+ 'mkdir clone && ( cd clone &&
+ git clone --recursive "$base_path/origin/main")'
+
+rev1_hash_main=$(git --git-dir=origin/main/.git show --pretty=format:%h -q "HEAD~1")
+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")'
+
+test_expect_failure 'can see submodule diffs just after checkout' \
+ '(cd default_checkout/main && git diff --submodule master"^!" | grep "file1 updated")'
+
+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") &&
+ (cd fully_cloned_submodule/main && git submodule update)'
+
+test_expect_success 'can see submodule diffs after independed cloning' \
+ '(cd fully_cloned_submodule/main && git diff --submodule master"^!" | grep "file1 updated")'
+
+test_done
diff --git a/templates/hooks--applypatch-msg.sample b/templates/hooks--applypatch-msg.sample
index 8b2a2fe84..a5d7b84a6 100755
--- a/templates/hooks--applypatch-msg.sample
+++ b/templates/hooks--applypatch-msg.sample
@@ -10,6 +10,6 @@
# To enable this hook, rename this file to "applypatch-msg".
. git-sh-setup
-test -x "$GIT_DIR/hooks/commit-msg" &&
- exec "$GIT_DIR/hooks/commit-msg" ${1+"$@"}
+commitmsg="$(git rev-parse --git-path hooks/commit-msg)"
+test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"}
:
diff --git a/templates/hooks--pre-applypatch.sample b/templates/hooks--pre-applypatch.sample
index b1f187c2e..4142082bc 100755
--- a/templates/hooks--pre-applypatch.sample
+++ b/templates/hooks--pre-applypatch.sample
@@ -9,6 +9,6 @@
# To enable this hook, rename this file to "pre-applypatch".
. git-sh-setup
-test -x "$GIT_DIR/hooks/pre-commit" &&
- exec "$GIT_DIR/hooks/pre-commit" ${1+"$@"}
+precommit="$(git rev-parse --git-path hooks/pre-commit)"
+test -x "$precommit" && exec "$precommit" ${1+"$@"}
:
diff --git a/trace.c b/trace.c
index 1dc5c7c91..3c3bd8fc9 100644
--- a/trace.c
+++ b/trace.c
@@ -310,6 +310,7 @@ void trace_repo_setup(const char *prefix)
prefix = "(null)";
trace_printf_key(&key, "setup: git_dir: %s\n", quote_crnl(get_git_dir()));
+ trace_printf_key(&key, "setup: git_common_dir: %s\n", quote_crnl(get_git_common_dir()));
trace_printf_key(&key, "setup: worktree: %s\n", quote_crnl(git_work_tree));
trace_printf_key(&key, "setup: cwd: %s\n", quote_crnl(cwd));
trace_printf_key(&key, "setup: prefix: %s\n", quote_crnl(prefix));
diff --git a/transport.c b/transport.c
index eca9b8c81..f080e93dc 100644
--- a/transport.c
+++ b/transport.c
@@ -283,7 +283,6 @@ static int write_one_ref(const char *name, const unsigned char *sha1,
{
struct strbuf *buf = data;
int len = buf->len;
- FILE *f;
/* when called via for_each_ref(), flags is non-zero */
if (flags && !starts_with(name, "refs/heads/") &&
@@ -292,10 +291,9 @@ static int write_one_ref(const char *name, const unsigned char *sha1,
strbuf_addstr(buf, name);
if (safe_create_leading_directories(buf->buf) ||
- !(f = fopen(buf->buf, "w")) ||
- fprintf(f, "%s\n", sha1_to_hex(sha1)) < 0 ||
- fclose(f))
- return error("problems writing temporary file %s", buf->buf);
+ write_file(buf->buf, 0, "%s\n", sha1_to_hex(sha1)))
+ return error("problems writing temporary file %s: %s",
+ buf->buf, strerror(errno));
strbuf_setlen(buf, len);
return 0;
}
diff --git a/wrapper.c b/wrapper.c
index d5a6cef2b..c1a663fd5 100644
--- a/wrapper.c
+++ b/wrapper.c
@@ -564,3 +564,34 @@ char *xgetcwd(void)
die_errno(_("unable to get current working directory"));
return strbuf_detach(&sb, NULL);
}
+
+int write_file(const char *path, int fatal, const char *fmt, ...)
+{
+ struct strbuf sb = STRBUF_INIT;
+ va_list params;
+ int fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0666);
+ if (fd < 0) {
+ if (fatal)
+ die_errno(_("could not open %s for writing"), path);
+ return -1;
+ }
+ va_start(params, fmt);
+ strbuf_vaddf(&sb, fmt, params);
+ va_end(params);
+ if (write_in_full(fd, sb.buf, sb.len) != sb.len) {
+ int err = errno;
+ close(fd);
+ strbuf_release(&sb);
+ errno = err;
+ if (fatal)
+ die_errno(_("could not write to %s"), path);
+ return -1;
+ }
+ strbuf_release(&sb);
+ if (close(fd)) {
+ if (fatal)
+ die_errno(_("could not close %s"), path);
+ return -1;
+ }
+ return 0;
+}