Merge branch 'nd/multiple-work-trees'
authorJunio C Hamano <gitster@pobox.com>
Mon, 11 May 2015 21:23:39 +0000 (14:23 -0700)
committerJunio C Hamano <gitster@pobox.com>
Mon, 11 May 2015 21:23:39 +0000 (14:23 -0700)
A replacement for contrib/workdir/git-new-workdir that does not
rely on symbolic links and make sharing of objects and refs safer
by making the borrowee and borrowers aware of each other.

* nd/multiple-work-trees: (41 commits)
prune --worktrees: fix expire vs worktree existence condition
t1501: fix test with split index
t2026: fix broken &&-chain
t2026 needs procondition SANITY
git-checkout.txt: a note about multiple checkout support for submodules
checkout: add --ignore-other-wortrees
checkout: pass whole struct to parse_branchname_arg instead of individual flags
git-common-dir: make "modules/" per-working-directory directory
checkout: do not fail if target is an empty directory
t2025: add a test to make sure grafts is working from a linked checkout
checkout: don't require a work tree when checking out into a new one
git_path(): keep "info/sparse-checkout" per work-tree
count-objects: report unused files in $GIT_DIR/worktrees/...
gc: support prune --worktrees
gc: factor out gc.pruneexpire parsing code
gc: style change -- no SP before closing parenthesis
checkout: clean up half-prepared directories in --to mode
checkout: reject if the branch is already checked out elsewhere
prune: strategies for linked checkouts
checkout: support checking out into a new working directory
...

52 files changed:
Documentation/config.txt
Documentation/git-checkout.txt
Documentation/git-prune.txt
Documentation/git-rev-parse.txt
Documentation/git.txt
Documentation/gitrepository-layout.txt
builtin/branch.c
builtin/checkout.c
builtin/clone.c
builtin/commit.c
builtin/count-objects.c
builtin/fetch.c
builtin/fsck.c
builtin/gc.c
builtin/init-db.c
builtin/prune.c
builtin/receive-pack.c
builtin/remote.c
builtin/repack.c
builtin/rev-parse.c
cache.h
daemon.c
environment.c
fast-import.c
git-am.sh
git-pull.sh
git-rebase--interactive.sh
git-rebase--merge.sh
git-rebase.sh
git-sh-setup.sh
git-stash.sh
git.c
notes-merge.c
path.c
refs.c
refs.h
run-command.c
run-command.h
setup.c
sha1_file.c
submodule.c
t/t0060-path-utils.sh
t/t1501-worktree.sh
t/t1510-repo-setup.sh
t/t2025-checkout-to.sh [new file with mode: 0755]
t/t2026-prune-linked-checkouts.sh [new file with mode: 0755]
t/t7410-submodule-checkout-to.sh [new file with mode: 0755]
templates/hooks--applypatch-msg.sample
templates/hooks--pre-applypatch.sample
trace.c
transport.c
wrapper.c
index 2e5ceaf71974b1e5404de79103648da1829eaf42..948b8b0e5c8e8639e989d2f14dbd310fe146278b 100644 (file)
@@ -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
index d5041082e88349d7c814ae1c036b15841f47a6d0..d263a5652f06777b987eefae4f14f2003d53448b 100644 (file)
@@ -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
 --------
 
index 7a493c80f776092265abd4fb2575639e683bcf5c..1cf3bed4ab7ab19364c6801fbc73920170dfd1e4 100644 (file)
@@ -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
index d6de42f74efeedda228a97746c123de6e3e1f0ab..97fc703f4c0db968151b39de537fc641e5a19c32 100644 (file)
@@ -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
index c71e818cf445d55e793229dd9cb46777e5f53a98..2789da4f8fb4b06c26cfc27abfecf01392e75ae5 100644 (file)
@@ -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'::
index 79653f313474fa658f072c97d8744e5afccb77d7..7173b38830015ba1be09772e80cf3973c10bab69 100644 (file)
@@ -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],
index 1d150378e91cd47c4cd555bfc858ec0b7ea9d604..258fe2ff9b6d08182b21b765191ee79e9a81f322 100644 (file)
@@ -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));
index 4aad49aaa1d984bb6852361118932db0c984491b..2f92328db46b4ff81e32b32339ad830c4b76688b 100644 (file)
@@ -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;
        }
index 53a2e5af35ebfc37b378442a238f1894e5bec962..166a645e2dcedc66b1d2f20ca01010a5ae2ee9d8 100644 (file)
@@ -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);
index da79ac4bc7a7247017e2f952b35642a1976c8101..310674cfd0f25c52b9952678eeb3ee164fd97fea 100644 (file)
@@ -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
index e47ef0b1af06a1abd63f7818c363db6c859a9bf6..ad0c79954aa0dfb8b9918500d08dd841d19d4ab3 100644 (file)
@@ -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);
index f9512652cf1389a770bd94fd170d123206e024c5..7910419c93275c58f4881202905ae1f24bfe8c91 100644 (file)
@@ -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)
index 0c757862e8cd414c087b27a608281b66c8608a4b..4783896fd65f0206f3382bddc68a0dad8b5dd896 100644 (file)
@@ -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;
                        }
index 5c634afc0022c0ea7a8cd8c81727935ad79a6333..36fe33300f644fc9c7e5139b452a9703548ba2a2 100644 (file)
@@ -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]);
 
index ab9f86b8890ed99547144096ff56c3cbef780505..4335738135df32aeea3712cbdae88bd862015969 100644 (file)
@@ -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)
index 17094ad954c9da68bc5e251dce1a87ec67c00146..0c73246c721b3307f0d68b4e58f7c684aebce2e9 100644 (file)
@@ -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++;
index 5292bb5a506805778c6b9c164523c80a44da3edd..d2ec52bca983d9dfe1094cffe6049c902049e4d6 100644 (file)
@@ -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) {
index 5d3ab906bc7ef6cc8cc9c65a3b86cf4c1ff443fb..ad57fc984edc8a7ea7f3784e14068e247a5d27ad 100644 (file)
@@ -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++)
index f2edeb0f4ca2b81246ee37d7ca5f53f58161ad6f..af7340c7bafbfbbf991f782139775e18b6ac570a 100644 (file)
@@ -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,
index 3626c61da67abbe418e492c7828f6e153f6055d8..4d10dd9545af70c32f9e19533b2cfc9b00b7f00a 100644 (file)
@@ -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 b34447ffcf3a97ee1cec76ce090f274f604b926d..771b775621595e312484295abd9ca768ece6f6ea 100644 (file)
--- 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);
index 9ee21877cd952a7aa47c793d877174d858488794..ac2bc852d1dfa20783de63c3dceec83a79e4df6a 100644 (file)
--- 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));
index a40044c3bf8040a36ddfb5c260b389ec63683abd..61c685b8d93091666891f0091d5005fe62543fc2 100644 (file)
@@ -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)
index e78ca107b3d66d7e537c86eb10dc7503be8681b9..6378726993445694581ac36d6351f817fe488c72 100644 (file)
@@ -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,
index a67d0f98989706fa69df4cf7d6242ad9531eb337..761befbd37088c176a65e4d3a12e8a8281e47553 100755 (executable)
--- 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
 
index 4d4fc77b05648c7d2d76ae932b7d68cdf411d364..ad442264cbae0003dc548b85769cbeb2107c7d5f 100755 (executable)
@@ -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
index f7deeb096e24f4de69bdfe08f0aa35ebf170577a..08e5d86fe5b5eda37f2ab44c3cce6dabb42c54e1 100644 (file)
@@ -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."
index d3fb67d75bd3cdfebd048594cdb67eb179b1c435..2cc2a6d2734c0ef12222004f22d717f4f724d82f 100644 (file)
@@ -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.
 }
index 55da9db818665f39ed205d3c77352987e3ca9963..fb935a03f3ababaa6359d6a7a4deafcbaa9f15c2 100755 (executable)
@@ -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
 }
index c42c6e6365090e22ec7892baa51b919f2898e956..4691fbcb64fe7ecbc58930bb96d4e6b28e2b87a7 100644 (file)
@@ -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"
index d4cf818be9488f1b94fdf4766a8f73db6bfd1029..6846b18dbc007a8b08c6f5a77fe568fb8625c28f 100755 (executable)
@@ -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 42a4ee57843f569fb754121f01bb8c46feee2fd3..44374b1d9b6b73b3669f0f851bee058f45ee4f32 100644 (file)
--- 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 },
index 109ff4ef410b77a814cca282f728f1b88948e4ec..0b2b82c41fc043a48e9bf458c4f75f992ead7175 100644 (file)
@@ -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 595da81ca67096bae9592d9455bdade442b92628..586f2c90a3c0def34016a692a14e335a46c015ed 100644 (file)
--- 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 47e4e5380a1e0fc04f8b81837c51c023f35871cf..0312f052575046f0dd539b19897b61eddc2862e0 100644 (file)
--- 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 cf642e6ddc438be77d64797a4dbcf790262a9fed..6d7d9b40f318119eea1e3240cca3a96d2c64e2a6 100644 (file)
--- 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,
index aad03ab705f301268980079282fcf370b9275c5a..4d73e90fad159184bfdd204b82dd8637ad28a955 100644 (file)
@@ -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;
 
index 263b9662adeba011adcd018f77b7ccbbcd53e94a..1103805af1b01e8396de15f48fbd06f75313fb0d 100644 (file)
@@ -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 979b13f0c6cd6bc3c265187e8ea79628bfe99f87..fb6186049d1d9af1e84b2468c069b346c5a24a3b 100644 (file)
--- 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;
index f860d67744784f69b83733757b9a894df3563de7..47c4929eb7060724411532dd0cec894067393d5e 100644 (file)
@@ -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");
index c0e6c81fc4656342fedeb4b5b68d9b938cb44b84..d491e6a7717ef0e1b9f7efd91becb024b98e0161 100644 (file)
@@ -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);
index c0143a0a70b7d3dc213b691aecea6b3fffbc5844..93605f42f27cef9ef3ffe381c84aea9f6f4be426 100755 (executable)
@@ -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
index 8f36aa9fc4d2bdd8b590e3241eccf88609db4ee2..cc5b870e5875bab8326393aea3a8d9f944210de6 100755 (executable)
@@ -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
index e1b2a99f105f8700e72974428c126ea049247194..33c1a587b3b9f28ecc824a17650467bdc6011bc6 100755 (executable)
@@ -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 (executable)
index 0000000..f8e4df4
--- /dev/null
@@ -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 (executable)
index 0000000..1821a48
--- /dev/null
@@ -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 (executable)
index 0000000..8f30aed
--- /dev/null
@@ -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
index 8b2a2fe84feaeaba56953d6d4d0d649b3cf755eb..a5d7b84a673458d14d9aab082183a1968c2c7492 100755 (executable)
@@ -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+"$@"}
 :
index b1f187c2e9acaba942639bca90a63c5b4f058967..4142082bcb939bbc17985a69ba748491ac6b62a5 100755 (executable)
@@ -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 1dc5c7c912d000699b753b1aab5c09a114e26557..3c3bd8fc98742075d6adc673ed2abc4f7e715b62 100644 (file)
--- 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));
index eca9b8c817bd723532d7f92da8cfa9f8ee82bb43..f080e93dcd50e5f2329b180bae93c59a461810f8 100644 (file)
@@ -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;
 }
index d5a6cef2be0fb13b262bed2e0b58bd58fbb5454d..c1a663fd592133b3e26314f3d598a8bca8bcceb1 100644 (file)
--- 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;
+}