{"thread":{"id":"37450","subject":"[PATCH 00/32] nd/multiple-work-trees cleanup","startedAt":"2014-08-30T08:33:30Z","lastAt":"2014-10-01T16:20:35Z","messageCount":134,"participants":["Nguyễn Thái Ngọc Duy","Eric Sunshine","Duy Nguyen","Philip Oakley","Marc Branchaud","Junio C Hamano","Scott Schmit","Johannes Sixt"],"isPatch":true,"patchVersion":1,"patchTotal":32},"messages":[{"id":"248607","messageId":"1409387642-24492-1-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":null,"subject":"[PATCH 00/32] nd/multiple-work-trees cleanup","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-08-30T08:33:30Z","receivedAt":"2014-08-30T08:33:30Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"This collapes some bug fix patches into the main ones, adds a few more\ntests to cover recent changes, and removes advice.checkoutLocked (when\nthings are controversal, probably best to go without them until they\nare settled). Diff against current version in 'pu'\n\ndiff --git a/Documentation/config.txt b/Documentation/config.txt\nindex b2c3388..57999fa 100644\n--- a/Documentation/config.txt\n+++ b/Documentation/config.txt\n@@ -201,10 +201,6 @@ advice.*::\n \trmHints::\n \t\tIn case of failure in the output of linkgit:git-rm[1],\n \t\tshow directions on how to proceed from the current state.\n-\tcheckoutLocked::\n-\t\tIn multiple checkout setup, attempting to checkout a\n-\t\tbranch already checked out elsewhere will fail. Show\n-\t\tsome useful options to proceed.\n --\n \n core.fileMode::\ndiff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt\nindex 6bd82af..c9a7d1a 100644\n--- a/Documentation/gitrepository-layout.txt\n+++ b/Documentation/gitrepository-layout.txt\n@@ -139,8 +139,8 @@ is often called 'detached HEAD.'  See linkgit:git-checkout[1]\n for details.\n \n config::\n-\tRepository specific configuration file. This file is ignored if\n-\t$GIT_COMMON_DIR is set and \"$GIT_COMMON_DIR/config\" will be\n+\tRepository specific configuration file. This file is ignored\n+\tif $GIT_COMMON_DIR is set and \"$GIT_COMMON_DIR/config\" will be\n \tused instead.\n \n branches::\n@@ -218,11 +218,10 @@ remotes::\n \t\"$GIT_COMMON_DIR/remotes\" will be used instead.\n \n logs::\n-\tRecords of changes made to refs are stored in this\n-\tdirectory.  See linkgit:git-update-ref[1]\n-\tfor more information. This directory is ignored if\n-\t$GIT_COMMON_DIR is set and \"$GIT_COMMON_DIR/logs\" will be used\n-\tinstead.\n+\tRecords of changes made to refs are stored in this directory.\n+\tSee linkgit:git-update-ref[1] for more information. This\n+\tdirectory is ignored $GIT_COMMON_DIR is set and\n+\t\"$GIT_COMMON_DIR/logs\" will be used instead.\n \n logs/refs/heads/`name`::\n \tRecords all changes made to the branch tip named `name`.\ndiff --git a/advice.c b/advice.c\nindex cf3b0f7..9b42033 100644\n--- a/advice.c\n+++ b/advice.c\n@@ -15,7 +15,6 @@ int advice_detached_head = 1;\n int advice_set_upstream_failure = 1;\n int advice_object_name_warning = 1;\n int advice_rm_hints = 1;\n-int advice_checkout_locked = 1;\n \n static struct {\n \tconst char *name;\n@@ -36,7 +35,6 @@ static struct {\n \t{ \"setupstreamfailure\", &advice_set_upstream_failure },\n \t{ \"objectnamewarning\", &advice_object_name_warning },\n \t{ \"rmhints\", &advice_rm_hints },\n-\t{ \"checkoutlocked\", &advice_checkout_locked },\n \n \t/* make this an alias for backward compatibility */\n \t{ \"pushnonfastforward\", &advice_push_update_rejected }\ndiff --git a/advice.h b/advice.h\nindex 935631d..5ecc6c1 100644\n--- a/advice.h\n+++ b/advice.h\n@@ -18,7 +18,6 @@ extern int advice_detached_head;\n extern int advice_set_upstream_failure;\n extern int advice_object_name_warning;\n extern int advice_rm_hints;\n-extern int advice_checkout_locked;\n \n int git_default_advice_config(const char *var, const char *value);\n __attribute__((format (printf, 1, 2)))\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 3dc416c..b88c646 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -862,7 +862,7 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,\n \tif (!new->commit)\n \t\tdie(_(\"no branch specified\"));\n \tif (file_exists(path))\n-\t\tdie(_(\"%s already exists\"), path);\n+\t\tdie(_(\"'%s' already exists\"), path);\n \n \tlen = strlen(path);\n \twhile (len && is_dir_sep(path[len - 1]))\n@@ -1038,16 +1038,7 @@ static void check_linked_checkout(struct branch_info *new, const char *id)\n \t\tstrbuf_rtrim(&gitdir);\n \t} else\n \t\tstrbuf_addstr(&gitdir, get_git_common_dir());\n-\tif (advice_checkout_locked)\n-\t\tdie(_(\"'%s' is already checked out at %s\\n\"\n-\t\t      \"Either go there and continue working, or detach HEAD using\\n\"\n-\t\t      \"    git checkout --detach [more options] %s\\n\"\n-\t\t      \"or create a new branch based off '%s' using\\n\"\n-\t\t      \"    git checkout -b <branch name> [more options] %s\\n\"\n-\t\t      \"or switch to another branch at the other checkout and retry.\"),\n-\t\t    new->name, gitdir.buf, new->name, new->name, new->name);\n-\telse\n-\t\tdie(_(\"'%s' is already checked out at %s\"), new->name, gitdir.buf);\n+\tdie(_(\"'%s' is already checked out at '%s'\"), new->name, gitdir.buf);\n done:\n \tstrbuf_release(&path);\n \tstrbuf_release(&sb);\ndiff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh\nindex 508993f..aa24ea8 100755\n--- a/t/t2025-checkout-to.sh\n+++ b/t/t2025-checkout-to.sh\n@@ -12,6 +12,11 @@ test_expect_success 'checkout --to not updating paths' '\n \ttest_must_fail git checkout --to -- init.t\n '\n \n+test_expect_success 'checkout --to an existing worktree' '\n+\tmkdir existing &&\n+\ttest_must_fail git checkout --detach --to existing master\n+'\n+\n test_expect_success 'checkout --to refuses to checkout locked branch' '\n \ttest_must_fail git checkout --to zere master &&\n \t! test -d zere &&\n@@ -31,6 +36,16 @@ test_expect_success 'checkout --to a new worktree' '\n \t)\n '\n \n+test_expect_success 'checkout --to a new worktree from a subdir' '\n+\t(\n+\t\tmkdir sub &&\n+\t\tcd sub &&\n+\t\tgit checkout --detach --to here master &&\n+\t\tcd here &&\n+\t\ttest_cmp ../../init.t init.t\n+\t)\n+'\n+\n test_expect_success 'checkout --to from a linked checkout' '\n \t(\n \t\tcd here &&\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"248608","messageId":"1409387642-24492-2-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1409387642-24492-1-git-send-email-pclouds@gmail.com","subject":"[PATCH 01/32] path.c: make get_pathname() return strbuf instead of static buffer","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-08-30T08:33:31Z","receivedAt":"2014-08-30T08:33:31Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"We've been avoiding PATH_MAX whenever possible. This patch makes\nget_pathname() return a strbuf and updates the callers to take\nadvantage of this. The code is simplified as we no longer need to\nworry about buffer overflow.\n\nvsnpath() behavior is changed slightly: previously it always clears\nthe buffer before writing, now it just appends. Fortunately this is a\nstatic function and all of its callers prepare the buffer properly:\ngit_path() gets the buffer from get_pathname() which resets the\nbuffer, the remaining call sites start with STRBUF_INIT'd buffer.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n path.c | 120 ++++++++++++++++++++++++++++-------------------------------------\n 1 file changed, 51 insertions(+), 69 deletions(-)\n\ndiff --git a/path.c b/path.c\nindex bc804a3..42ef3af 100644\n--- a/path.c\n+++ b/path.c\n@@ -16,11 +16,15 @@ static int get_st_mode_bits(const char *path, int *mode)\n \n static char bad_path[] = \"/bad-path/\";\n \n-static char *get_pathname(void)\n+static struct strbuf *get_pathname(void)\n {\n-\tstatic char pathname_array[4][PATH_MAX];\n+\tstatic struct strbuf pathname_array[4] = {\n+\t\tSTRBUF_INIT, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT\n+\t};\n \tstatic int index;\n-\treturn pathname_array[3 & ++index];\n+\tstruct strbuf *sb = &pathname_array[3 & ++index];\n+\tstrbuf_reset(sb);\n+\treturn sb;\n }\n \n static char *cleanup_path(char *path)\n@@ -34,6 +38,13 @@ static char *cleanup_path(char *path)\n \treturn path;\n }\n \n+static void strbuf_cleanup_path(struct strbuf *sb)\n+{\n+\tchar *path = cleanup_path(sb->buf);\n+\tif (path > sb->buf)\n+\t\tstrbuf_remove(sb, 0, path - sb->buf);\n+}\n+\n char *mksnpath(char *buf, size_t n, const char *fmt, ...)\n {\n \tva_list args;\n@@ -49,85 +60,70 @@ char *mksnpath(char *buf, size_t n, const char *fmt, ...)\n \treturn cleanup_path(buf);\n }\n \n-static char *vsnpath(char *buf, size_t n, const char *fmt, va_list args)\n+static void vsnpath(struct strbuf *buf, const char *fmt, va_list args)\n {\n \tconst char *git_dir = get_git_dir();\n-\tsize_t len;\n-\n-\tlen = strlen(git_dir);\n-\tif (n < len + 1)\n-\t\tgoto bad;\n-\tmemcpy(buf, git_dir, len);\n-\tif (len && !is_dir_sep(git_dir[len-1]))\n-\t\tbuf[len++] = '/';\n-\tlen += vsnprintf(buf + len, n - len, fmt, args);\n-\tif (len >= n)\n-\t\tgoto bad;\n-\treturn cleanup_path(buf);\n-bad:\n-\tstrlcpy(buf, bad_path, n);\n-\treturn buf;\n+\tstrbuf_addstr(buf, git_dir);\n+\tif (buf->len && !is_dir_sep(buf->buf[buf->len - 1]))\n+\t\tstrbuf_addch(buf, '/');\n+\tstrbuf_vaddf(buf, fmt, args);\n+\tstrbuf_cleanup_path(buf);\n }\n \n char *git_snpath(char *buf, size_t n, const char *fmt, ...)\n {\n-\tchar *ret;\n+\tstruct strbuf sb = STRBUF_INIT;\n \tva_list args;\n \tva_start(args, fmt);\n-\tret = vsnpath(buf, n, fmt, args);\n+\tvsnpath(&sb, fmt, args);\n \tva_end(args);\n-\treturn ret;\n+\tif (sb.len >= n)\n+\t\tstrlcpy(buf, bad_path, n);\n+\telse\n+\t\tmemcpy(buf, sb.buf, sb.len + 1);\n+\tstrbuf_release(&sb);\n+\treturn buf;\n }\n \n char *git_pathdup(const char *fmt, ...)\n {\n-\tchar path[PATH_MAX], *ret;\n+\tstruct strbuf path = STRBUF_INIT;\n \tva_list args;\n \tva_start(args, fmt);\n-\tret = vsnpath(path, sizeof(path), fmt, args);\n+\tvsnpath(&path, fmt, args);\n \tva_end(args);\n-\treturn xstrdup(ret);\n+\treturn strbuf_detach(&path, NULL);\n }\n \n char *mkpathdup(const char *fmt, ...)\n {\n-\tchar *path;\n \tstruct strbuf sb = STRBUF_INIT;\n \tva_list args;\n-\n \tva_start(args, fmt);\n \tstrbuf_vaddf(&sb, fmt, args);\n \tva_end(args);\n-\tpath = xstrdup(cleanup_path(sb.buf));\n-\n-\tstrbuf_release(&sb);\n-\treturn path;\n+\tstrbuf_cleanup_path(&sb);\n+\treturn strbuf_detach(&sb, NULL);\n }\n \n char *mkpath(const char *fmt, ...)\n {\n \tva_list args;\n-\tunsigned len;\n-\tchar *pathname = get_pathname();\n-\n+\tstruct strbuf *pathname = get_pathname();\n \tva_start(args, fmt);\n-\tlen = vsnprintf(pathname, PATH_MAX, fmt, args);\n+\tstrbuf_vaddf(pathname, fmt, args);\n \tva_end(args);\n-\tif (len >= PATH_MAX)\n-\t\treturn bad_path;\n-\treturn cleanup_path(pathname);\n+\treturn cleanup_path(pathname->buf);\n }\n \n char *git_path(const char *fmt, ...)\n {\n-\tchar *pathname = get_pathname();\n+\tstruct strbuf *pathname = get_pathname();\n \tva_list args;\n-\tchar *ret;\n-\n \tva_start(args, fmt);\n-\tret = vsnpath(pathname, PATH_MAX, fmt, args);\n+\tvsnpath(pathname, fmt, args);\n \tva_end(args);\n-\treturn ret;\n+\treturn pathname->buf;\n }\n \n void home_config_paths(char **global, char **xdg, char *file)\n@@ -158,41 +154,27 @@ void home_config_paths(char **global, char **xdg, char *file)\n \n char *git_path_submodule(const char *path, const char *fmt, ...)\n {\n-\tchar *pathname = get_pathname();\n-\tstruct strbuf buf = STRBUF_INIT;\n+\tstruct strbuf *buf = get_pathname();\n \tconst char *git_dir;\n \tva_list args;\n-\tunsigned len;\n-\n-\tlen = strlen(path);\n-\tif (len > PATH_MAX-100)\n-\t\treturn bad_path;\n \n-\tstrbuf_addstr(&buf, path);\n-\tif (len && path[len-1] != '/')\n-\t\tstrbuf_addch(&buf, '/');\n-\tstrbuf_addstr(&buf, \".git\");\n+\tstrbuf_addstr(buf, path);\n+\tif (buf->len && buf->buf[buf->len - 1] != '/')\n+\t\tstrbuf_addch(buf, '/');\n+\tstrbuf_addstr(buf, \".git\");\n \n-\tgit_dir = read_gitfile(buf.buf);\n+\tgit_dir = read_gitfile(buf->buf);\n \tif (git_dir) {\n-\t\tstrbuf_reset(&buf);\n-\t\tstrbuf_addstr(&buf, git_dir);\n+\t\tstrbuf_reset(buf);\n+\t\tstrbuf_addstr(buf, git_dir);\n \t}\n-\tstrbuf_addch(&buf, '/');\n-\n-\tif (buf.len >= PATH_MAX)\n-\t\treturn bad_path;\n-\tmemcpy(pathname, buf.buf, buf.len + 1);\n-\n-\tstrbuf_release(&buf);\n-\tlen = strlen(pathname);\n+\tstrbuf_addch(buf, '/');\n \n \tva_start(args, fmt);\n-\tlen += vsnprintf(pathname + len, PATH_MAX - len, fmt, args);\n+\tstrbuf_vaddf(buf, fmt, args);\n \tva_end(args);\n-\tif (len >= PATH_MAX)\n-\t\treturn bad_path;\n-\treturn cleanup_path(pathname);\n+\tstrbuf_cleanup_path(buf);\n+\treturn buf->buf;\n }\n \n int validate_headref(const char *path)\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"248610","messageId":"1409387642-24492-3-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1409387642-24492-1-git-send-email-pclouds@gmail.com","subject":"[PATCH 02/32] path.c: make get_pathname() call sites return const char *","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-08-30T08:33:32Z","receivedAt":"2014-08-30T08:33:32Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Before the previous commit, get_pathname returns an array of PATH_MAX\nlength. Even if git_path() and similar functions does not use the\nwhole array, git_path() caller can, in theory.\n\nAfter the commit, get_pathname() may return a buffer that has just\nenough room for the returned string and git_path() caller should never\nwrite beyond that.\n\nMake git_path(), mkpath() and git_path_submodule() return a const\nbuffer to make sure callers do not write in it at all.\n\nThis could have been part of the previous commit, but the \"const\"\nconversion is too much distraction from the core changes in path.c.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n builtin/checkout.c     | 2 +-\n builtin/clone.c        | 9 +++++----\n builtin/fetch.c        | 5 +++--\n builtin/fsck.c         | 4 ++--\n builtin/receive-pack.c | 2 +-\n builtin/remote.c       | 2 +-\n builtin/repack.c       | 8 +++++---\n cache.h                | 6 +++---\n fast-import.c          | 2 +-\n notes-merge.c          | 6 +++---\n path.c                 | 6 +++---\n refs.c                 | 8 ++++----\n run-command.c          | 4 ++--\n run-command.h          | 2 +-\n sha1_file.c            | 2 +-\n 15 files changed, 36 insertions(+), 32 deletions(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 463cfee..3abcef1 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -585,7 +585,7 @@ static void update_refs_for_switch(const struct checkout_opts *opts,\n \t\t\tif (opts->new_branch_log && !log_all_ref_updates) {\n \t\t\t\tint temp;\n \t\t\t\tchar log_file[PATH_MAX];\n-\t\t\t\tchar *ref_name = mkpath(\"refs/heads/%s\", opts->new_orphan_branch);\n+\t\t\t\tconst char *ref_name = mkpath(\"refs/heads/%s\", opts->new_orphan_branch);\n \n \t\t\t\ttemp = log_all_ref_updates;\n \t\t\t\tlog_all_ref_updates = 1;\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex e15ca33..91fac9d 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -289,16 +289,17 @@ static void copy_alternates(struct strbuf *src, struct strbuf *dst,\n \tstruct strbuf line = STRBUF_INIT;\n \n \twhile (strbuf_getline(&line, in, '\\n') != EOF) {\n-\t\tchar *abs_path, abs_buf[PATH_MAX];\n+\t\tchar *abs_path;\n \t\tif (!line.len || line.buf[0] == '#')\n \t\t\tcontinue;\n \t\tif (is_absolute_path(line.buf)) {\n \t\t\tadd_to_alternates_file(line.buf);\n \t\t\tcontinue;\n \t\t}\n-\t\tabs_path = mkpath(\"%s/objects/%s\", src_repo, line.buf);\n-\t\tnormalize_path_copy(abs_buf, abs_path);\n-\t\tadd_to_alternates_file(abs_buf);\n+\t\tabs_path = mkpathdup(\"%s/objects/%s\", src_repo, line.buf);\n+\t\tnormalize_path_copy(abs_path, abs_path);\n+\t\tadd_to_alternates_file(abs_path);\n+\t\tfree(abs_path);\n \t}\n \tstrbuf_release(&line);\n \tfclose(in);\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex e8d0cca..9522b1b 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -573,7 +573,8 @@ static int store_updated_refs(const char *raw_url, const char *remote_name,\n \tstruct strbuf note = STRBUF_INIT;\n \tconst char *what, *kind;\n \tstruct ref *rm;\n-\tchar *url, *filename = dry_run ? \"/dev/null\" : git_path(\"FETCH_HEAD\");\n+\tchar *url;\n+\tconst char *filename = dry_run ? \"/dev/null\" : git_path(\"FETCH_HEAD\");\n \tint want_status;\n \n \tfp = fopen(filename, \"a\");\n@@ -807,7 +808,7 @@ static void check_not_current_branch(struct ref *ref_map)\n \n static int truncate_fetch_head(void)\n {\n-\tchar *filename = git_path(\"FETCH_HEAD\");\n+\tconst char *filename = git_path(\"FETCH_HEAD\");\n \tFILE *fp = fopen(filename, \"w\");\n \n \tif (!fp)\ndiff --git a/builtin/fsck.c b/builtin/fsck.c\nindex 8aadca1..d414962 100644\n--- a/builtin/fsck.c\n+++ b/builtin/fsck.c\n@@ -225,12 +225,12 @@ static void check_unreachable_object(struct object *obj)\n \t\t\tprintf(\"dangling %s %s\\n\", typename(obj->type),\n \t\t\t       sha1_to_hex(obj->sha1));\n \t\tif (write_lost_and_found) {\n-\t\t\tchar *filename = git_path(\"lost-found/%s/%s\",\n+\t\t\tconst char *filename = git_path(\"lost-found/%s/%s\",\n \t\t\t\tobj->type == OBJ_COMMIT ? \"commit\" : \"other\",\n \t\t\t\tsha1_to_hex(obj->sha1));\n \t\t\tFILE *f;\n \n-\t\t\tif (safe_create_leading_directories(filename)) {\n+\t\t\tif (safe_create_leading_directories_const(filename)) {\n \t\t\t\terror(\"Could not create lost-found\");\n \t\t\t\treturn;\n \t\t\t}\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 18458e8..ed11e7e 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -599,7 +599,7 @@ static void run_update_post_hook(struct command *commands)\n \tint argc;\n \tconst char **argv;\n \tstruct child_process proc;\n-\tchar *hook;\n+\tconst char *hook;\n \n \thook = find_hook(\"post-update\");\n \tfor (argc = 0, cmd = commands; cmd; cmd = cmd->next) {\ndiff --git a/builtin/remote.c b/builtin/remote.c\nindex a8efe3d..3d99c9c 100644\n--- a/builtin/remote.c\n+++ b/builtin/remote.c\n@@ -580,7 +580,7 @@ static int migrate_file(struct remote *remote)\n {\n \tstruct strbuf buf = STRBUF_INIT;\n \tint i;\n-\tchar *path = NULL;\n+\tconst char *path = NULL;\n \n \tstrbuf_addf(&buf, \"remote.%s.url\", remote->name);\n \tfor (i = 0; i < remote->url_nr; i++)\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex ff2216a..a64a4a9 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -285,7 +285,8 @@ int cmd_repack(int argc, const char **argv, const char *prefix)\n \tfailed = 0;\n \tfor_each_string_list_item(item, &names) {\n \t\tfor (ext = 0; ext < ARRAY_SIZE(exts); ext++) {\n-\t\t\tchar *fname, *fname_old;\n+\t\t\tconst char *fname_old;\n+\t\t\tchar *fname;\n \t\t\tfname = mkpathdup(\"%s/pack-%s%s\", packdir,\n \t\t\t\t\t\titem->string, exts[ext].name);\n \t\t\tif (!file_exists(fname)) {\n@@ -313,7 +314,8 @@ int cmd_repack(int argc, const char **argv, const char *prefix)\n \tif (failed) {\n \t\tstruct string_list rollback_failure = STRING_LIST_INIT_DUP;\n \t\tfor_each_string_list_item(item, &rollback) {\n-\t\t\tchar *fname, *fname_old;\n+\t\t\tconst char *fname_old;\n+\t\t\tchar *fname;\n \t\t\tfname = mkpathdup(\"%s/%s\", packdir, item->string);\n \t\t\tfname_old = mkpath(\"%s/old-%s\", packdir, item->string);\n \t\t\tif (rename(fname_old, fname))\n@@ -366,7 +368,7 @@ int cmd_repack(int argc, const char **argv, const char *prefix)\n \t/* Remove the \"old-\" files */\n \tfor_each_string_list_item(item, &names) {\n \t\tfor (ext = 0; ext < ARRAY_SIZE(exts); ext++) {\n-\t\t\tchar *fname;\n+\t\t\tconst char *fname;\n \t\t\tfname = mkpath(\"%s/old-%s%s\",\n \t\t\t\t\tpackdir,\n \t\t\t\t\titem->string,\ndiff --git a/cache.h b/cache.h\nindex 44aa439..707408b 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -682,9 +682,9 @@ extern char *mkpathdup(const char *fmt, ...)\n \t__attribute__((format (printf, 1, 2)));\n \n /* Return a statically allocated filename matching the sha1 signature */\n-extern char *mkpath(const char *fmt, ...) __attribute__((format (printf, 1, 2)));\n-extern char *git_path(const char *fmt, ...) __attribute__((format (printf, 1, 2)));\n-extern char *git_path_submodule(const char *path, const char *fmt, ...)\n+extern const char *mkpath(const char *fmt, ...) __attribute__((format (printf, 1, 2)));\n+extern const char *git_path(const char *fmt, ...) __attribute__((format (printf, 1, 2)));\n+extern const char *git_path_submodule(const char *path, const char *fmt, ...)\n \t__attribute__((format (printf, 2, 3)));\n \n /*\ndiff --git a/fast-import.c b/fast-import.c\nindex fa635f7..d9c068b 100644\n--- a/fast-import.c\n+++ b/fast-import.c\n@@ -404,7 +404,7 @@ static void dump_marks_helper(FILE *, uintmax_t, struct mark_set *);\n \n static void write_crash_report(const char *err)\n {\n-\tchar *loc = git_path(\"fast_import_crash_%\"PRIuMAX, (uintmax_t) getpid());\n+\tconst char *loc = git_path(\"fast_import_crash_%\"PRIuMAX, (uintmax_t) getpid());\n \tFILE *rpt = fopen(loc, \"w\");\n \tstruct branch *b;\n \tunsigned long lu;\ndiff --git a/notes-merge.c b/notes-merge.c\nindex fd5fae2..a9e6b15 100644\n--- a/notes-merge.c\n+++ b/notes-merge.c\n@@ -280,7 +280,7 @@ static void check_notes_merge_worktree(struct notes_merge_options *o)\n \t\t\t\t    \"(%s exists).\", git_path(\"NOTES_MERGE_*\"));\n \t\t}\n \n-\t\tif (safe_create_leading_directories(git_path(\n+\t\tif (safe_create_leading_directories_const(git_path(\n \t\t\t\tNOTES_MERGE_WORKTREE \"/.test\")))\n \t\t\tdie_errno(\"unable to create directory %s\",\n \t\t\t\t  git_path(NOTES_MERGE_WORKTREE));\n@@ -295,8 +295,8 @@ static void write_buf_to_worktree(const unsigned char *obj,\n \t\t\t\t  const char *buf, unsigned long size)\n {\n \tint fd;\n-\tchar *path = git_path(NOTES_MERGE_WORKTREE \"/%s\", sha1_to_hex(obj));\n-\tif (safe_create_leading_directories(path))\n+\tconst char *path = git_path(NOTES_MERGE_WORKTREE \"/%s\", sha1_to_hex(obj));\n+\tif (safe_create_leading_directories_const(path))\n \t\tdie_errno(\"unable to create directory for '%s'\", path);\n \tif (file_exists(path))\n \t\tdie(\"found existing file at '%s'\", path);\ndiff --git a/path.c b/path.c\nindex 42ef3af..a3f8826 100644\n--- a/path.c\n+++ b/path.c\n@@ -106,7 +106,7 @@ char *mkpathdup(const char *fmt, ...)\n \treturn strbuf_detach(&sb, NULL);\n }\n \n-char *mkpath(const char *fmt, ...)\n+const char *mkpath(const char *fmt, ...)\n {\n \tva_list args;\n \tstruct strbuf *pathname = get_pathname();\n@@ -116,7 +116,7 @@ char *mkpath(const char *fmt, ...)\n \treturn cleanup_path(pathname->buf);\n }\n \n-char *git_path(const char *fmt, ...)\n+const char *git_path(const char *fmt, ...)\n {\n \tstruct strbuf *pathname = get_pathname();\n \tva_list args;\n@@ -152,7 +152,7 @@ void home_config_paths(char **global, char **xdg, char *file)\n \tfree(to_free);\n }\n \n-char *git_path_submodule(const char *path, const char *fmt, ...)\n+const char *git_path_submodule(const char *path, const char *fmt, ...)\n {\n \tstruct strbuf *buf = get_pathname();\n \tconst char *git_dir;\ndiff --git a/refs.c b/refs.c\nindex 82e4842..dbd131e 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1442,7 +1442,7 @@ static int resolve_gitlink_ref_recursive(struct ref_cache *refs,\n {\n \tint fd, len;\n \tchar buffer[128], *p;\n-\tchar *path;\n+\tconst char *path;\n \n \tif (recursion > MAXDEPTH || strlen(refname) > MAXREFLEN)\n \t\treturn -1;\n@@ -2255,7 +2255,7 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname,\n \t\t\t\t\t    const unsigned char *old_sha1,\n \t\t\t\t\t    int flags, int *type_p)\n {\n-\tchar *ref_file;\n+\tconst char *ref_file;\n \tconst char *orig_refname = refname;\n \tstruct ref_lock *lock;\n \tint last_errno = 0;\n@@ -2318,7 +2318,7 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname,\n \t\tlock->force_write = 1;\n \n  retry:\n-\tswitch (safe_create_leading_directories(ref_file)) {\n+\tswitch (safe_create_leading_directories_const(ref_file)) {\n \tcase SCLD_OK:\n \t\tbreak; /* success */\n \tcase SCLD_VANISHED:\n@@ -2756,7 +2756,7 @@ static int rename_tmp_log(const char *newrefname)\n \tint attempts_remaining = 4;\n \n  retry:\n-\tswitch (safe_create_leading_directories(git_path(\"logs/%s\", newrefname))) {\n+\tswitch (safe_create_leading_directories_const(git_path(\"logs/%s\", newrefname))) {\n \tcase SCLD_OK:\n \t\tbreak; /* success */\n \tcase SCLD_VANISHED:\ndiff --git a/run-command.c b/run-command.c\nindex be07d4a..614b8ac 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -758,9 +758,9 @@ int finish_async(struct async *async)\n #endif\n }\n \n-char *find_hook(const char *name)\n+const char *find_hook(const char *name)\n {\n-\tchar *path = git_path(\"hooks/%s\", name);\n+\tconst char *path = git_path(\"hooks/%s\", name);\n \tif (access(path, X_OK) < 0)\n \t\tpath = NULL;\n \ndiff --git a/run-command.h b/run-command.h\nindex ea73de3..890cc95 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -48,7 +48,7 @@ int start_command(struct child_process *);\n int finish_command(struct child_process *);\n int run_command(struct child_process *);\n \n-extern char *find_hook(const char *name);\n+extern const char *find_hook(const char *name);\n LAST_ARG_MUST_BE_NULL\n extern int run_hook_le(const char *const *env, const char *name, ...);\n extern int run_hook_ve(const char *const *env, const char *name, va_list args);\ndiff --git a/sha1_file.c b/sha1_file.c\nindex a38854c..9700108 100644\n--- a/sha1_file.c\n+++ b/sha1_file.c\n@@ -402,7 +402,7 @@ void add_to_alternates_file(const char *reference)\n {\n \tstruct lock_file *lock = xcalloc(1, sizeof(struct lock_file));\n \tint fd = hold_lock_file_for_append(lock, git_path(\"objects/info/alternates\"), LOCK_DIE_ON_ERROR);\n-\tchar *alt = mkpath(\"%s\\n\", reference);\n+\tconst char *alt = mkpath(\"%s\\n\", reference);\n \twrite_or_die(fd, alt, strlen(alt));\n \tif (commit_lock_file(lock))\n \t\tdie(\"could not close alternates file\");\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"248609","messageId":"1409387642-24492-4-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1409387642-24492-1-git-send-email-pclouds@gmail.com","subject":"[PATCH 03/32] git_snpath(): retire and replace with strbuf_git_path()","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-08-30T08:33:33Z","receivedAt":"2014-08-30T08:33:33Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"In the previous patch, git_snpath() is modified to allocate a new\nstrbuf buffer because vsnpath() needs that. But that makes it\nawkward because git_snpath() receives a pre-allocated buffer from\noutside and has to copy data back. Rename it to strbuf_git_path()\nand make it receive strbuf directly.\n\nUsing git_path() in update_refs_for_switch() which used to call\ngit_snpath() is safe because that function and all of its callers do\nnot keep any pointer to the round-robin buffer pool allocated by\nget_pathname().\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n builtin/checkout.c | 13 +++++----\n cache.h            |  4 +--\n path.c             | 11 ++------\n refs.c             | 78 +++++++++++++++++++++++++++++++++---------------------\n refs.h             |  2 +-\n 5 files changed, 61 insertions(+), 47 deletions(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 3abcef1..8023987 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -584,18 +584,21 @@ static void update_refs_for_switch(const struct checkout_opts *opts,\n \t\tif (opts->new_orphan_branch) {\n \t\t\tif (opts->new_branch_log && !log_all_ref_updates) {\n \t\t\t\tint temp;\n-\t\t\t\tchar log_file[PATH_MAX];\n-\t\t\t\tconst char *ref_name = mkpath(\"refs/heads/%s\", opts->new_orphan_branch);\n+\t\t\t\tstruct strbuf log_file = STRBUF_INIT;\n+\t\t\t\tint ret;\n+\t\t\t\tconst char *ref_name;\n \n+\t\t\t\tref_name = mkpath(\"refs/heads/%s\", opts->new_orphan_branch);\n \t\t\t\ttemp = log_all_ref_updates;\n \t\t\t\tlog_all_ref_updates = 1;\n-\t\t\t\tif (log_ref_setup(ref_name, log_file, sizeof(log_file))) {\n+\t\t\t\tret = log_ref_setup(ref_name, &log_file);\n+\t\t\t\tlog_all_ref_updates = temp;\n+\t\t\t\tstrbuf_release(&log_file);\n+\t\t\t\tif (ret) {\n \t\t\t\t\tfprintf(stderr, _(\"Can not do reflog for '%s'\\n\"),\n \t\t\t\t\t    opts->new_orphan_branch);\n-\t\t\t\t\tlog_all_ref_updates = temp;\n \t\t\t\t\treturn;\n \t\t\t\t}\n-\t\t\t\tlog_all_ref_updates = temp;\n \t\t\t}\n \t\t}\n \t\telse\ndiff --git a/cache.h b/cache.h\nindex 707408b..279b581 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -674,8 +674,8 @@ extern int check_repository_format(void);\n \n extern char *mksnpath(char *buf, size_t n, const char *fmt, ...)\n \t__attribute__((format (printf, 3, 4)));\n-extern char *git_snpath(char *buf, size_t n, const char *fmt, ...)\n-\t__attribute__((format (printf, 3, 4)));\n+extern void strbuf_git_path(struct strbuf *sb, const char *fmt, ...)\n+\t__attribute__((format (printf, 2, 3)));\n extern char *git_pathdup(const char *fmt, ...)\n \t__attribute__((format (printf, 1, 2)));\n extern char *mkpathdup(const char *fmt, ...)\ndiff --git a/path.c b/path.c\nindex a3f8826..e545064 100644\n--- a/path.c\n+++ b/path.c\n@@ -70,19 +70,12 @@ static void vsnpath(struct strbuf *buf, const char *fmt, va_list args)\n \tstrbuf_cleanup_path(buf);\n }\n \n-char *git_snpath(char *buf, size_t n, const char *fmt, ...)\n+void strbuf_git_path(struct strbuf *sb, const char *fmt, ...)\n {\n-\tstruct strbuf sb = STRBUF_INIT;\n \tva_list args;\n \tva_start(args, fmt);\n-\tvsnpath(&sb, fmt, args);\n+\tvsnpath(sb, fmt, args);\n \tva_end(args);\n-\tif (sb.len >= n)\n-\t\tstrlcpy(buf, bad_path, n);\n-\telse\n-\t\tmemcpy(buf, sb.buf, sb.len + 1);\n-\tstrbuf_release(&sb);\n-\treturn buf;\n }\n \n char *git_pathdup(const char *fmt, ...)\ndiff --git a/refs.c b/refs.c\nindex dbd131e..00b2312 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1535,10 +1535,12 @@ static const char *handle_missing_loose_ref(const char *refname,\n \n const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int reading, int *flag)\n {\n+\tstruct strbuf sb_path = STRBUF_INIT;\n \tint depth = MAXDEPTH;\n \tssize_t len;\n \tchar buffer[256];\n \tstatic char refname_buffer[256];\n+\tconst char *ret;\n \n \tif (flag)\n \t\t*flag = 0;\n@@ -1547,15 +1549,17 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea\n \t\treturn NULL;\n \n \tfor (;;) {\n-\t\tchar path[PATH_MAX];\n+\t\tconst char *path;\n \t\tstruct stat st;\n \t\tchar *buf;\n \t\tint fd;\n \n \t\tif (--depth < 0)\n-\t\t\treturn NULL;\n+\t\t\tgoto fail;\n \n-\t\tgit_snpath(path, sizeof(path), \"%s\", refname);\n+\t\tstrbuf_reset(&sb_path);\n+\t\tstrbuf_git_path(&sb_path, \"%s\", refname);\n+\t\tpath = sb_path.buf;\n \n \t\t/*\n \t\t * We might have to loop back here to avoid a race\n@@ -1569,10 +1573,11 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea\n \tstat_ref:\n \t\tif (lstat(path, &st) < 0) {\n \t\t\tif (errno == ENOENT)\n-\t\t\t\treturn handle_missing_loose_ref(refname, sha1,\n-\t\t\t\t\t\t\t\treading, flag);\n+\t\t\t\tret = handle_missing_loose_ref(refname, sha1,\n+\t\t\t\t\t\t\t       reading, flag);\n \t\t\telse\n-\t\t\t\treturn NULL;\n+\t\t\t\tret = NULL;\n+\t\t\tgoto done;\n \t\t}\n \n \t\t/* Follow \"normalized\" - ie \"refs/..\" symlinks by hand */\n@@ -1583,7 +1588,7 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea\n \t\t\t\t\t/* inconsistent with lstat; retry */\n \t\t\t\t\tgoto stat_ref;\n \t\t\t\telse\n-\t\t\t\t\treturn NULL;\n+\t\t\t\t\tgoto fail;\n \t\t\t}\n \t\t\tbuffer[len] = 0;\n \t\t\tif (starts_with(buffer, \"refs/\") &&\n@@ -1599,7 +1604,7 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea\n \t\t/* Is it a directory? */\n \t\tif (S_ISDIR(st.st_mode)) {\n \t\t\terrno = EISDIR;\n-\t\t\treturn NULL;\n+\t\t\tgoto fail;\n \t\t}\n \n \t\t/*\n@@ -1612,12 +1617,13 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea\n \t\t\t\t/* inconsistent with lstat; retry */\n \t\t\t\tgoto stat_ref;\n \t\t\telse\n-\t\t\t\treturn NULL;\n+\t\t\t\tgoto fail;\n \t\t}\n+\n \t\tlen = read_in_full(fd, buffer, sizeof(buffer)-1);\n \t\tclose(fd);\n \t\tif (len < 0)\n-\t\t\treturn NULL;\n+\t\t\tgoto fail;\n \t\twhile (len && isspace(buffer[len-1]))\n \t\t\tlen--;\n \t\tbuffer[len] = '\\0';\n@@ -1634,9 +1640,10 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea\n \t\t\t    (buffer[40] != '\\0' && !isspace(buffer[40]))) {\n \t\t\t\tif (flag)\n \t\t\t\t\t*flag |= REF_ISBROKEN;\n-\t\t\t\treturn NULL;\n+\t\t\t\tgoto fail;\n \t\t\t}\n-\t\t\treturn refname;\n+\t\t\tret = refname;\n+\t\t\tgoto done;\n \t\t}\n \t\tif (flag)\n \t\t\t*flag |= REF_ISSYMREF;\n@@ -1646,10 +1653,15 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea\n \t\tif (check_refname_format(buf, REFNAME_ALLOW_ONELEVEL)) {\n \t\t\tif (flag)\n \t\t\t\t*flag |= REF_ISBROKEN;\n-\t\t\treturn NULL;\n+\t\t\tgoto fail;\n \t\t}\n \t\trefname = strcpy(refname_buffer, buf);\n \t}\n+fail:\n+\tret = NULL;\n+done:\n+\tstrbuf_release(&sb_path);\n+\treturn ret;\n }\n \n char *resolve_refdup(const char *ref, unsigned char *sha1, int reading, int *flag)\n@@ -2940,41 +2952,41 @@ static int copy_msg(char *buf, const char *msg)\n \treturn cp - buf;\n }\n \n-int log_ref_setup(const char *refname, char *logfile, int bufsize)\n+int log_ref_setup(const char *refname, struct strbuf *logfile)\n {\n \tint logfd, oflags = O_APPEND | O_WRONLY;\n \n-\tgit_snpath(logfile, bufsize, \"logs/%s\", refname);\n+\tstrbuf_git_path(logfile, \"logs/%s\", refname);\n \tif (log_all_ref_updates &&\n \t    (starts_with(refname, \"refs/heads/\") ||\n \t     starts_with(refname, \"refs/remotes/\") ||\n \t     starts_with(refname, \"refs/notes/\") ||\n \t     !strcmp(refname, \"HEAD\"))) {\n-\t\tif (safe_create_leading_directories(logfile) < 0)\n+\t\tif (safe_create_leading_directories(logfile->buf) < 0)\n \t\t\treturn error(\"unable to create directory for %s\",\n-\t\t\t\t     logfile);\n+\t\t\t\t     logfile->buf);\n \t\toflags |= O_CREAT;\n \t}\n \n-\tlogfd = open(logfile, oflags, 0666);\n+\tlogfd = open(logfile->buf, oflags, 0666);\n \tif (logfd < 0) {\n \t\tif (!(oflags & O_CREAT) && errno == ENOENT)\n \t\t\treturn 0;\n \n \t\tif ((oflags & O_CREAT) && errno == EISDIR) {\n-\t\t\tif (remove_empty_directories(logfile)) {\n+\t\t\tif (remove_empty_directories(logfile->buf)) {\n \t\t\t\treturn error(\"There are still logs under '%s'\",\n-\t\t\t\t\t     logfile);\n+\t\t\t\t\t     logfile->buf);\n \t\t\t}\n-\t\t\tlogfd = open(logfile, oflags, 0666);\n+\t\t\tlogfd = open(logfile->buf, oflags, 0666);\n \t\t}\n \n \t\tif (logfd < 0)\n \t\t\treturn error(\"Unable to append to %s: %s\",\n-\t\t\t\t     logfile, strerror(errno));\n+\t\t\t\t     logfile->buf, strerror(errno));\n \t}\n \n-\tadjust_shared_perm(logfile);\n+\tadjust_shared_perm(logfile->buf);\n \tclose(logfd);\n \treturn 0;\n }\n@@ -2985,20 +2997,22 @@ static int log_ref_write(const char *refname, const unsigned char *old_sha1,\n \tint logfd, result, written, oflags = O_APPEND | O_WRONLY;\n \tunsigned maxlen, len;\n \tint msglen;\n-\tchar log_file[PATH_MAX];\n+\tstruct strbuf sb_log_file = STRBUF_INIT;\n+\tconst char *log_file;\n \tchar *logrec;\n \tconst char *committer;\n \n \tif (log_all_ref_updates < 0)\n \t\tlog_all_ref_updates = !is_bare_repository();\n \n-\tresult = log_ref_setup(refname, log_file, sizeof(log_file));\n+\tresult = log_ref_setup(refname, &sb_log_file);\n \tif (result)\n-\t\treturn result;\n+\t\tgoto done;\n+\tlog_file = sb_log_file.buf;\n \n \tlogfd = open(log_file, oflags);\n \tif (logfd < 0)\n-\t\treturn 0;\n+\t\tgoto done;\n \tmsglen = msg ? strlen(msg) : 0;\n \tcommitter = git_committer_info(0);\n \tmaxlen = strlen(committer) + msglen + 100;\n@@ -3011,9 +3025,13 @@ static int log_ref_write(const char *refname, const unsigned char *old_sha1,\n \t\tlen += copy_msg(logrec + len - 1, msg) - 1;\n \twritten = len <= maxlen ? write_in_full(logfd, logrec, len) : -1;\n \tfree(logrec);\n-\tif (close(logfd) != 0 || written != len)\n-\t\treturn error(\"Unable to append to %s\", log_file);\n-\treturn 0;\n+\tif (close(logfd) != 0 || written != len) {\n+\t\terror(\"Unable to append to %s\", log_file);\n+\t\tresult = -1;\n+\t}\n+done:\n+\tstrbuf_release(&sb_log_file);\n+\treturn result;\n }\n \n static int is_branch(const char *refname)\ndiff --git a/refs.h b/refs.h\nindex 4e3050d..99d88a7 100644\n--- a/refs.h\n+++ b/refs.h\n@@ -157,7 +157,7 @@ extern void unlock_ref(struct ref_lock *lock);\n extern int write_ref_sha1(struct ref_lock *lock, const unsigned char *sha1, const char *msg);\n \n /** Setup reflog before using. **/\n-int log_ref_setup(const char *refname, char *logfile, int bufsize);\n+int log_ref_setup(const char *refname, struct strbuf *logfile);\n \n /** Reads log for the value of ref during at_time. **/\n extern int read_ref_at(const char *refname, unsigned long at_time, int cnt,\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"248611","messageId":"1409387642-24492-5-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1409387642-24492-1-git-send-email-pclouds@gmail.com","subject":"[PATCH 04/32] path.c: rename vsnpath() to do_git_path()","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-08-30T08:33:34Z","receivedAt":"2014-08-30T08:33:34Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"The name vsnpath() gives an impression that this is general path\nhandling function. It's not. This is the underlying implementation of\ngit_path(), git_pathdup() and strbuf_git_path() which will prefix\n$GIT_DIR in the result string.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n path.c | 8 ++++----\n 1 file changed, 4 insertions(+), 4 deletions(-)\n\ndiff --git a/path.c b/path.c\nindex e545064..2cb2e61 100644\n--- a/path.c\n+++ b/path.c\n@@ -60,7 +60,7 @@ char *mksnpath(char *buf, size_t n, const char *fmt, ...)\n \treturn cleanup_path(buf);\n }\n \n-static void vsnpath(struct strbuf *buf, const char *fmt, va_list args)\n+static void do_git_path(struct strbuf *buf, const char *fmt, va_list args)\n {\n \tconst char *git_dir = get_git_dir();\n \tstrbuf_addstr(buf, git_dir);\n@@ -74,7 +74,7 @@ void strbuf_git_path(struct strbuf *sb, const char *fmt, ...)\n {\n \tva_list args;\n \tva_start(args, fmt);\n-\tvsnpath(sb, fmt, args);\n+\tdo_git_path(sb, fmt, args);\n \tva_end(args);\n }\n \n@@ -83,7 +83,7 @@ char *git_pathdup(const char *fmt, ...)\n \tstruct strbuf path = STRBUF_INIT;\n \tva_list args;\n \tva_start(args, fmt);\n-\tvsnpath(&path, fmt, args);\n+\tdo_git_path(&path, fmt, args);\n \tva_end(args);\n \treturn strbuf_detach(&path, NULL);\n }\n@@ -114,7 +114,7 @@ const char *git_path(const char *fmt, ...)\n \tstruct strbuf *pathname = get_pathname();\n \tva_list args;\n \tva_start(args, fmt);\n-\tvsnpath(pathname, fmt, args);\n+\tdo_git_path(pathname, fmt, args);\n \tva_end(args);\n \treturn pathname->buf;\n }\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"248612","messageId":"1409387642-24492-6-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1409387642-24492-1-git-send-email-pclouds@gmail.com","subject":"[PATCH 05/32] path.c: group git_path(), git_pathdup() and strbuf_git_path() together","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-08-30T08:33:35Z","receivedAt":"2014-08-30T08:33:35Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n path.c | 20 ++++++++++----------\n 1 file changed, 10 insertions(+), 10 deletions(-)\n\ndiff --git a/path.c b/path.c\nindex 2cb2e61..65881aa 100644\n--- a/path.c\n+++ b/path.c\n@@ -78,6 +78,16 @@ void strbuf_git_path(struct strbuf *sb, const char *fmt, ...)\n \tva_end(args);\n }\n \n+const char *git_path(const char *fmt, ...)\n+{\n+\tstruct strbuf *pathname = get_pathname();\n+\tva_list args;\n+\tva_start(args, fmt);\n+\tdo_git_path(pathname, fmt, args);\n+\tva_end(args);\n+\treturn pathname->buf;\n+}\n+\n char *git_pathdup(const char *fmt, ...)\n {\n \tstruct strbuf path = STRBUF_INIT;\n@@ -109,16 +119,6 @@ const char *mkpath(const char *fmt, ...)\n \treturn cleanup_path(pathname->buf);\n }\n \n-const char *git_path(const char *fmt, ...)\n-{\n-\tstruct strbuf *pathname = get_pathname();\n-\tva_list args;\n-\tva_start(args, fmt);\n-\tdo_git_path(pathname, fmt, args);\n-\tva_end(args);\n-\treturn pathname->buf;\n-}\n-\n void home_config_paths(char **global, char **xdg, char *file)\n {\n \tchar *xdg_home = getenv(\"XDG_CONFIG_HOME\");\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"248613","messageId":"1409387642-24492-7-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1409387642-24492-1-git-send-email-pclouds@gmail.com","subject":"[PATCH 06/32] git_path(): be aware of file relocation in $GIT_DIR","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-08-30T08:33:36Z","receivedAt":"2014-08-30T08:33:36Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"We allow the user to relocate certain paths out of $GIT_DIR via\nenvironment variables, e.g. GIT_OBJECT_DIRECTORY, GIT_INDEX_FILE and\nGIT_GRAFT_FILE. Callers are not supposed to use git_path() or\ngit_pathdup() to get those paths. Instead they must use\nget_object_directory(), get_index_file() and get_graft_file()\nrespectively. This is inconvenient and could be missed in review (for\nexample, there's git_path(\"objects/info/alternates\") somewhere in\nsha1_file.c).\n\nThis patch makes git_path() and git_pathdup() understand those\nenvironment variables. So if you set GIT_OBJECT_DIRECTORY to /foo/bar,\ngit_path(\"objects/abc\") should return /foo/bar/abc. The same is done\nfor the two remaining env variables.\n\n\"git rev-parse --git-path\" is the wrapper for script use.\n\nThis patch kinda reverts a0279e1 (setup_git_env: use git_pathdup\ninstead of xmalloc + sprintf - 2014-06-19) because using git_pathdup\nhere would result in infinite recursion:\n\n  setup_git_env() -> git_pathdup(\"objects\") -> .. -> adjust_git_path()\n  -> get_object_directory() -> oops, git_object_directory is NOT set\n  yet -> setup_git_env()\n\nI wanted to make git_pathdup_literal() that skips adjust_git_path().\nBut that won't work because later on when $GIT_COMMON_DIR is\nintroduced, git_pathdup_literal(\"objects\") needs adjust_git_path() to\nreplace $GIT_DIR with $GIT_COMMON_DIR.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n Documentation/git-rev-parse.txt |  7 ++++++\n builtin/rev-parse.c             |  7 ++++++\n cache.h                         |  1 +\n environment.c                   | 19 +++++++++++-----\n path.c                          | 49 +++++++++++++++++++++++++++++++++++++++--\n t/t0060-path-utils.sh           | 19 ++++++++++++++++\n 6 files changed, 95 insertions(+), 7 deletions(-)\n\ndiff --git a/Documentation/git-rev-parse.txt b/Documentation/git-rev-parse.txt\nindex 987395d..9465399 100644\n--- a/Documentation/git-rev-parse.txt\n+++ b/Documentation/git-rev-parse.txt\n@@ -232,6 +232,13 @@ print a message to stderr and exit with nonzero status.\n \trepository.  If <path> is a gitfile then the resolved path\n \tto the real repository is printed.\n \n+--git-path <path>::\n+\tResolve \"$GIT_DIR/<path>\" and takes other path relocation\n+\tvariables such as $GIT_OBJECT_DIRECTORY,\n+\t$GIT_INDEX_FILE... into account. For example, if\n+\t$GIT_OBJECT_DIRECTORY is set to /foo/bar then \"git rev-parse\n+\t--git-path objects/abc\" returns /foo/bar/abc.\n+\n --show-cdup::\n \tWhen the command is invoked from a subdirectory, show the\n \tpath of the top-level directory relative to the current\ndiff --git a/builtin/rev-parse.c b/builtin/rev-parse.c\nindex 1a6122d..7606d43 100644\n--- a/builtin/rev-parse.c\n+++ b/builtin/rev-parse.c\n@@ -529,6 +529,13 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)\n \tfor (i = 1; i < argc; i++) {\n \t\tconst char *arg = argv[i];\n \n+\t\tif (!strcmp(arg, \"--git-path\")) {\n+\t\t\tif (!argv[i + 1])\n+\t\t\t\tdie(\"--git-path requires an argument\");\n+\t\t\tputs(git_path(\"%s\", argv[i + 1]));\n+\t\t\ti++;\n+\t\t\tcontinue;\n+\t\t}\n \t\tif (as_is) {\n \t\t\tif (show_file(arg, output_prefix) && as_is < 2)\n \t\t\t\tverify_filename(prefix, arg, 0);\ndiff --git a/cache.h b/cache.h\nindex 279b581..0128b9a 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -612,6 +612,7 @@ extern int fsync_object_files;\n extern int core_preload_index;\n extern int core_apply_sparse_checkout;\n extern int precomposed_unicode;\n+extern int git_db_env, git_index_env, git_graft_env;\n \n /*\n  * The character that begins a commented line in user-editable file\ndiff --git a/environment.c b/environment.c\nindex 565f652..fee12a6 100644\n--- a/environment.c\n+++ b/environment.c\n@@ -83,6 +83,7 @@ static size_t namespace_len;\n \n static const char *git_dir;\n static char *git_object_dir, *git_index_file, *git_graft_file;\n+int git_db_env, git_index_env, git_graft_env;\n \n /*\n  * Repository-local GIT_* environment variables; see cache.h for details.\n@@ -124,10 +125,18 @@ static char *expand_namespace(const char *raw_namespace)\n \treturn strbuf_detach(&buf, NULL);\n }\n \n-static char *git_path_from_env(const char *envvar, const char *path)\n+static char *git_path_from_env(const char *envvar, const char *path,\n+\t\t\t       int *fromenv)\n {\n \tconst char *value = getenv(envvar);\n-\treturn value ? xstrdup(value) : git_pathdup(\"%s\", path);\n+\tif (!value) {\n+\t\tchar *buf = xmalloc(strlen(git_dir) + strlen(path) + 2);\n+\t\tsprintf(buf, \"%s/%s\", git_dir, path);\n+\t\treturn buf;\n+\t}\n+\tif (fromenv)\n+\t\t*fromenv = 1;\n+\treturn xstrdup(value);\n }\n \n static void setup_git_env(void)\n@@ -140,9 +149,9 @@ static void setup_git_env(void)\n \t\tgit_dir = DEFAULT_GIT_DIR_ENVIRONMENT;\n \tgitfile = read_gitfile(git_dir);\n \tgit_dir = xstrdup(gitfile ? gitfile : git_dir);\n-\tgit_object_dir = git_path_from_env(DB_ENVIRONMENT, \"objects\");\n-\tgit_index_file = git_path_from_env(INDEX_ENVIRONMENT, \"index\");\n-\tgit_graft_file = git_path_from_env(GRAFT_ENVIRONMENT, \"info/grafts\");\n+\tgit_object_dir = git_path_from_env(DB_ENVIRONMENT, \"objects\", &git_db_env);\n+\tgit_index_file = git_path_from_env(INDEX_ENVIRONMENT, \"index\", &git_index_env);\n+\tgit_graft_file = git_path_from_env(GRAFT_ENVIRONMENT, \"info/grafts\", &git_graft_env);\n \tif (getenv(NO_REPLACE_OBJECTS_ENVIRONMENT))\n \t\tcheck_replace_refs = 0;\n \tnamespace = expand_namespace(getenv(GIT_NAMESPACE_ENVIRONMENT));\ndiff --git a/path.c b/path.c\nindex 65881aa..3deb80c 100644\n--- a/path.c\n+++ b/path.c\n@@ -60,13 +60,58 @@ char *mksnpath(char *buf, size_t n, const char *fmt, ...)\n \treturn cleanup_path(buf);\n }\n \n+static int dir_prefix(const char *buf, const char *dir)\n+{\n+\tint len = strlen(dir);\n+\treturn !strncmp(buf, dir, len) &&\n+\t\t(is_dir_sep(buf[len]) || buf[len] == '\\0');\n+}\n+\n+/* $buf =~ m|$dir/+$file| but without regex */\n+static int is_dir_file(const char *buf, const char *dir, const char *file)\n+{\n+\tint len = strlen(dir);\n+\tif (strncmp(buf, dir, len) || !is_dir_sep(buf[len]))\n+\t\treturn 0;\n+\twhile (is_dir_sep(buf[len]))\n+\t\tlen++;\n+\treturn !strcmp(buf + len, file);\n+}\n+\n+static void replace_dir(struct strbuf *buf, int len, const char *newdir)\n+{\n+\tint newlen = strlen(newdir);\n+\tint need_sep = (buf->buf[len] && !is_dir_sep(buf->buf[len])) &&\n+\t\t!is_dir_sep(newdir[newlen - 1]);\n+\tif (need_sep)\n+\t\tlen--;\t /* keep one char, to be replaced with '/'  */\n+\tstrbuf_splice(buf, 0, len, newdir, newlen);\n+\tif (need_sep)\n+\t\tbuf->buf[newlen] = '/';\n+}\n+\n+static void adjust_git_path(struct strbuf *buf, int git_dir_len)\n+{\n+\tconst char *base = buf->buf + git_dir_len;\n+\tif (git_graft_env && is_dir_file(base, \"info\", \"grafts\"))\n+\t\tstrbuf_splice(buf, 0, buf->len,\n+\t\t\t      get_graft_file(), strlen(get_graft_file()));\n+\telse if (git_index_env && !strcmp(base, \"index\"))\n+\t\tstrbuf_splice(buf, 0, buf->len,\n+\t\t\t      get_index_file(), strlen(get_index_file()));\n+\telse if (git_db_env && dir_prefix(base, \"objects\"))\n+\t\treplace_dir(buf, git_dir_len + 7, get_object_directory());\n+}\n+\n static void do_git_path(struct strbuf *buf, const char *fmt, va_list args)\n {\n-\tconst char *git_dir = get_git_dir();\n-\tstrbuf_addstr(buf, git_dir);\n+\tint gitdir_len;\n+\tstrbuf_addstr(buf, get_git_dir());\n \tif (buf->len && !is_dir_sep(buf->buf[buf->len - 1]))\n \t\tstrbuf_addch(buf, '/');\n+\tgitdir_len = buf->len;\n \tstrbuf_vaddf(buf, fmt, args);\n+\tadjust_git_path(buf, gitdir_len);\n \tstrbuf_cleanup_path(buf);\n }\n \ndiff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh\nindex c0143a0..33d2818 100755\n--- a/t/t0060-path-utils.sh\n+++ b/t/t0060-path-utils.sh\n@@ -19,6 +19,14 @@ relative_path() {\n \t\"test \\\"\\$(test-path-utils relative_path '$1' '$2')\\\" = '$expected'\"\n }\n \n+test_git_path() {\n+\ttest_expect_success \"git-path $1 $2 => $3\" \"\n+\t\t$1 git rev-parse --git-path $2 >actual &&\n+\t\techo $3 >expect &&\n+\t\ttest_cmp expect actual\n+\t\"\n+}\n+\n # On Windows, we are using MSYS's bash, which mangles the paths.\n # Absolute paths are anchored at the MSYS installation directory,\n # which means that the path / accounts for this many characters:\n@@ -244,4 +252,15 @@ relative_path \"<null>\"\t\t\"<empty>\"\t./\n relative_path \"<null>\"\t\t\"<null>\"\t./\n relative_path \"<null>\"\t\t/foo/a/b\t./\n \n+test_git_path A=B                info/grafts .git/info/grafts\n+test_git_path GIT_GRAFT_FILE=foo info/grafts foo\n+test_git_path GIT_GRAFT_FILE=foo info/////grafts foo\n+test_git_path GIT_INDEX_FILE=foo index foo\n+test_git_path GIT_INDEX_FILE=foo index/foo .git/index/foo\n+test_git_path GIT_INDEX_FILE=foo index2 .git/index2\n+test_expect_success 'setup fake objects directory foo' 'mkdir foo'\n+test_git_path GIT_OBJECT_DIRECTORY=foo objects foo\n+test_git_path GIT_OBJECT_DIRECTORY=foo objects/foo foo/foo\n+test_git_path GIT_OBJECT_DIRECTORY=foo objects2 .git/objects2\n+\n test_done\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"248614","messageId":"1409387642-24492-8-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1409387642-24492-1-git-send-email-pclouds@gmail.com","subject":"[PATCH 07/32] *.sh: respect $GIT_INDEX_FILE","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-08-30T08:33:37Z","receivedAt":"2014-08-30T08:33:37Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n git-pull.sh  | 2 +-\n git-stash.sh | 2 +-\n 2 files changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/git-pull.sh b/git-pull.sh\nindex 18a394f..6ab0c31 100755\n--- a/git-pull.sh\n+++ b/git-pull.sh\n@@ -240,7 +240,7 @@ test true = \"$rebase\" && {\n \tif ! git rev-parse -q --verify HEAD >/dev/null\n \tthen\n \t\t# On an unborn branch\n-\t\tif test -f \"$GIT_DIR/index\"\n+\t\tif test -f \"$(git rev-parse --git-path index)\"\n \t\tthen\n \t\t\tdie \"$(gettext \"updating an unborn branch with changes added to the index\")\"\n \t\tfi\ndiff --git a/git-stash.sh b/git-stash.sh\nindex bcc757b..393e1ec 100755\n--- a/git-stash.sh\n+++ b/git-stash.sh\n@@ -20,7 +20,7 @@ require_work_tree\n cd_to_toplevel\n \n TMP=\"$GIT_DIR/.git-stash.$$\"\n-TMPindex=${GIT_INDEX_FILE-\"$GIT_DIR/index\"}.stash.$$\n+TMPindex=${GIT_INDEX_FILE-\"$(git rev-parse --git-path index)\"}.stash.$$\n trap 'rm -f \"$TMP-\"* \"$TMPindex\"' 0\n \n ref_stash=refs/stash\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"248615","messageId":"1409387642-24492-9-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1409387642-24492-1-git-send-email-pclouds@gmail.com","subject":"[PATCH 08/32] reflog: avoid constructing .lock path with git_path","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-08-30T08:33:38Z","receivedAt":"2014-08-30T08:33:38Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Among pathnames in $GIT_DIR, e.g. \"index\" or \"packed-refs\", we want to\nautomatically and silently map some of them to the $GIT_DIR of the\nrepository we are borrowing from via $GIT_COMMON_DIR mechanism.  When\nwe formulate the pathname for its lockfile, we want it to be in the\nsame location as its final destination.  \"index\" is not shared and\nneeds to remain in the borrowing repository, while \"packed-refs\" is\nshared and needs to go to the borrowed repository.\n\ngit_path() could be taught about the \".lock\" suffix and map\n\"index.lock\" and \"packed-refs.lock\" the same way their basenames are\nmapped, but instead the caller can help by asking where the basename\n(e.g. \"index\") is mapped to git_path() and then appending \".lock\"\nafter the mapping is done.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n builtin/reflog.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/builtin/reflog.c b/builtin/reflog.c\nindex e8a8fb1..9bd874d 100644\n--- a/builtin/reflog.c\n+++ b/builtin/reflog.c\n@@ -372,7 +372,7 @@ static int expire_reflog(const char *ref, const unsigned char *sha1, int unused,\n \tif (!reflog_exists(ref))\n \t\tgoto finish;\n \tif (!cmd->dry_run) {\n-\t\tnewlog_path = git_pathdup(\"logs/%s.lock\", ref);\n+\t\tnewlog_path = mkpathdup(\"%s.lock\", log_file);\n \t\tcb.newlog = fopen(newlog_path, \"w\");\n \t}\n \n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"248616","messageId":"1409387642-24492-10-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1409387642-24492-1-git-send-email-pclouds@gmail.com","subject":"[PATCH 09/32] fast-import: use git_path() for accessing .git dir instead of get_git_dir()","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-08-30T08:33:39Z","receivedAt":"2014-08-30T08:33:39Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"This allows git_path() to redirect info/fast-import to another place\nif needed\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n fast-import.c | 5 +----\n 1 file changed, 1 insertion(+), 4 deletions(-)\n\ndiff --git a/fast-import.c b/fast-import.c\nindex d9c068b..ea426c4 100644\n--- a/fast-import.c\n+++ b/fast-import.c\n@@ -3103,12 +3103,9 @@ static void parse_progress(void)\n \n static char* make_fast_import_path(const char *path)\n {\n-\tstruct strbuf abs_path = STRBUF_INIT;\n-\n \tif (!relative_marks_paths || is_absolute_path(path))\n \t\treturn xstrdup(path);\n-\tstrbuf_addf(&abs_path, \"%s/info/fast-import/%s\", get_git_dir(), path);\n-\treturn strbuf_detach(&abs_path, NULL);\n+\treturn xstrdup(git_path(\"info/fast-import/%s\", path));\n }\n \n static void option_import_marks(const char *marks,\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"248617","messageId":"1409387642-24492-11-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1409387642-24492-1-git-send-email-pclouds@gmail.com","subject":"[PATCH 10/32] commit: use SEQ_DIR instead of hardcoding \"sequencer\"","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-08-30T08:33:40Z","receivedAt":"2014-08-30T08:33:40Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n builtin/commit.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 461c3b1..4b9f012 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -156,7 +156,7 @@ static void determine_whence(struct wt_status *s)\n \t\twhence = FROM_MERGE;\n \telse if (file_exists(git_path(\"CHERRY_PICK_HEAD\"))) {\n \t\twhence = FROM_CHERRY_PICK;\n-\t\tif (file_exists(git_path(\"sequencer\")))\n+\t\tif (file_exists(git_path(SEQ_DIR)))\n \t\t\tsequencer_in_use = 1;\n \t}\n \telse\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"248618","messageId":"1409387642-24492-12-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1409387642-24492-1-git-send-email-pclouds@gmail.com","subject":"[PATCH 11/32] $GIT_COMMON_DIR: a new environment variable","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-08-30T08:33:41Z","receivedAt":"2014-08-30T08:33:41Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"This variable is intended to support multiple working directories\nattached to a repository. Such a repository may have a main working\ndirectory, created by either \"git init\" or \"git clone\" and one or more\nlinked working directories. These working directories and the main\nrepository share the same repository directory.\n\nIn linked working directories, $GIT_COMMON_DIR must be defined to point\nto the real repository directory and $GIT_DIR points to an unused\nsubdirectory inside $GIT_COMMON_DIR. File locations inside the\nrepository are reorganized from the linked worktree view point:\n\n - worktree-specific such as HEAD, logs/HEAD, index, other top-level\n   refs and unrecognized files are from $GIT_DIR.\n\n - the rest like objects, refs, info, hooks, packed-refs, shallow...\n   are from $GIT_COMMON_DIR (except info/sparse-checkout, but that's\n   a separate patch)\n\nScripts are supposed to retrieve paths in $GIT_DIR with \"git rev-parse\n--git-path\", which will take care of \"$GIT_DIR vs $GIT_COMMON_DIR\"\nbusiness.\n\nThe redirection is done by git_path(), git_pathdup() and\nstrbuf_git_path(). The selected list of paths goes to $GIT_COMMON_DIR,\nnot the other way around in case a developer adds a new\nworktree-specific file and it's accidentally promoted to be shared\nacross repositories (this includes unknown files added by third party\ncommands)\n\nThe list of known files that belong to $GIT_DIR are:\n\nADD_EDIT.patch BISECT_ANCESTORS_OK BISECT_EXPECTED_REV BISECT_LOG\nBISECT_NAMES CHERRY_PICK_HEAD COMMIT_MSG FETCH_HEAD HEAD MERGE_HEAD\nMERGE_MODE MERGE_RR NOTES_EDITMSG NOTES_MERGE_WORKTREE ORIG_HEAD\nREVERT_HEAD SQUASH_MSG TAG_EDITMSG fast_import_crash_* logs/HEAD\nnext-index-* rebase-apply rebase-merge rsync-refs-* sequencer/*\nshallow_*\n\nPath mapping is NOT done for git_path_submodule(). Multi-checkouts are\nnot supported as submodules.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n Documentation/git.txt                  |  8 ++++++\n Documentation/gitrepository-layout.txt | 45 +++++++++++++++++++++++++---------\n cache.h                                |  4 ++-\n environment.c                          | 28 +++++++++++++++------\n path.c                                 | 34 +++++++++++++++++++++++++\n t/t0060-path-utils.sh                  | 16 ++++++++++++\n 6 files changed, 116 insertions(+), 19 deletions(-)\n\ndiff --git a/Documentation/git.txt b/Documentation/git.txt\nindex 7924209..749052f 100644\n--- a/Documentation/git.txt\n+++ b/Documentation/git.txt\n@@ -788,6 +788,14 @@ Git so take care if using Cogito etc.\n \tan explicit repository directory set via 'GIT_DIR' or on the\n \tcommand line.\n \n+'GIT_COMMON_DIR'::\n+\tIf this variable is set to a path, non-worktree files that are\n+\tnormally in $GIT_DIR will be taken from this path\n+\tinstead. Worktree-specific files such as HEAD or index are\n+\ttaken from $GIT_DIR. See linkgit:gitrepository-layout[5] for\n+\tdetails. This variable has lower precedence than other path\n+\tvariables such as GIT_INDEX_FILE, GIT_OBJECT_DIRECTORY...\n+\n Git Commits\n ~~~~~~~~~~~\n 'GIT_AUTHOR_NAME'::\ndiff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt\nindex 17d2ea6..b88ac65 100644\n--- a/Documentation/gitrepository-layout.txt\n+++ b/Documentation/gitrepository-layout.txt\n@@ -46,6 +46,9 @@ of incomplete object store is not suitable to be published for\n use with dumb transports but otherwise is OK as long as\n `objects/info/alternates` points at the object stores it\n borrows from.\n++\n+This directory is ignored if $GIT_COMMON_DIR is set and\n+\"$GIT_COMMON_DIR/objects\" will be used instead.\n \n objects/[0-9a-f][0-9a-f]::\n \tA newly created object is stored in its own file.\n@@ -92,7 +95,8 @@ refs::\n \tReferences are stored in subdirectories of this\n \tdirectory.  The 'git prune' command knows to preserve\n \tobjects reachable from refs found in this directory and\n-\tits subdirectories.\n+\tits subdirectories. This directory is ignored if $GIT_COMMON_DIR\n+\tis set and \"$GIT_COMMON_DIR/refs\" will be used instead.\n \n refs/heads/`name`::\n \trecords tip-of-the-tree commit objects of branch `name`\n@@ -114,7 +118,8 @@ refs/replace/`<obj-sha1>`::\n packed-refs::\n \trecords the same information as refs/heads/, refs/tags/,\n \tand friends record in a more efficient way.  See\n-\tlinkgit:git-pack-refs[1].\n+\tlinkgit:git-pack-refs[1]. This file is ignored if $GIT_COMMON_DIR\n+\tis set and \"$GIT_COMMON_DIR/packed-refs\" will be used instead.\n \n HEAD::\n \tA symref (see glossary) to the `refs/heads/` namespace\n@@ -133,6 +138,11 @@ being a symref to point at the current branch.  Such a state\n is often called 'detached HEAD.'  See linkgit:git-checkout[1]\n for details.\n \n+config::\n+\tRepository specific configuration file. This file is ignored\n+\tif $GIT_COMMON_DIR is set and \"$GIT_COMMON_DIR/config\" will be\n+\tused instead.\n+\n branches::\n \tA slightly deprecated way to store shorthands to be used\n \tto specify a URL to 'git fetch', 'git pull' and 'git push'.\n@@ -140,7 +150,10 @@ branches::\n \t'name' can be given to these commands in place of\n \t'repository' argument.  See the REMOTES section in\n \tlinkgit:git-fetch[1] for details.  This mechanism is legacy\n-\tand not likely to be found in modern repositories.\n+\tand not likely to be found in modern repositories. This\n+\tdirectory is ignored if $GIT_COMMON_DIR is set and\n+\t\"$GIT_COMMON_DIR/branches\" will be used instead.\n+\n \n hooks::\n \tHooks are customization scripts used by various Git\n@@ -149,7 +162,9 @@ hooks::\n \tdefault.  To enable, the `.sample` suffix has to be\n \tremoved from the filename by renaming.\n \tRead linkgit:githooks[5] for more details about\n-\teach hook.\n+\teach hook. This directory is ignored if $GIT_COMMON_DIR is set\n+\tand \"$GIT_COMMON_DIR/hooks\" will be used instead.\n+\n \n index::\n \tThe current index file for the repository.  It is\n@@ -157,7 +172,8 @@ index::\n \n info::\n \tAdditional information about the repository is recorded\n-\tin this directory.\n+\tin this directory. This directory is ignored if $GIT_COMMON_DIR\n+\tis set and \"$GIT_COMMON_DIR/index\" will be used instead.\n \n info/refs::\n \tThis file helps dumb transports discover what refs are\n@@ -197,12 +213,15 @@ remotes::\n \twhen interacting with remote repositories via 'git fetch',\n \t'git pull' and 'git push' commands.  See the REMOTES section\n \tin linkgit:git-fetch[1] for details.  This mechanism is legacy\n-\tand not likely to be found in modern repositories.\n+\tand not likely to be found in modern repositories. This\n+\tdirectory is ignored if $GIT_COMMON_DIR is set and\n+\t\"$GIT_COMMON_DIR/remotes\" will be used instead.\n \n logs::\n-\tRecords of changes made to refs are stored in this\n-\tdirectory.  See linkgit:git-update-ref[1]\n-\tfor more information.\n+\tRecords of changes made to refs are stored in this directory.\n+\tSee linkgit:git-update-ref[1] for more information. This\n+\tdirectory is ignored $GIT_COMMON_DIR is set and\n+\t\"$GIT_COMMON_DIR/logs\" will be used instead.\n \n logs/refs/heads/`name`::\n \tRecords all changes made to the branch tip named `name`.\n@@ -213,10 +232,14 @@ logs/refs/tags/`name`::\n shallow::\n \tThis is similar to `info/grafts` but is internally used\n \tand maintained by shallow clone mechanism.  See `--depth`\n-\toption to linkgit:git-clone[1] and linkgit:git-fetch[1].\n+\toption to linkgit:git-clone[1] and linkgit:git-fetch[1]. This\n+\tfile is ignored if $GIT_COMMON_DIR is set and\n+\t\"$GIT_COMMON_DIR/shallow\" will be used instead.\n \n modules::\n-\tContains the git-repositories of the submodules.\n+\tContains the git-repositories of the submodules. This\n+\tdirectory is ignored if $GIT_COMMON_DIR is set and\n+\t\"$GIT_COMMON_DIR/modules\" will be used instead.\n \n SEE ALSO\n --------\ndiff --git a/cache.h b/cache.h\nindex 0128b9a..b606ee4 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -362,6 +362,7 @@ static inline enum object_type object_type(unsigned int mode)\n \n /* Double-check local_repo_env below if you add to this list. */\n #define GIT_DIR_ENVIRONMENT \"GIT_DIR\"\n+#define GIT_COMMON_DIR_ENVIRONMENT \"GIT_COMMON_DIR\"\n #define GIT_NAMESPACE_ENVIRONMENT \"GIT_NAMESPACE\"\n #define GIT_WORK_TREE_ENVIRONMENT \"GIT_WORK_TREE\"\n #define GIT_PREFIX_ENVIRONMENT \"GIT_PREFIX\"\n@@ -415,6 +416,7 @@ extern int is_inside_git_dir(void);\n extern char *git_work_tree_cfg;\n extern int is_inside_work_tree(void);\n extern const char *get_git_dir(void);\n+extern const char *get_git_common_dir(void);\n extern int is_git_directory(const char *path);\n extern char *get_object_directory(void);\n extern char *get_index_file(void);\n@@ -612,7 +614,7 @@ extern int fsync_object_files;\n extern int core_preload_index;\n extern int core_apply_sparse_checkout;\n extern int precomposed_unicode;\n-extern int git_db_env, git_index_env, git_graft_env;\n+extern int git_db_env, git_index_env, git_graft_env, git_common_dir_env;\n \n /*\n  * The character that begins a commented line in user-editable file\ndiff --git a/environment.c b/environment.c\nindex fee12a6..78a07e4 100644\n--- a/environment.c\n+++ b/environment.c\n@@ -81,9 +81,9 @@ static char *work_tree;\n static const char *namespace;\n static size_t namespace_len;\n \n-static const char *git_dir;\n+static const char *git_dir, *git_common_dir;\n static char *git_object_dir, *git_index_file, *git_graft_file;\n-int git_db_env, git_index_env, git_graft_env;\n+int git_db_env, git_index_env, git_graft_env, git_common_dir_env;\n \n /*\n  * Repository-local GIT_* environment variables; see cache.h for details.\n@@ -125,8 +125,8 @@ static char *expand_namespace(const char *raw_namespace)\n \treturn strbuf_detach(&buf, NULL);\n }\n \n-static char *git_path_from_env(const char *envvar, const char *path,\n-\t\t\t       int *fromenv)\n+static char *git_path_from_env(const char *envvar, const char *git_dir,\n+\t\t\t       const char *path, int *fromenv)\n {\n \tconst char *value = getenv(envvar);\n \tif (!value) {\n@@ -149,9 +149,18 @@ static void setup_git_env(void)\n \t\tgit_dir = DEFAULT_GIT_DIR_ENVIRONMENT;\n \tgitfile = read_gitfile(git_dir);\n \tgit_dir = xstrdup(gitfile ? gitfile : git_dir);\n-\tgit_object_dir = git_path_from_env(DB_ENVIRONMENT, \"objects\", &git_db_env);\n-\tgit_index_file = git_path_from_env(INDEX_ENVIRONMENT, \"index\", &git_index_env);\n-\tgit_graft_file = git_path_from_env(GRAFT_ENVIRONMENT, \"info/grafts\", &git_graft_env);\n+\tgit_common_dir = getenv(GIT_COMMON_DIR_ENVIRONMENT);\n+\tif (git_common_dir) {\n+\t\tgit_common_dir_env = 1;\n+\t\tgit_common_dir = xstrdup(git_common_dir);\n+\t} else\n+\t\tgit_common_dir = git_dir;\n+\tgit_object_dir = git_path_from_env(DB_ENVIRONMENT, git_common_dir,\n+\t\t\t\t\t   \"objects\", &git_db_env);\n+\tgit_index_file = git_path_from_env(INDEX_ENVIRONMENT, git_dir,\n+\t\t\t\t\t   \"index\", &git_index_env);\n+\tgit_graft_file = git_path_from_env(GRAFT_ENVIRONMENT, git_common_dir,\n+\t\t\t\t\t   \"info/grafts\", &git_graft_env);\n \tif (getenv(NO_REPLACE_OBJECTS_ENVIRONMENT))\n \t\tcheck_replace_refs = 0;\n \tnamespace = expand_namespace(getenv(GIT_NAMESPACE_ENVIRONMENT));\n@@ -174,6 +183,11 @@ const char *get_git_dir(void)\n \treturn git_dir;\n }\n \n+const char *get_git_common_dir(void)\n+{\n+\treturn git_common_dir;\n+}\n+\n const char *get_git_namespace(void)\n {\n \tif (!namespace)\ndiff --git a/path.c b/path.c\nindex 3deb80c..8a6586c 100644\n--- a/path.c\n+++ b/path.c\n@@ -90,6 +90,38 @@ static void replace_dir(struct strbuf *buf, int len, const char *newdir)\n \t\tbuf->buf[newlen] = '/';\n }\n \n+static const char *common_list[] = {\n+\t\"/branches\", \"/hooks\", \"/info\", \"/logs\", \"/lost-found\", \"/modules\",\n+\t\"/objects\", \"/refs\", \"/remotes\", \"/rr-cache\", \"/svn\",\n+\t\"config\", \"gc.pid\", \"packed-refs\", \"shallow\",\n+\tNULL\n+};\n+\n+static void update_common_dir(struct strbuf *buf, int git_dir_len)\n+{\n+\tchar *base = buf->buf + git_dir_len;\n+\tconst char **p;\n+\n+\tif (is_dir_file(base, \"logs\", \"HEAD\"))\n+\t\treturn;\t/* keep this in $GIT_DIR */\n+\tfor (p = common_list; *p; p++) {\n+\t\tconst char *path = *p;\n+\t\tint is_dir = 0;\n+\t\tif (*path == '/') {\n+\t\t\tpath++;\n+\t\t\tis_dir = 1;\n+\t\t}\n+\t\tif (is_dir && dir_prefix(base, path)) {\n+\t\t\treplace_dir(buf, git_dir_len, get_git_common_dir());\n+\t\t\treturn;\n+\t\t}\n+\t\tif (!is_dir && !strcmp(base, path)) {\n+\t\t\treplace_dir(buf, git_dir_len, get_git_common_dir());\n+\t\t\treturn;\n+\t\t}\n+\t}\n+}\n+\n static void adjust_git_path(struct strbuf *buf, int git_dir_len)\n {\n \tconst char *base = buf->buf + git_dir_len;\n@@ -101,6 +133,8 @@ static void adjust_git_path(struct strbuf *buf, int git_dir_len)\n \t\t\t      get_index_file(), strlen(get_index_file()));\n \telse if (git_db_env && dir_prefix(base, \"objects\"))\n \t\treplace_dir(buf, git_dir_len + 7, get_object_directory());\n+\telse if (git_common_dir_env)\n+\t\tupdate_common_dir(buf, git_dir_len);\n }\n \n static void do_git_path(struct strbuf *buf, const char *fmt, va_list args)\ndiff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh\nindex 33d2818..f5d6f80 100755\n--- a/t/t0060-path-utils.sh\n+++ b/t/t0060-path-utils.sh\n@@ -262,5 +262,21 @@ test_expect_success 'setup fake objects directory foo' 'mkdir foo'\n test_git_path GIT_OBJECT_DIRECTORY=foo objects foo\n test_git_path GIT_OBJECT_DIRECTORY=foo objects/foo foo/foo\n test_git_path GIT_OBJECT_DIRECTORY=foo objects2 .git/objects2\n+test_expect_success 'setup common repository' 'git --git-dir=bar init'\n+test_git_path GIT_COMMON_DIR=bar index                    .git/index\n+test_git_path GIT_COMMON_DIR=bar HEAD                     .git/HEAD\n+test_git_path GIT_COMMON_DIR=bar logs/HEAD                .git/logs/HEAD\n+test_git_path GIT_COMMON_DIR=bar objects                  bar/objects\n+test_git_path GIT_COMMON_DIR=bar objects/bar              bar/objects/bar\n+test_git_path GIT_COMMON_DIR=bar info/exclude             bar/info/exclude\n+test_git_path GIT_COMMON_DIR=bar info/grafts              bar/info/grafts\n+test_git_path GIT_COMMON_DIR=bar remotes/bar              bar/remotes/bar\n+test_git_path GIT_COMMON_DIR=bar branches/bar             bar/branches/bar\n+test_git_path GIT_COMMON_DIR=bar logs/refs/heads/master   bar/logs/refs/heads/master\n+test_git_path GIT_COMMON_DIR=bar refs/heads/master        bar/refs/heads/master\n+test_git_path GIT_COMMON_DIR=bar hooks/me                 bar/hooks/me\n+test_git_path GIT_COMMON_DIR=bar config                   bar/config\n+test_git_path GIT_COMMON_DIR=bar packed-refs              bar/packed-refs\n+test_git_path GIT_COMMON_DIR=bar shallow                  bar/shallow\n \n test_done\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"248620","messageId":"1409387642-24492-13-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1409387642-24492-1-git-send-email-pclouds@gmail.com","subject":"[PATCH 12/32] git-sh-setup.sh: use rev-parse --git-path to get $GIT_DIR/objects","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-08-30T08:33:42Z","receivedAt":"2014-08-30T08:33:42Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"If $GIT_COMMON_DIR is set, $GIT_OBJECT_DIRECTORY should be\n$GIT_COMMON_DIR/objects, not $GIT_DIR/objects. Just let rev-parse\n--git-path handle it.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n git-sh-setup.sh | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/git-sh-setup.sh b/git-sh-setup.sh\nindex 9447980..d3dbb2f 100644\n--- a/git-sh-setup.sh\n+++ b/git-sh-setup.sh\n@@ -345,7 +345,7 @@ then\n \t\techo >&2 \"Unable to determine absolute path of git directory\"\n \t\texit 1\n \t}\n-\t: ${GIT_OBJECT_DIRECTORY=\"$GIT_DIR/objects\"}\n+\t: ${GIT_OBJECT_DIRECTORY=\"$(git rev-parse --git-path objects)\"}\n fi\n \n peel_committish () {\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"248619","messageId":"1409387642-24492-14-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1409387642-24492-1-git-send-email-pclouds@gmail.com","subject":"[PATCH 13/32] *.sh: avoid hardcoding $GIT_DIR/hooks/...","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-08-30T08:33:43Z","receivedAt":"2014-08-30T08:33:43Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"If $GIT_COMMON_DIR is set, it should be $GIT_COMMON_DIR/hooks/, not\n$GIT_DIR/hooks/. Just let rev-parse --git-path handle it.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n git-am.sh                              | 22 +++++++++++-----------\n git-rebase--interactive.sh             |  6 +++---\n git-rebase--merge.sh                   |  6 ++----\n git-rebase.sh                          |  4 ++--\n templates/hooks--applypatch-msg.sample |  4 ++--\n templates/hooks--pre-applypatch.sample |  4 ++--\n 6 files changed, 22 insertions(+), 24 deletions(-)\n\ndiff --git a/git-am.sh b/git-am.sh\nindex ee61a77..66803d1 100755\n--- a/git-am.sh\n+++ b/git-am.sh\n@@ -810,10 +810,10 @@ To restore the original branch and stop patching run \\\"\\$cmdline --abort\\\".\"\n \t\tcontinue\n \tfi\n \n-\tif test -x \"$GIT_DIR\"/hooks/applypatch-msg\n+\thook=\"$(git rev-parse --git-path hooks/applypatch-msg)\"\n+\tif test -x \"$hook\"\n \tthen\n-\t\t\"$GIT_DIR\"/hooks/applypatch-msg \"$dotest/final-commit\" ||\n-\t\tstop_here $this\n+\t\t\"$hook\" \"$dotest/final-commit\" || stop_here $this\n \tfi\n \n \tif test -f \"$dotest/final-commit\"\n@@ -887,9 +887,10 @@ did you forget to use 'git add'?\"\n \t\tstop_here_user_resolve $this\n \tfi\n \n-\tif test -x \"$GIT_DIR\"/hooks/pre-applypatch\n+\thook=\"$(git rev-parse --git-path hooks/pre-applypatch)\"\n+\tif test -x \"$hook\"\n \tthen\n-\t\t\"$GIT_DIR\"/hooks/pre-applypatch || stop_here $this\n+\t\t\"$hook\" || stop_here $this\n \tfi\n \n \ttree=$(git write-tree) &&\n@@ -916,18 +917,17 @@ did you forget to use 'git add'?\"\n \t\techo \"$(cat \"$dotest/original-commit\") $commit\" >> \"$dotest/rewritten\"\n \tfi\n \n-\tif test -x \"$GIT_DIR\"/hooks/post-applypatch\n-\tthen\n-\t\t\"$GIT_DIR\"/hooks/post-applypatch\n-\tfi\n+\thook=\"$(git rev-parse --git-path hooks/post-applypatch)\"\n+\ttest -x \"$hook\" && \"$hook\"\n \n \tgo_next\n done\n \n if test -s \"$dotest\"/rewritten; then\n     git notes copy --for-rewrite=rebase < \"$dotest\"/rewritten\n-    if test -x \"$GIT_DIR\"/hooks/post-rewrite; then\n-\t\"$GIT_DIR\"/hooks/post-rewrite rebase < \"$dotest\"/rewritten\n+    hook=\"$(git rev-parse --git-path hooks/post-rewrite)\"\n+    if test -x \"$hook\"; then\n+\t\"$hook\" rebase < \"$dotest\"/rewritten\n     fi\n fi\n \ndiff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh\nindex 7e1eda0..e8995f9 100644\n--- a/git-rebase--interactive.sh\n+++ b/git-rebase--interactive.sh\n@@ -642,9 +642,9 @@ do_next () {\n \t\tgit notes copy --for-rewrite=rebase < \"$rewritten_list\" ||\n \t\ttrue # we don't care if this copying failed\n \t} &&\n-\tif test -x \"$GIT_DIR\"/hooks/post-rewrite &&\n-\t\ttest -s \"$rewritten_list\"; then\n-\t\t\"$GIT_DIR\"/hooks/post-rewrite rebase < \"$rewritten_list\"\n+\thook=\"$(git rev-parse --git-path hooks/post-rewrite)\"\n+\tif test -x \"$hook\" && test -s \"$rewritten_list\"; then\n+\t\t\"$hook\" rebase < \"$rewritten_list\"\n \t\ttrue # we don't care if this hook failed\n \tfi &&\n \twarn \"Successfully rebased and updated $head_name.\"\ndiff --git a/git-rebase--merge.sh b/git-rebase--merge.sh\nindex d3fb67d..2cc2a6d 100644\n--- a/git-rebase--merge.sh\n+++ b/git-rebase--merge.sh\n@@ -94,10 +94,8 @@ finish_rb_merge () {\n \tif test -s \"$state_dir\"/rewritten\n \tthen\n \t\tgit notes copy --for-rewrite=rebase <\"$state_dir\"/rewritten\n-\t\tif test -x \"$GIT_DIR\"/hooks/post-rewrite\n-\t\tthen\n-\t\t\t\"$GIT_DIR\"/hooks/post-rewrite rebase <\"$state_dir\"/rewritten\n-\t\tfi\n+\t\thook=\"$(git rev-parse --git-path hooks/post-rewrite)\"\n+\t\ttest -x \"$hook\" && \"$hook\" rebase <\"$state_dir\"/rewritten\n \tfi\n \tsay All done.\n }\ndiff --git a/git-rebase.sh b/git-rebase.sh\nindex 06c810b..d60e710 100755\n--- a/git-rebase.sh\n+++ b/git-rebase.sh\n@@ -201,9 +201,9 @@ run_specific_rebase () {\n \n run_pre_rebase_hook () {\n \tif test -z \"$ok_to_skip_pre_rebase\" &&\n-\t   test -x \"$GIT_DIR/hooks/pre-rebase\"\n+\t   test -x \"$(git rev-parse --git-path hooks/pre-rebase)\"\n \tthen\n-\t\t\"$GIT_DIR/hooks/pre-rebase\" ${1+\"$@\"} ||\n+\t\t\"$(git rev-parse --git-path hooks/pre-rebase)\" ${1+\"$@\"} ||\n \t\tdie \"$(gettext \"The pre-rebase hook refused to rebase.\")\"\n \tfi\n }\ndiff --git a/templates/hooks--applypatch-msg.sample b/templates/hooks--applypatch-msg.sample\nindex 8b2a2fe..a5d7b84 100755\n--- a/templates/hooks--applypatch-msg.sample\n+++ b/templates/hooks--applypatch-msg.sample\n@@ -10,6 +10,6 @@\n # To enable this hook, rename this file to \"applypatch-msg\".\n \n . git-sh-setup\n-test -x \"$GIT_DIR/hooks/commit-msg\" &&\n-\texec \"$GIT_DIR/hooks/commit-msg\" ${1+\"$@\"}\n+commitmsg=\"$(git rev-parse --git-path hooks/commit-msg)\"\n+test -x \"$commitmsg\" && exec \"$commitmsg\" ${1+\"$@\"}\n :\ndiff --git a/templates/hooks--pre-applypatch.sample b/templates/hooks--pre-applypatch.sample\nindex b1f187c..4142082 100755\n--- a/templates/hooks--pre-applypatch.sample\n+++ b/templates/hooks--pre-applypatch.sample\n@@ -9,6 +9,6 @@\n # To enable this hook, rename this file to \"pre-applypatch\".\n \n . git-sh-setup\n-test -x \"$GIT_DIR/hooks/pre-commit\" &&\n-\texec \"$GIT_DIR/hooks/pre-commit\" ${1+\"$@\"}\n+precommit=\"$(git rev-parse --git-path hooks/pre-commit)\"\n+test -x \"$precommit\" && exec \"$precommit\" ${1+\"$@\"}\n :\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"248621","messageId":"1409387642-24492-15-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1409387642-24492-1-git-send-email-pclouds@gmail.com","subject":"[PATCH 14/32] git-stash: avoid hardcoding $GIT_DIR/logs/....","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-08-30T08:33:44Z","receivedAt":"2014-08-30T08:33:44Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n git-stash.sh | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/git-stash.sh b/git-stash.sh\nindex 393e1ec..41f8f6b 100755\n--- a/git-stash.sh\n+++ b/git-stash.sh\n@@ -184,7 +184,7 @@ store_stash () {\n \tfi\n \n \t# Make sure the reflog for stash is kept.\n-\t: >>\"$GIT_DIR/logs/$ref_stash\"\n+\t: >>\"$(git rev-parse --git-path logs/$ref_stash)\"\n \tgit update-ref -m \"$stash_msg\" $ref_stash $w_commit\n \tret=$?\n \ttest $ret != 0 && test -z $quiet &&\n@@ -259,7 +259,7 @@ save_stash () {\n \t\tsay \"$(gettext \"No local changes to save\")\"\n \t\texit 0\n \tfi\n-\ttest -f \"$GIT_DIR/logs/$ref_stash\" ||\n+\ttest -f \"$(git rev-parse --git-path logs/$ref_stash)\" ||\n \t\tclear_stash || die \"$(gettext \"Cannot initialize stash\")\"\n \n \tcreate_stash \"$stash_msg\" $untracked\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"248623","messageId":"1409387642-24492-16-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1409387642-24492-1-git-send-email-pclouds@gmail.com","subject":"[PATCH 15/32] setup.c: convert is_git_directory() to use strbuf","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-08-30T08:33:45Z","receivedAt":"2014-08-30T08:33:45Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n setup.c | 37 +++++++++++++++++++++----------------\n 1 file changed, 21 insertions(+), 16 deletions(-)\n\ndiff --git a/setup.c b/setup.c\nindex 0a22f8b..425fd79 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -238,31 +238,36 @@ void verify_non_filename(const char *prefix, const char *arg)\n  */\n int is_git_directory(const char *suspect)\n {\n-\tchar path[PATH_MAX];\n-\tsize_t len = strlen(suspect);\n+\tstruct strbuf path = STRBUF_INIT;\n+\tint ret = 0;\n+\tsize_t len;\n \n-\tif (PATH_MAX <= len + strlen(\"/objects\"))\n-\t\tdie(\"Too long path: %.*s\", 60, suspect);\n-\tstrcpy(path, suspect);\n+\tstrbuf_addstr(&path, suspect);\n+\tlen = path.len;\n \tif (getenv(DB_ENVIRONMENT)) {\n \t\tif (access(getenv(DB_ENVIRONMENT), X_OK))\n-\t\t\treturn 0;\n+\t\t\tgoto done;\n \t}\n \telse {\n-\t\tstrcpy(path + len, \"/objects\");\n-\t\tif (access(path, X_OK))\n-\t\t\treturn 0;\n+\t\tstrbuf_addstr(&path, \"/objects\");\n+\t\tif (access(path.buf, X_OK))\n+\t\t\tgoto done;\n \t}\n \n-\tstrcpy(path + len, \"/refs\");\n-\tif (access(path, X_OK))\n-\t\treturn 0;\n+\tstrbuf_setlen(&path, len);\n+\tstrbuf_addstr(&path, \"/refs\");\n+\tif (access(path.buf, X_OK))\n+\t\tgoto done;\n \n-\tstrcpy(path + len, \"/HEAD\");\n-\tif (validate_headref(path))\n-\t\treturn 0;\n+\tstrbuf_setlen(&path, len);\n+\tstrbuf_addstr(&path, \"/HEAD\");\n+\tif (validate_headref(path.buf))\n+\t\tgoto done;\n \n-\treturn 1;\n+\tret = 1;\n+done:\n+\tstrbuf_release(&path);\n+\treturn ret;\n }\n \n int is_inside_git_dir(void)\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"248622","messageId":"1409387642-24492-17-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1409387642-24492-1-git-send-email-pclouds@gmail.com","subject":"[PATCH 16/32] setup.c: detect $GIT_COMMON_DIR in is_git_directory()","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-08-30T08:33:46Z","receivedAt":"2014-08-30T08:33:46Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"If the file \"$GIT_DIR/commondir\" exists, it contains the value of\n$GIT_COMMON_DIR.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n Documentation/gitrepository-layout.txt |  7 ++++++\n setup.c                                | 43 +++++++++++++++++++++++++++++-----\n 2 files changed, 44 insertions(+), 6 deletions(-)\n\ndiff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt\nindex b88ac65..58d1087 100644\n--- a/Documentation/gitrepository-layout.txt\n+++ b/Documentation/gitrepository-layout.txt\n@@ -236,6 +236,13 @@ shallow::\n \tfile is ignored if $GIT_COMMON_DIR is set and\n \t\"$GIT_COMMON_DIR/shallow\" will be used instead.\n \n+commondir::\n+\tIf this file exists, $GIT_COMMON_DIR (see linkgit:git[1]) will\n+\tbe set to the path specified in this file if it is not\n+\texplicitly set. If the specified path is relative, it is\n+\trelative to $GIT_DIR. The repository with commondir is\n+\tincomplete without the repository pointed by \"commondir\".\n+\n modules::\n \tContains the git-repositories of the submodules. This\n \tdirectory is ignored if $GIT_COMMON_DIR is set and\ndiff --git a/setup.c b/setup.c\nindex 425fd79..176d505 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -224,6 +224,33 @@ void verify_non_filename(const char *prefix, const char *arg)\n \t    \"'git <command> [<revision>...] -- [<file>...]'\", arg);\n }\n \n+static void get_common_dir(struct strbuf *sb, const char *gitdir)\n+{\n+\tstruct strbuf data = STRBUF_INIT;\n+\tstruct strbuf path = STRBUF_INIT;\n+\tconst char *git_common_dir = getenv(GIT_COMMON_DIR_ENVIRONMENT);\n+\tif (git_common_dir) {\n+\t\tstrbuf_addstr(sb, git_common_dir);\n+\t\treturn;\n+\t}\n+\tstrbuf_addf(&path, \"%s/commondir\", gitdir);\n+\tif (file_exists(path.buf)) {\n+\t\tif (strbuf_read_file(&data, path.buf, 0) <= 0)\n+\t\t\tdie_errno(_(\"failed to read %s\"), path.buf);\n+\t\twhile (data.len && (data.buf[data.len - 1] == '\\n' ||\n+\t\t\t\t    data.buf[data.len - 1] == '\\r'))\n+\t\t\tdata.len--;\n+\t\tdata.buf[data.len] = '\\0';\n+\t\tstrbuf_reset(&path);\n+\t\tif (!is_absolute_path(data.buf))\n+\t\t\tstrbuf_addf(&path, \"%s/\", gitdir);\n+\t\tstrbuf_addbuf(&path, &data);\n+\t\tstrbuf_addstr(sb, real_path(path.buf));\n+\t} else\n+\t\tstrbuf_addstr(sb, gitdir);\n+\tstrbuf_release(&data);\n+\tstrbuf_release(&path);\n+}\n \n /*\n  * Test if it looks like we're at a git directory.\n@@ -242,13 +269,22 @@ int is_git_directory(const char *suspect)\n \tint ret = 0;\n \tsize_t len;\n \n-\tstrbuf_addstr(&path, suspect);\n+\t/* Check worktree-related signatures */\n+\tstrbuf_addf(&path, \"%s/HEAD\", suspect);\n+\tif (validate_headref(path.buf))\n+\t\tgoto done;\n+\n+\tstrbuf_reset(&path);\n+\tget_common_dir(&path, suspect);\n \tlen = path.len;\n+\n+\t/* Check non-worktree-related signatures */\n \tif (getenv(DB_ENVIRONMENT)) {\n \t\tif (access(getenv(DB_ENVIRONMENT), X_OK))\n \t\t\tgoto done;\n \t}\n \telse {\n+\t\tstrbuf_setlen(&path, len);\n \t\tstrbuf_addstr(&path, \"/objects\");\n \t\tif (access(path.buf, X_OK))\n \t\t\tgoto done;\n@@ -259,11 +295,6 @@ int is_git_directory(const char *suspect)\n \tif (access(path.buf, X_OK))\n \t\tgoto done;\n \n-\tstrbuf_setlen(&path, len);\n-\tstrbuf_addstr(&path, \"/HEAD\");\n-\tif (validate_headref(path.buf))\n-\t\tgoto done;\n-\n \tret = 1;\n done:\n \tstrbuf_release(&path);\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"248624","messageId":"1409387642-24492-18-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1409387642-24492-1-git-send-email-pclouds@gmail.com","subject":"[PATCH 17/32] setup.c: convert check_repository_format_gently to use strbuf","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-08-30T08:33:47Z","receivedAt":"2014-08-30T08:33:47Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n setup.c | 12 ++++++++----\n 1 file changed, 8 insertions(+), 4 deletions(-)\n\ndiff --git a/setup.c b/setup.c\nindex 176d505..a17389f 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -342,7 +342,9 @@ void setup_work_tree(void)\n \n static int check_repository_format_gently(const char *gitdir, int *nongit_ok)\n {\n-\tchar repo_config[PATH_MAX+1];\n+\tstruct strbuf sb = STRBUF_INIT;\n+\tconst char *repo_config;\n+\tint ret = 0;\n \n \t/*\n \t * git_config() can't be used here because it calls git_pathdup()\n@@ -353,7 +355,8 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)\n \t * Use a gentler version of git_config() to check if this repo\n \t * is a good one.\n \t */\n-\tsnprintf(repo_config, PATH_MAX, \"%s/config\", gitdir);\n+\tstrbuf_addf(&sb, \"%s/config\", gitdir);\n+\trepo_config = sb.buf;\n \tgit_config_early(check_repository_format_version, NULL, repo_config);\n \tif (GIT_REPO_VERSION < repository_format_version) {\n \t\tif (!nongit_ok)\n@@ -363,9 +366,10 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)\n \t\t\tGIT_REPO_VERSION, repository_format_version);\n \t\twarning(\"Please upgrade Git\");\n \t\t*nongit_ok = -1;\n-\t\treturn -1;\n+\t\tret = -1;\n \t}\n-\treturn 0;\n+\tstrbuf_release(&sb);\n+\treturn ret;\n }\n \n /*\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"248625","messageId":"1409387642-24492-19-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1409387642-24492-1-git-send-email-pclouds@gmail.com","subject":"[PATCH 18/32] setup.c: detect $GIT_COMMON_DIR check_repository_format_gently()","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-08-30T08:33:48Z","receivedAt":"2014-08-30T08:33:48Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n setup.c | 6 ++++--\n 1 file changed, 4 insertions(+), 2 deletions(-)\n\ndiff --git a/setup.c b/setup.c\nindex a17389f..79f79f2 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -346,6 +346,10 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)\n \tconst char *repo_config;\n \tint ret = 0;\n \n+\tget_common_dir(&sb, gitdir);\n+\tstrbuf_addstr(&sb, \"/config\");\n+\trepo_config = sb.buf;\n+\n \t/*\n \t * git_config() can't be used here because it calls git_pathdup()\n \t * to get $GIT_CONFIG/config. That call will make setup_git_env()\n@@ -355,8 +359,6 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)\n \t * Use a gentler version of git_config() to check if this repo\n \t * is a good one.\n \t */\n-\tstrbuf_addf(&sb, \"%s/config\", gitdir);\n-\trepo_config = sb.buf;\n \tgit_config_early(check_repository_format_version, NULL, repo_config);\n \tif (GIT_REPO_VERSION < repository_format_version) {\n \t\tif (!nongit_ok)\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"248626","messageId":"1409387642-24492-20-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1409387642-24492-1-git-send-email-pclouds@gmail.com","subject":"[PATCH 19/32] setup.c: support multi-checkout repo setup","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-08-30T08:33:49Z","receivedAt":"2014-08-30T08:33:49Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"The repo setup procedure is updated to detect $GIT_DIR/commondir and\nset $GIT_COMMON_DIR properly.\n\nThe core.worktree is ignored when $GIT_COMMON_DIR is set. This is\nbecause the config file is shared in multi-checkout setup, but\ncheckout directories _are_ different. Making core.worktree effective\nin all checkouts mean it's back to a single checkout.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n Documentation/config.txt        |  2 ++\n Documentation/git-rev-parse.txt |  3 ++\n builtin/rev-parse.c             |  4 +++\n cache.h                         |  1 +\n environment.c                   |  8 ++---\n setup.c                         | 33 +++++++++++++-----\n t/t1501-worktree.sh             | 76 +++++++++++++++++++++++++++++++++++++++++\n t/t1510-repo-setup.sh           |  1 +\n trace.c                         |  1 +\n 9 files changed, 115 insertions(+), 14 deletions(-)\n\ndiff --git a/Documentation/config.txt b/Documentation/config.txt\nindex 1d718bd..286e539 100644\n--- a/Documentation/config.txt\n+++ b/Documentation/config.txt\n@@ -380,6 +380,8 @@ false), while all other repositories are assumed to be bare (bare\n \n core.worktree::\n \tSet the path to the root of the working tree.\n+\tIf GIT_COMMON_DIR environment variable is set, core.worktree\n+\tis ignored and not used for determining the root of working tree.\n \tThis can be overridden by the GIT_WORK_TREE environment\n \tvariable and the '--work-tree' command-line option.\n \tThe value can be an absolute path or relative to the path to\ndiff --git a/Documentation/git-rev-parse.txt b/Documentation/git-rev-parse.txt\nindex 9465399..f1867d3 100644\n--- a/Documentation/git-rev-parse.txt\n+++ b/Documentation/git-rev-parse.txt\n@@ -215,6 +215,9 @@ If `$GIT_DIR` is not defined and the current directory\n is not detected to lie in a Git repository or work tree\n print a message to stderr and exit with nonzero status.\n \n+--git-common-dir::\n+\tShow `$GIT_COMMON_DIR` if defined, else `$GIT_DIR`.\n+\n --is-inside-git-dir::\n \tWhen the current working directory is below the repository\n \tdirectory print \"true\", otherwise \"false\".\ndiff --git a/builtin/rev-parse.c b/builtin/rev-parse.c\nindex 7606d43..29475c5 100644\n--- a/builtin/rev-parse.c\n+++ b/builtin/rev-parse.c\n@@ -757,6 +757,10 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)\n \t\t\t\tprintf(\"%s%s.git\\n\", cwd, len && cwd[len-1] != '/' ? \"/\" : \"\");\n \t\t\t\tcontinue;\n \t\t\t}\n+\t\t\tif (!strcmp(arg, \"--git-common-dir\")) {\n+\t\t\t\tputs(get_git_common_dir());\n+\t\t\t\tcontinue;\n+\t\t\t}\n \t\t\tif (!strcmp(arg, \"--resolve-git-dir\")) {\n \t\t\t\tconst char *gitdir = argv[++i];\n \t\t\t\tif (!gitdir)\ndiff --git a/cache.h b/cache.h\nindex b606ee4..e3ff7dc 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -422,6 +422,7 @@ extern char *get_object_directory(void);\n extern char *get_index_file(void);\n extern char *get_graft_file(void);\n extern int set_git_dir(const char *path);\n+extern int get_common_dir(struct strbuf *sb, const char *gitdir);\n extern const char *get_git_namespace(void);\n extern const char *strip_namespace(const char *namespaced_ref);\n extern const char *get_git_work_tree(void);\ndiff --git a/environment.c b/environment.c\nindex 78a07e4..d5b0788 100644\n--- a/environment.c\n+++ b/environment.c\n@@ -141,6 +141,7 @@ static char *git_path_from_env(const char *envvar, const char *git_dir,\n \n static void setup_git_env(void)\n {\n+\tstruct strbuf sb = STRBUF_INIT;\n \tconst char *gitfile;\n \tconst char *shallow_file;\n \n@@ -149,12 +150,9 @@ static void setup_git_env(void)\n \t\tgit_dir = DEFAULT_GIT_DIR_ENVIRONMENT;\n \tgitfile = read_gitfile(git_dir);\n \tgit_dir = xstrdup(gitfile ? gitfile : git_dir);\n-\tgit_common_dir = getenv(GIT_COMMON_DIR_ENVIRONMENT);\n-\tif (git_common_dir) {\n+\tif (get_common_dir(&sb, git_dir))\n \t\tgit_common_dir_env = 1;\n-\t\tgit_common_dir = xstrdup(git_common_dir);\n-\t} else\n-\t\tgit_common_dir = git_dir;\n+\tgit_common_dir = strbuf_detach(&sb, NULL);\n \tgit_object_dir = git_path_from_env(DB_ENVIRONMENT, git_common_dir,\n \t\t\t\t\t   \"objects\", &git_db_env);\n \tgit_index_file = git_path_from_env(INDEX_ENVIRONMENT, git_dir,\ndiff --git a/setup.c b/setup.c\nindex 79f79f2..8f90bc3 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -224,14 +224,15 @@ void verify_non_filename(const char *prefix, const char *arg)\n \t    \"'git <command> [<revision>...] -- [<file>...]'\", arg);\n }\n \n-static void get_common_dir(struct strbuf *sb, const char *gitdir)\n+int get_common_dir(struct strbuf *sb, const char *gitdir)\n {\n \tstruct strbuf data = STRBUF_INIT;\n \tstruct strbuf path = STRBUF_INIT;\n \tconst char *git_common_dir = getenv(GIT_COMMON_DIR_ENVIRONMENT);\n+\tint ret = 0;\n \tif (git_common_dir) {\n \t\tstrbuf_addstr(sb, git_common_dir);\n-\t\treturn;\n+\t\treturn 1;\n \t}\n \tstrbuf_addf(&path, \"%s/commondir\", gitdir);\n \tif (file_exists(path.buf)) {\n@@ -246,10 +247,12 @@ static void get_common_dir(struct strbuf *sb, const char *gitdir)\n \t\t\tstrbuf_addf(&path, \"%s/\", gitdir);\n \t\tstrbuf_addbuf(&path, &data);\n \t\tstrbuf_addstr(sb, real_path(path.buf));\n+\t\tret = 1;\n \t} else\n \t\tstrbuf_addstr(sb, gitdir);\n \tstrbuf_release(&data);\n \tstrbuf_release(&path);\n+\treturn ret;\n }\n \n /*\n@@ -340,13 +343,26 @@ void setup_work_tree(void)\n \tinitialized = 1;\n }\n \n+static int check_repo_format(const char *var, const char *value, void *cb)\n+{\n+\tif (strcmp(var, \"core.repositoryformatversion\") == 0)\n+\t\trepository_format_version = git_config_int(var, value);\n+\telse if (strcmp(var, \"core.sharedrepository\") == 0)\n+\t\tshared_repository = git_config_perm(var, value);\n+\treturn 0;\n+}\n+\n static int check_repository_format_gently(const char *gitdir, int *nongit_ok)\n {\n \tstruct strbuf sb = STRBUF_INIT;\n \tconst char *repo_config;\n+\tconfig_fn_t fn;\n \tint ret = 0;\n \n-\tget_common_dir(&sb, gitdir);\n+\tif (get_common_dir(&sb, gitdir))\n+\t\tfn = check_repo_format;\n+\telse\n+\t\tfn = check_repository_format_version;\n \tstrbuf_addstr(&sb, \"/config\");\n \trepo_config = sb.buf;\n \n@@ -359,7 +375,7 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)\n \t * Use a gentler version of git_config() to check if this repo\n \t * is a good one.\n \t */\n-\tgit_config_early(check_repository_format_version, NULL, repo_config);\n+\tgit_config_early(fn, NULL, repo_config);\n \tif (GIT_REPO_VERSION < repository_format_version) {\n \t\tif (!nongit_ok)\n \t\t\tdie (\"Expected git repo version <= %d, found %d\",\n@@ -831,11 +847,10 @@ int git_config_perm(const char *var, const char *value)\n \n int check_repository_format_version(const char *var, const char *value, void *cb)\n {\n-\tif (strcmp(var, \"core.repositoryformatversion\") == 0)\n-\t\trepository_format_version = git_config_int(var, value);\n-\telse if (strcmp(var, \"core.sharedrepository\") == 0)\n-\t\tshared_repository = git_config_perm(var, value);\n-\telse if (strcmp(var, \"core.bare\") == 0) {\n+\tint ret = check_repo_format(var, value, cb);\n+\tif (ret)\n+\t\treturn ret;\n+\tif (strcmp(var, \"core.bare\") == 0) {\n \t\tis_bare_repository_cfg = git_config_bool(var, value);\n \t\tif (is_bare_repository_cfg == 1)\n \t\t\tinside_work_tree = -1;\ndiff --git a/t/t1501-worktree.sh b/t/t1501-worktree.sh\nindex 8f36aa9..e6ac7a4 100755\n--- a/t/t1501-worktree.sh\n+++ b/t/t1501-worktree.sh\n@@ -346,4 +346,80 @@ test_expect_success 'relative $GIT_WORK_TREE and git subprocesses' '\n \ttest_cmp expected actual\n '\n \n+test_expect_success 'Multi-worktree setup' '\n+\tmkdir work &&\n+\tmkdir -p repo.git/repos/foo &&\n+\tcp repo.git/HEAD repo.git/index repo.git/repos/foo &&\n+\tsane_unset GIT_DIR GIT_CONFIG GIT_WORK_TREE\n+'\n+\n+test_expect_success 'GIT_DIR set (1)' '\n+\techo \"gitdir: repo.git/repos/foo\" >gitfile &&\n+\techo ../.. >repo.git/repos/foo/commondir &&\n+\t(\n+\t\tcd work &&\n+\t\tGIT_DIR=../gitfile git rev-parse --git-common-dir >actual &&\n+\t\ttest-path-utils real_path \"$TRASH_DIRECTORY/repo.git\" >expect &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'GIT_DIR set (2)' '\n+\techo \"gitdir: repo.git/repos/foo\" >gitfile &&\n+\techo \"$TRASH_DIRECTORY/repo.git\" >repo.git/repos/foo/commondir &&\n+\t(\n+\t\tcd work &&\n+\t\tGIT_DIR=../gitfile git rev-parse --git-common-dir >actual &&\n+\t\ttest-path-utils real_path \"$TRASH_DIRECTORY/repo.git\" >expect &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'Auto discovery' '\n+\techo \"gitdir: repo.git/repos/foo\" >.git &&\n+\techo ../.. >repo.git/repos/foo/commondir &&\n+\t(\n+\t\tcd work &&\n+\t\tgit rev-parse --git-common-dir >actual &&\n+\t\ttest-path-utils real_path \"$TRASH_DIRECTORY/repo.git\" >expect &&\n+\t\ttest_cmp expect actual &&\n+\t\techo haha >data1 &&\n+\t\tgit add data1 &&\n+\t\tgit ls-files --full-name :/ | grep data1 >actual &&\n+\t\techo work/data1 >expect &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success '$GIT_DIR/common overrides core.worktree' '\n+\tmkdir elsewhere &&\n+\tgit --git-dir=repo.git config core.worktree \"$TRASH_DIRECTORY/elsewhere\" &&\n+\techo \"gitdir: repo.git/repos/foo\" >.git &&\n+\techo ../.. >repo.git/repos/foo/commondir &&\n+\t(\n+\t\tcd work &&\n+\t\tgit rev-parse --git-common-dir >actual &&\n+\t\ttest-path-utils real_path \"$TRASH_DIRECTORY/repo.git\" >expect &&\n+\t\ttest_cmp expect actual &&\n+\t\techo haha >data2 &&\n+\t\tgit add data2 &&\n+\t\tgit ls-files --full-name :/ | grep data2 >actual &&\n+\t\techo work/data2 >expect &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success '$GIT_WORK_TREE overrides $GIT_DIR/common' '\n+\techo \"gitdir: repo.git/repos/foo\" >.git &&\n+\techo ../.. >repo.git/repos/foo/commondir &&\n+\t(\n+\t\tcd work &&\n+\t\techo haha >data3 &&\n+\t\tgit --git-dir=../.git --work-tree=. add data3 &&\n+\t\tgit ls-files --full-name -- :/ | grep data3 >actual &&\n+\t\techo data3 >expect &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n test_done\ndiff --git a/t/t1510-repo-setup.sh b/t/t1510-repo-setup.sh\nindex e1b2a99..33c1a58 100755\n--- a/t/t1510-repo-setup.sh\n+++ b/t/t1510-repo-setup.sh\n@@ -106,6 +106,7 @@ setup_env () {\n expect () {\n \tcat >\"$1/expected\" <<-EOF\n \tsetup: git_dir: $2\n+\tsetup: git_common_dir: $2\n \tsetup: worktree: $3\n \tsetup: cwd: $4\n \tsetup: prefix: $5\ndiff --git a/trace.c b/trace.c\nindex 08180a9..a594761 100644\n--- a/trace.c\n+++ b/trace.c\n@@ -173,6 +173,7 @@ void trace_repo_setup(const char *prefix)\n \t\tprefix = \"(null)\";\n \n \ttrace_printf_key(key, \"setup: git_dir: %s\\n\", quote_crnl(get_git_dir()));\n+\ttrace_printf_key(key, \"setup: git_common_dir: %s\\n\", quote_crnl(get_git_common_dir()));\n \ttrace_printf_key(key, \"setup: worktree: %s\\n\", quote_crnl(git_work_tree));\n \ttrace_printf_key(key, \"setup: cwd: %s\\n\", quote_crnl(cwd));\n \ttrace_printf_key(key, \"setup: prefix: %s\\n\", quote_crnl(prefix));\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"248627","messageId":"1409387642-24492-21-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1409387642-24492-1-git-send-email-pclouds@gmail.com","subject":"[PATCH 20/32] wrapper.c: wrapper to open a file, fprintf then close","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-08-30T08:33:50Z","receivedAt":"2014-08-30T08:33:50Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n cache.h   |  2 ++\n wrapper.c | 31 +++++++++++++++++++++++++++++++\n 2 files changed, 33 insertions(+)\n\ndiff --git a/cache.h b/cache.h\nindex e3ff7dc..bf4d15e 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -1359,6 +1359,8 @@ static inline ssize_t write_str_in_full(int fd, const char *str)\n {\n \treturn write_in_full(fd, str, strlen(str));\n }\n+__attribute__((format (printf,3,4)))\n+extern int write_file(const char *path, int fatal, const char *fmt, ...);\n \n /* pager.c */\n extern void setup_pager(void);\ndiff --git a/wrapper.c b/wrapper.c\nindex bc1bfb8..9d7b9ac 100644\n--- a/wrapper.c\n+++ b/wrapper.c\n@@ -493,3 +493,34 @@ struct passwd *xgetpwuid_self(void)\n \t\t    errno ? strerror(errno) : _(\"no such user\"));\n \treturn pw;\n }\n+\n+int write_file(const char *path, int fatal, const char *fmt, ...)\n+{\n+\tstruct strbuf sb = STRBUF_INIT;\n+\tva_list params;\n+\tint fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0666);\n+\tif (fd < 0) {\n+\t\tif (fatal)\n+\t\t\tdie_errno(_(\"could not open %s for writing\"), path);\n+\t\treturn -1;\n+\t}\n+\tva_start(params, fmt);\n+\tstrbuf_vaddf(&sb, fmt, params);\n+\tva_end(params);\n+\tif (write_in_full(fd, sb.buf, sb.len) != sb.len) {\n+\t\tint err = errno;\n+\t\tclose(fd);\n+\t\tstrbuf_release(&sb);\n+\t\terrno = err;\n+\t\tif (fatal)\n+\t\t\tdie_errno(_(\"could not write to %s\"), path);\n+\t\treturn -1;\n+\t}\n+\tstrbuf_release(&sb);\n+\tif (close(fd)) {\n+\t\tif (fatal)\n+\t\t\tdie_errno(_(\"could not close %s\"), path);\n+\t\treturn -1;\n+\t}\n+\treturn 0;\n+}\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"248628","messageId":"1409387642-24492-22-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1409387642-24492-1-git-send-email-pclouds@gmail.com","subject":"[PATCH 21/32] use new wrapper write_file() for simple file writing","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-08-30T08:33:51Z","receivedAt":"2014-08-30T08:33:51Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"This fixes common problems in these code about error handling,\nforgetting to close the file handle after fprintf() fails, or not\nprinting out the error string..\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n builtin/branch.c  |  4 +---\n builtin/init-db.c |  7 +------\n daemon.c          | 11 +----------\n submodule.c       |  9 ++-------\n transport.c       |  8 +++-----\n 5 files changed, 8 insertions(+), 31 deletions(-)\n\ndiff --git a/builtin/branch.c b/builtin/branch.c\nindex 0591b22..e4265a1 100644\n--- a/builtin/branch.c\n+++ b/builtin/branch.c\n@@ -754,7 +754,6 @@ static const char edit_description[] = \"BRANCH_DESCRIPTION\";\n \n static int edit_branch_description(const char *branch_name)\n {\n-\tFILE *fp;\n \tint status;\n \tstruct strbuf buf = STRBUF_INIT;\n \tstruct strbuf name = STRBUF_INIT;\n@@ -767,8 +766,7 @@ static int edit_branch_description(const char *branch_name)\n \t\t    \"  %s\\n\"\n \t\t    \"Lines starting with '%c' will be stripped.\\n\",\n \t\t    branch_name, comment_line_char);\n-\tfp = fopen(git_path(edit_description), \"w\");\n-\tif ((fwrite(buf.buf, 1, buf.len, fp) < buf.len) || fclose(fp)) {\n+\tif (write_file(git_path(edit_description), 0, \"%s\", buf.buf)) {\n \t\tstrbuf_release(&buf);\n \t\treturn error(_(\"could not write branch description template: %s\"),\n \t\t\t     strerror(errno));\ndiff --git a/builtin/init-db.c b/builtin/init-db.c\nindex 56f85e2..ce8416a 100644\n--- a/builtin/init-db.c\n+++ b/builtin/init-db.c\n@@ -342,7 +342,6 @@ int set_git_dir_init(const char *git_dir, const char *real_git_dir,\n static void separate_git_dir(const char *git_dir)\n {\n \tstruct stat st;\n-\tFILE *fp;\n \n \tif (!stat(git_link, &st)) {\n \t\tconst char *src;\n@@ -358,11 +357,7 @@ static void separate_git_dir(const char *git_dir)\n \t\t\tdie_errno(_(\"unable to move %s to %s\"), src, git_dir);\n \t}\n \n-\tfp = fopen(git_link, \"w\");\n-\tif (!fp)\n-\t\tdie(_(\"Could not create git link %s\"), git_link);\n-\tfprintf(fp, \"gitdir: %s\\n\", git_dir);\n-\tfclose(fp);\n+\twrite_file(git_link, 1, \"gitdir: %s\\n\", git_dir);\n }\n \n int init_db(const char *template_dir, unsigned int flags)\ndiff --git a/daemon.c b/daemon.c\nindex 1eb6631..dd638a9 100644\n--- a/daemon.c\n+++ b/daemon.c\n@@ -1094,15 +1094,6 @@ static struct credentials *prepare_credentials(const char *user_name,\n }\n #endif\n \n-static void store_pid(const char *path)\n-{\n-\tFILE *f = fopen(path, \"w\");\n-\tif (!f)\n-\t\tdie_errno(\"cannot open pid file '%s'\", path);\n-\tif (fprintf(f, \"%\"PRIuMAX\"\\n\", (uintmax_t) getpid()) < 0 || fclose(f) != 0)\n-\t\tdie_errno(\"failed to write pid file '%s'\", path);\n-}\n-\n static int serve(struct string_list *listen_addr, int listen_port,\n     struct credentials *cred)\n {\n@@ -1313,7 +1304,7 @@ int main(int argc, char **argv)\n \t\tsanitize_stdfds();\n \n \tif (pid_file)\n-\t\tstore_pid(pid_file);\n+\t\twrite_file(pid_file, 1, \"%\"PRIuMAX\"\\n\", (uintmax_t) getpid());\n \n \t/* prepare argv for serving-processes */\n \tcld_argv = xmalloc(sizeof (char *) * (argc + 2));\ndiff --git a/submodule.c b/submodule.c\nindex b80ecac..b7b6059 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -1112,16 +1112,11 @@ void connect_work_tree_and_git_dir(const char *work_tree, const char *git_dir)\n \tstruct strbuf file_name = STRBUF_INIT;\n \tstruct strbuf rel_path = STRBUF_INIT;\n \tconst char *real_work_tree = xstrdup(real_path(work_tree));\n-\tFILE *fp;\n \n \t/* Update gitfile */\n \tstrbuf_addf(&file_name, \"%s/.git\", work_tree);\n-\tfp = fopen(file_name.buf, \"w\");\n-\tif (!fp)\n-\t\tdie(_(\"Could not create git link %s\"), file_name.buf);\n-\tfprintf(fp, \"gitdir: %s\\n\", relative_path(git_dir, real_work_tree,\n-\t\t\t\t\t\t  &rel_path));\n-\tfclose(fp);\n+\twrite_file(file_name.buf, 1, \"gitdir: %s\\n\",\n+\t\t   relative_path(git_dir, real_work_tree, &rel_path));\n \n \t/* Update core.worktree setting */\n \tstrbuf_reset(&file_name);\ndiff --git a/transport.c b/transport.c\nindex 59c9727..1b779bb 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -296,7 +296,6 @@ static int write_one_ref(const char *name, const unsigned char *sha1,\n {\n \tstruct strbuf *buf = data;\n \tint len = buf->len;\n-\tFILE *f;\n \n \t/* when called via for_each_ref(), flags is non-zero */\n \tif (flags && !starts_with(name, \"refs/heads/\") &&\n@@ -305,10 +304,9 @@ static int write_one_ref(const char *name, const unsigned char *sha1,\n \n \tstrbuf_addstr(buf, name);\n \tif (safe_create_leading_directories(buf->buf) ||\n-\t\t\t!(f = fopen(buf->buf, \"w\")) ||\n-\t\t\tfprintf(f, \"%s\\n\", sha1_to_hex(sha1)) < 0 ||\n-\t\t\tfclose(f))\n-\t\treturn error(\"problems writing temporary file %s\", buf->buf);\n+\t    write_file(buf->buf, 0, \"%s\\n\", sha1_to_hex(sha1)))\n+\t\treturn error(\"problems writing temporary file %s: %s\",\n+\t\t\t     buf->buf, strerror(errno));\n \tstrbuf_setlen(buf, len);\n \treturn 0;\n }\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"248629","messageId":"1409387642-24492-23-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1409387642-24492-1-git-send-email-pclouds@gmail.com","subject":"[PATCH 22/32] checkout: support checking out into a new working directory","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-08-30T08:33:52Z","receivedAt":"2014-08-30T08:33:52Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"\"git checkout --to\" sets up a new working directory with a .git file\npointing to $GIT_DIR/repos/<id>. It then executes \"git checkout\" again\non the new worktree with the same arguments except \"--to\" is taken\nout. The second checkout execution, which is not contaminated with any\ninfo from the current repository, will actually check out and\neverything that normal \"git checkout\" does.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n Documentation/git-checkout.txt         | 34 ++++++++++++\n Documentation/git.txt                  |  3 +-\n Documentation/gitrepository-layout.txt |  7 +++\n builtin/checkout.c                     | 95 +++++++++++++++++++++++++++++++++-\n path.c                                 |  2 +-\n t/t2025-checkout-to.sh (new +x)        | 63 ++++++++++++++++++++++\n 6 files changed, 200 insertions(+), 4 deletions(-)\n create mode 100755 t/t2025-checkout-to.sh\n\ndiff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt\nindex 33ad2ad..38c70c5 100644\n--- a/Documentation/git-checkout.txt\n+++ b/Documentation/git-checkout.txt\n@@ -225,6 +225,13 @@ This means that you can use `git checkout -p` to selectively discard\n edits from your current working tree. See the ``Interactive Mode''\n section of linkgit:git-add[1] to learn how to operate the `--patch` mode.\n \n+--to=<path>::\n+\tCheck out a new branch in a separate working directory at\n+\t`<path>`. A new working directory is linked to the current\n+\trepository, sharing everything except working directory\n+\tspecific files such as HEAD, index... See \"MULTIPLE CHECKOUT\n+\tMODE\" section for more information.\n+\n <branch>::\n \tBranch to checkout; if it refers to a branch (i.e., a name that,\n \twhen prepended with \"refs/heads/\", is a valid ref), then that\n@@ -388,6 +395,33 @@ $ git reflog -2 HEAD # or\n $ git log -g -2 HEAD\n ------------\n \n+MULTIPLE CHECKOUT MODE\n+----------------------\n+Normally a working directory is attached to repository. When \"git\n+checkout --to\" is used, a new working directory is attached to the\n+current repository. This new working directory is called \"linked\n+checkout\" as compared to the \"main checkout\" prepared by \"git init\" or\n+\"git clone\". A repository has one main checkout and zero or more\n+linked checkouts.\n+\n+All checkouts share the same repository. Linked checkouts see the\n+repository a bit different from the main checkout. When the checkout\n+\"new\" reads the path $GIT_DIR/HEAD for example, the actual path\n+returned could be $GIT_DIR/repos/new/HEAD. This ensures checkouts\n+won't step on each other.\n+\n+Each linked checkout has a private space in $GIT_DIR/repos, usually\n+named after the base name of the working directory with a number added\n+to make it unique. The linked checkout's $GIT_DIR points to this\n+private space while $GIT_COMMON_DIR points to the main checkout's\n+$GIT_DIR. These settings are done by \"git checkout --to\".\n+\n+Because in this mode $GIT_DIR becomes a lightweight virtual file\n+system where a path could be rewritten to some place else, accessing\n+$GIT_DIR from scripts should use `git rev-parse --git-path` to resolve\n+a path instead of using it directly unless the path is known to be\n+private to the working directory.\n+\n EXAMPLES\n --------\n \ndiff --git a/Documentation/git.txt b/Documentation/git.txt\nindex 749052f..c0a4940 100644\n--- a/Documentation/git.txt\n+++ b/Documentation/git.txt\n@@ -792,7 +792,8 @@ Git so take care if using Cogito etc.\n \tIf this variable is set to a path, non-worktree files that are\n \tnormally in $GIT_DIR will be taken from this path\n \tinstead. Worktree-specific files such as HEAD or index are\n-\ttaken from $GIT_DIR. See linkgit:gitrepository-layout[5] for\n+\ttaken from $GIT_DIR. See linkgit:gitrepository-layout[5] and\n+\tthe section 'MULTIPLE CHECKOUT MODE' in linkgit:checkout[1]\n \tdetails. This variable has lower precedence than other path\n \tvariables such as GIT_INDEX_FILE, GIT_OBJECT_DIRECTORY...\n \ndiff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt\nindex 58d1087..fab398a 100644\n--- a/Documentation/gitrepository-layout.txt\n+++ b/Documentation/gitrepository-layout.txt\n@@ -248,6 +248,13 @@ modules::\n \tdirectory is ignored if $GIT_COMMON_DIR is set and\n \t\"$GIT_COMMON_DIR/modules\" will be used instead.\n \n+repos::\n+\tContains worktree specific information of linked\n+\tcheckouts. Each subdirectory contains the worktree-related\n+\tpart of a linked checkout. This directory is ignored if\n+\t$GIT_COMMON_DIR is set and \"$GIT_COMMON_DIR/repos\" will be\n+\tused instead.\n+\n SEE ALSO\n --------\n linkgit:git-init[1],\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 8023987..6373823 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -48,6 +48,10 @@ struct checkout_opts {\n \tconst char *prefix;\n \tstruct pathspec pathspec;\n \tstruct tree *source_tree;\n+\n+\tconst char *new_worktree;\n+\tconst char **saved_argv;\n+\tint new_worktree_mode;\n };\n \n static int post_checkout_hook(struct commit *old, struct commit *new,\n@@ -250,6 +254,9 @@ static int checkout_paths(const struct checkout_opts *opts,\n \t\tdie(_(\"Cannot update paths and switch to branch '%s' at the same time.\"),\n \t\t    opts->new_branch);\n \n+\tif (opts->new_worktree)\n+\t\tdie(_(\"'%s' cannot be used with updating paths\"), \"--to\");\n+\n \tif (opts->patch_mode)\n \t\treturn run_add_interactive(revision, \"--patch=checkout\",\n \t\t\t\t\t   &opts->pathspec);\n@@ -485,7 +492,7 @@ static int merge_working_tree(const struct checkout_opts *opts,\n \t\t\ttopts.dir->flags |= DIR_SHOW_IGNORED;\n \t\t\tsetup_standard_excludes(topts.dir);\n \t\t}\n-\t\ttree = parse_tree_indirect(old->commit ?\n+\t\ttree = parse_tree_indirect(old->commit && !opts->new_worktree_mode ?\n \t\t\t\t\t   old->commit->object.sha1 :\n \t\t\t\t\t   EMPTY_TREE_SHA1_BIN);\n \t\tinit_tree_desc(&trees[0], tree->buffer, tree->size);\n@@ -796,7 +803,8 @@ static int switch_branches(const struct checkout_opts *opts,\n \t\treturn ret;\n \t}\n \n-\tif (!opts->quiet && !old.path && old.commit && new->commit != old.commit)\n+\tif (!opts->quiet && !old.path && old.commit &&\n+\t    new->commit != old.commit && !opts->new_worktree_mode)\n \t\torphaned_commit_warning(old.commit, new->commit);\n \n \tupdate_refs_for_switch(opts, &old, new);\n@@ -806,6 +814,76 @@ static int switch_branches(const struct checkout_opts *opts,\n \treturn ret || writeout_error;\n }\n \n+static int prepare_linked_checkout(const struct checkout_opts *opts,\n+\t\t\t\t   struct branch_info *new)\n+{\n+\tstruct strbuf sb_git = STRBUF_INIT, sb_repo = STRBUF_INIT;\n+\tstruct strbuf sb = STRBUF_INIT;\n+\tconst char *path = opts->new_worktree, *name;\n+\tstruct stat st;\n+\tstruct child_process cp;\n+\tint counter = 0, len;\n+\n+\tif (!new->commit)\n+\t\tdie(_(\"no branch specified\"));\n+\tif (file_exists(path))\n+\t\tdie(_(\"'%s' already exists\"), path);\n+\n+\tlen = strlen(path);\n+\twhile (len && is_dir_sep(path[len - 1]))\n+\t\tlen--;\n+\n+\tfor (name = path + len - 1; name > path; name--)\n+\t\tif (is_dir_sep(*name)) {\n+\t\t\tname++;\n+\t\t\tbreak;\n+\t\t}\n+\tstrbuf_addstr(&sb_repo,\n+\t\t      git_path(\"repos/%.*s\", (int)(path + len - name), name));\n+\tlen = sb_repo.len;\n+\tif (safe_create_leading_directories_const(sb_repo.buf))\n+\t\tdie_errno(_(\"could not create leading directories of '%s'\"),\n+\t\t\t  sb_repo.buf);\n+\twhile (!stat(sb_repo.buf, &st)) {\n+\t\tcounter++;\n+\t\tstrbuf_setlen(&sb_repo, len);\n+\t\tstrbuf_addf(&sb_repo, \"%d\", counter);\n+\t}\n+\tname = strrchr(sb_repo.buf, '/') + 1;\n+\tif (mkdir(sb_repo.buf, 0777))\n+\t\tdie_errno(_(\"could not create directory of '%s'\"), sb_repo.buf);\n+\n+\tstrbuf_addf(&sb_git, \"%s/.git\", path);\n+\tif (safe_create_leading_directories_const(sb_git.buf))\n+\t\tdie_errno(_(\"could not create leading directories of '%s'\"),\n+\t\t\t  sb_git.buf);\n+\n+\twrite_file(sb_git.buf, 1, \"gitdir: %s/repos/%s\\n\",\n+\t\t   real_path(get_git_common_dir()), name);\n+\t/*\n+\t * This is to keep resolve_ref() happy. We need a valid HEAD\n+\t * or is_git_directory() will reject the directory. Any valid\n+\t * value would do because this value will be ignored and\n+\t * replaced at the next (real) checkout.\n+\t */\n+\tstrbuf_addf(&sb, \"%s/HEAD\", sb_repo.buf);\n+\twrite_file(sb.buf, 1, \"%s\\n\", sha1_to_hex(new->commit->object.sha1));\n+\tstrbuf_reset(&sb);\n+\tstrbuf_addf(&sb, \"%s/commondir\", sb_repo.buf);\n+\twrite_file(sb.buf, 1, \"../..\\n\");\n+\n+\tif (!opts->quiet)\n+\t\tfprintf_ln(stderr, _(\"Enter %s (identifier %s)\"), path, name);\n+\n+\tsetenv(\"GIT_CHECKOUT_NEW_WORKTREE\", \"1\", 1);\n+\tsetenv(GIT_DIR_ENVIRONMENT, sb_git.buf, 1);\n+\tsetenv(GIT_WORK_TREE_ENVIRONMENT, path, 1);\n+\tmemset(&cp, 0, sizeof(cp));\n+\tcp.git_cmd = 1;\n+\tcp.argv = opts->saved_argv;\n+\treturn run_command(&cp);\n+}\n+\n static int git_checkout_config(const char *var, const char *value, void *cb)\n {\n \tif (!strcmp(var, \"diff.ignoresubmodules\")) {\n@@ -1067,6 +1145,9 @@ static int checkout_branch(struct checkout_opts *opts,\n \t\tdie(_(\"Cannot switch branch to a non-commit '%s'\"),\n \t\t    new->name);\n \n+\tif (opts->new_worktree)\n+\t\treturn prepare_linked_checkout(opts, new);\n+\n \tif (!new->commit && opts->new_branch) {\n \t\tunsigned char rev[20];\n \t\tint flag;\n@@ -1109,6 +1190,8 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)\n \t\t\t N_(\"do not limit pathspecs to sparse entries only\")),\n \t\tOPT_HIDDEN_BOOL(0, \"guess\", &dwim_new_local_branch,\n \t\t\t\tN_(\"second guess 'git checkout no-such-branch'\")),\n+\t\tOPT_FILENAME(0, \"to\", &opts.new_worktree,\n+\t\t\t   N_(\"check a branch out in a separate working directory\")),\n \t\tOPT_END(),\n \t};\n \n@@ -1117,6 +1200,9 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)\n \topts.overwrite_ignore = 1;\n \topts.prefix = prefix;\n \n+\topts.saved_argv = xmalloc(sizeof(const char *) * (argc + 2));\n+\tmemcpy(opts.saved_argv, argv, sizeof(const char *) * (argc + 1));\n+\n \tgitmodules_config();\n \tgit_config(git_checkout_config, &opts);\n \n@@ -1125,6 +1211,11 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)\n \targc = parse_options(argc, argv, prefix, options, checkout_usage,\n \t\t\t     PARSE_OPT_KEEP_DASHDASH);\n \n+\t/* recursive execution from checkout_new_worktree() */\n+\topts.new_worktree_mode = getenv(\"GIT_CHECKOUT_NEW_WORKTREE\") != NULL;\n+\tif (opts.new_worktree_mode)\n+\t\topts.new_worktree = NULL;\n+\n \tif (conflict_style) {\n \t\topts.merge = 1; /* implied */\n \t\tgit_xmerge_config(\"merge.conflictstyle\", conflict_style, NULL);\ndiff --git a/path.c b/path.c\nindex 8a6586c..e41d6b3 100644\n--- a/path.c\n+++ b/path.c\n@@ -92,7 +92,7 @@ static void replace_dir(struct strbuf *buf, int len, const char *newdir)\n \n static const char *common_list[] = {\n \t\"/branches\", \"/hooks\", \"/info\", \"/logs\", \"/lost-found\", \"/modules\",\n-\t\"/objects\", \"/refs\", \"/remotes\", \"/rr-cache\", \"/svn\",\n+\t\"/objects\", \"/refs\", \"/remotes\", \"/repos\", \"/rr-cache\", \"/svn\",\n \t\"config\", \"gc.pid\", \"packed-refs\", \"shallow\",\n \tNULL\n };\ndiff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh\nnew file mode 100755\nindex 0000000..8c73b18\n--- /dev/null\n+++ b/t/t2025-checkout-to.sh\n@@ -0,0 +1,63 @@\n+#!/bin/sh\n+\n+test_description='test git checkout --to'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'setup' '\n+\ttest_commit init\n+'\n+\n+test_expect_success 'checkout --to not updating paths' '\n+\ttest_must_fail git checkout --to -- init.t\n+'\n+\n+test_expect_success 'checkout --to an existing worktree' '\n+\tmkdir existing &&\n+\ttest_must_fail git checkout --detach --to existing master\n+'\n+\n+test_expect_success 'checkout --to a new worktree' '\n+\tgit checkout --to here master &&\n+\t(\n+\t\tcd here &&\n+\t\ttest_cmp ../init.t init.t &&\n+\t\tgit symbolic-ref HEAD >actual &&\n+\t\techo refs/heads/master >expect &&\n+\t\ttest_cmp expect actual &&\n+\t\tgit fsck\n+\t)\n+'\n+\n+test_expect_success 'checkout --to a new worktree from a subdir' '\n+\t(\n+\t\tmkdir sub &&\n+\t\tcd sub &&\n+\t\tgit checkout --detach --to here master &&\n+\t\tcd here &&\n+\t\ttest_cmp ../../init.t init.t\n+\t)\n+'\n+\n+test_expect_success 'checkout --to from a linked checkout' '\n+\t(\n+\t\tcd here &&\n+\t\tgit checkout --to nested-here master\n+\t\tcd nested-here &&\n+\t\tgit fsck\n+\t)\n+'\n+\n+test_expect_success 'checkout --to a new worktree creating new branch' '\n+\tgit checkout --to there -b newmaster master &&\n+\t(\n+\t\tcd there &&\n+\t\ttest_cmp ../init.t init.t &&\n+\t\tgit symbolic-ref HEAD >actual &&\n+\t\techo refs/heads/newmaster >expect &&\n+\t\ttest_cmp expect actual &&\n+\t\tgit fsck\n+\t)\n+'\n+\n+test_done\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"248630","messageId":"1409387642-24492-24-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1409387642-24492-1-git-send-email-pclouds@gmail.com","subject":"[PATCH 23/32] checkout: clean up half-prepared directories in --to mode","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-08-30T08:33:53Z","receivedAt":"2014-08-30T08:33:53Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n builtin/checkout.c     | 54 ++++++++++++++++++++++++++++++++++++++++++++++++--\n t/t2025-checkout-to.sh |  6 ++++++\n 2 files changed, 58 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 6373823..4ae925a 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -20,6 +20,7 @@\n #include \"resolve-undo.h\"\n #include \"submodule.h\"\n #include \"argv-array.h\"\n+#include \"sigchain.h\"\n \n static const char * const checkout_usage[] = {\n \tN_(\"git checkout [options] <branch>\"),\n@@ -814,6 +815,35 @@ static int switch_branches(const struct checkout_opts *opts,\n \treturn ret || writeout_error;\n }\n \n+static char *junk_work_tree;\n+static char *junk_git_dir;\n+static int is_junk;\n+static pid_t junk_pid;\n+\n+static void remove_junk(void)\n+{\n+\tstruct strbuf sb = STRBUF_INIT;\n+\tif (!is_junk || getpid() != junk_pid)\n+\t\treturn;\n+\tif (junk_git_dir) {\n+\t\tstrbuf_addstr(&sb, junk_git_dir);\n+\t\tremove_dir_recursively(&sb, 0);\n+\t\tstrbuf_reset(&sb);\n+\t}\n+\tif (junk_work_tree) {\n+\t\tstrbuf_addstr(&sb, junk_work_tree);\n+\t\tremove_dir_recursively(&sb, 0);\n+\t}\n+\tstrbuf_release(&sb);\n+}\n+\n+static void remove_junk_on_signal(int signo)\n+{\n+\tremove_junk();\n+\tsigchain_pop(signo);\n+\traise(signo);\n+}\n+\n static int prepare_linked_checkout(const struct checkout_opts *opts,\n \t\t\t\t   struct branch_info *new)\n {\n@@ -822,7 +852,7 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,\n \tconst char *path = opts->new_worktree, *name;\n \tstruct stat st;\n \tstruct child_process cp;\n-\tint counter = 0, len;\n+\tint counter = 0, len, ret;\n \n \tif (!new->commit)\n \t\tdie(_(\"no branch specified\"));\n@@ -850,13 +880,21 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,\n \t\tstrbuf_addf(&sb_repo, \"%d\", counter);\n \t}\n \tname = strrchr(sb_repo.buf, '/') + 1;\n+\n+\tjunk_pid = getpid();\n+\tatexit(remove_junk);\n+\tsigchain_push_common(remove_junk_on_signal);\n+\n \tif (mkdir(sb_repo.buf, 0777))\n \t\tdie_errno(_(\"could not create directory of '%s'\"), sb_repo.buf);\n+\tjunk_git_dir = xstrdup(sb_repo.buf);\n+\tis_junk = 1;\n \n \tstrbuf_addf(&sb_git, \"%s/.git\", path);\n \tif (safe_create_leading_directories_const(sb_git.buf))\n \t\tdie_errno(_(\"could not create leading directories of '%s'\"),\n \t\t\t  sb_git.buf);\n+\tjunk_work_tree = xstrdup(path);\n \n \twrite_file(sb_git.buf, 1, \"gitdir: %s/repos/%s\\n\",\n \t\t   real_path(get_git_common_dir()), name);\n@@ -881,7 +919,19 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,\n \tmemset(&cp, 0, sizeof(cp));\n \tcp.git_cmd = 1;\n \tcp.argv = opts->saved_argv;\n-\treturn run_command(&cp);\n+\tret = run_command(&cp);\n+\tif (!ret) {\n+\t\tis_junk = 0;\n+\t\tfree(junk_work_tree);\n+\t\tfree(junk_git_dir);\n+\t\tjunk_work_tree = NULL;\n+\t\tjunk_git_dir = NULL;\n+\t}\n+\tstrbuf_release(&sb);\n+\tstrbuf_release(&sb_repo);\n+\tstrbuf_release(&sb_git);\n+\treturn ret;\n+\n }\n \n static int git_checkout_config(const char *var, const char *value, void *cb)\ndiff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh\nindex 8c73b18..75ac420 100755\n--- a/t/t2025-checkout-to.sh\n+++ b/t/t2025-checkout-to.sh\n@@ -17,6 +17,12 @@ test_expect_success 'checkout --to an existing worktree' '\n \ttest_must_fail git checkout --detach --to existing master\n '\n \n+test_expect_success 'checkout --to refuses to checkout locked branch' '\n+\ttest_must_fail git checkout --to zere master &&\n+\t! test -d zere &&\n+\t! test -d .git/repos/zere\n+'\n+\n test_expect_success 'checkout --to a new worktree' '\n \tgit checkout --to here master &&\n \t(\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"248631","messageId":"1409387642-24492-25-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1409387642-24492-1-git-send-email-pclouds@gmail.com","subject":"[PATCH 24/32] checkout: reject if the branch is already checked out elsewhere","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-08-30T08:33:54Z","receivedAt":"2014-08-30T08:33:54Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"One branch obviously can't be checked out at two places (but detached\nheads are ok). Give the user a choice in this case: --detach, -b\nnew-branch, switch branch in the other checkout first or simply 'cd'\nand continue to work there.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n builtin/checkout.c     | 86 ++++++++++++++++++++++++++++++++++++++++++++++++--\n t/t2025-checkout-to.sh | 25 ++++++++++++---\n 2 files changed, 104 insertions(+), 7 deletions(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 4ae925a..614db34 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -432,6 +432,11 @@ struct branch_info {\n \tconst char *name; /* The short name used */\n \tconst char *path; /* The full name of a real branch */\n \tstruct commit *commit; /* The named commit */\n+\t/*\n+\t * if not null the branch is detached because it's already\n+\t * checked out in this checkout\n+\t */\n+\tchar *checkout;\n };\n \n static void setup_branch_path(struct branch_info *branch)\n@@ -989,12 +994,78 @@ static const char *unique_tracking_name(const char *name, unsigned char *sha1)\n \treturn NULL;\n }\n \n+static void check_linked_checkout(struct branch_info *new, const char *id)\n+{\n+\tstruct strbuf sb = STRBUF_INIT;\n+\tstruct strbuf path = STRBUF_INIT;\n+\tstruct strbuf gitdir = STRBUF_INIT;\n+\tconst char *start, *end;\n+\n+\tif (id)\n+\t\tstrbuf_addf(&path, \"%s/repos/%s/HEAD\", get_git_common_dir(), id);\n+\telse\n+\t\tstrbuf_addf(&path, \"%s/HEAD\", get_git_common_dir());\n+\n+\tif (strbuf_read_file(&sb, path.buf, 0) < 0 ||\n+\t    !skip_prefix(sb.buf, \"ref:\", &start))\n+\t\tgoto done;\n+\twhile (isspace(*start))\n+\t\tstart++;\n+\tend = start;\n+\twhile (*end && !isspace(*end))\n+\t\tend++;\n+\tif (strncmp(start, new->path, end - start) || new->path[end - start] != '\\0')\n+\t\tgoto done;\n+\tif (id) {\n+\t\tstrbuf_reset(&path);\n+\t\tstrbuf_addf(&path, \"%s/repos/%s/gitdir\", get_git_common_dir(), id);\n+\t\tif (strbuf_read_file(&gitdir, path.buf, 0) <= 0)\n+\t\t\tgoto done;\n+\t\tstrbuf_rtrim(&gitdir);\n+\t} else\n+\t\tstrbuf_addstr(&gitdir, get_git_common_dir());\n+\tdie(_(\"'%s' is already checked out at '%s'\"), new->name, gitdir.buf);\n+done:\n+\tstrbuf_release(&path);\n+\tstrbuf_release(&sb);\n+\tstrbuf_release(&gitdir);\n+}\n+\n+static void check_linked_checkouts(struct branch_info *new)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\tDIR *dir;\n+\tstruct dirent *d;\n+\n+\tstrbuf_addf(&path, \"%s/repos\", get_git_common_dir());\n+\tif ((dir = opendir(path.buf)) == NULL) {\n+\t\tstrbuf_release(&path);\n+\t\treturn;\n+\t}\n+\n+\t/*\n+\t * $GIT_COMMON_DIR/HEAD is practically outside\n+\t * $GIT_DIR so resolve_ref_unsafe() won't work (it\n+\t * uses git_path). Parse the ref ourselves.\n+\t */\n+\tcheck_linked_checkout(new, NULL);\n+\n+\twhile ((d = readdir(dir)) != NULL) {\n+\t\tif (!strcmp(d->d_name, \".\") || !strcmp(d->d_name, \"..\"))\n+\t\t\tcontinue;\n+\t\tcheck_linked_checkout(new, d->d_name);\n+\t}\n+\tstrbuf_release(&path);\n+\tclosedir(dir);\n+}\n+\n static int parse_branchname_arg(int argc, const char **argv,\n \t\t\t\tint dwim_new_local_branch_ok,\n \t\t\t\tstruct branch_info *new,\n \t\t\t\tstruct tree **source_tree,\n \t\t\t\tunsigned char rev[20],\n-\t\t\t\tconst char **new_branch)\n+\t\t\t\tconst char **new_branch,\n+\t\t\t\tint force_detach)\n {\n \tint argcount = 0;\n \tunsigned char branch_rev[20];\n@@ -1116,6 +1187,16 @@ static int parse_branchname_arg(int argc, const char **argv,\n \telse\n \t\tnew->path = NULL; /* not an existing branch */\n \n+\tif (new->path && !force_detach && !*new_branch) {\n+\t\tunsigned char sha1[20];\n+\t\tint flag;\n+\t\tchar *head_ref = resolve_refdup(\"HEAD\", sha1, 0, &flag);\n+\t\tif (head_ref &&\n+\t\t    (!(flag & REF_ISSYMREF) || strcmp(head_ref, new->path)))\n+\t\t\tcheck_linked_checkouts(new);\n+\t\tfree(head_ref);\n+\t}\n+\n \tnew->commit = lookup_commit_reference_gently(rev, 1);\n \tif (!new->commit) {\n \t\t/* not a commit */\n@@ -1322,7 +1403,8 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)\n \t\t\t!opts.new_branch;\n \t\tint n = parse_branchname_arg(argc, argv, dwim_ok,\n \t\t\t\t\t     &new, &opts.source_tree,\n-\t\t\t\t\t     rev, &opts.new_branch);\n+\t\t\t\t\t     rev, &opts.new_branch,\n+\t\t\t\t\t     opts.force_detach);\n \t\targv += n;\n \t\targc -= n;\n \t}\ndiff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh\nindex 75ac420..796472b 100755\n--- a/t/t2025-checkout-to.sh\n+++ b/t/t2025-checkout-to.sh\n@@ -24,13 +24,14 @@ test_expect_success 'checkout --to refuses to checkout locked branch' '\n '\n \n test_expect_success 'checkout --to a new worktree' '\n-\tgit checkout --to here master &&\n+\tgit rev-parse HEAD >expect &&\n+\tgit checkout --detach --to here master &&\n \t(\n \t\tcd here &&\n \t\ttest_cmp ../init.t init.t &&\n-\t\tgit symbolic-ref HEAD >actual &&\n-\t\techo refs/heads/master >expect &&\n-\t\ttest_cmp expect actual &&\n+\t\ttest_must_fail git symbolic-ref HEAD &&\n+\t\tgit rev-parse HEAD >actual &&\n+\t\ttest_cmp ../expect actual &&\n \t\tgit fsck\n \t)\n '\n@@ -48,7 +49,7 @@ test_expect_success 'checkout --to a new worktree from a subdir' '\n test_expect_success 'checkout --to from a linked checkout' '\n \t(\n \t\tcd here &&\n-\t\tgit checkout --to nested-here master\n+\t\tgit checkout --detach --to nested-here master\n \t\tcd nested-here &&\n \t\tgit fsck\n \t)\n@@ -66,4 +67,18 @@ test_expect_success 'checkout --to a new worktree creating new branch' '\n \t)\n '\n \n+test_expect_success 'die the same branch is already checked out' '\n+\t(\n+\t\tcd here &&\n+\t\ttest_must_fail git checkout newmaster\n+\t)\n+'\n+\n+test_expect_success 'not die on re-checking out current branch' '\n+\t(\n+\t\tcd there &&\n+\t\tgit checkout newmaster\n+\t)\n+'\n+\n test_done\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"248633","messageId":"1409387642-24492-26-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1409387642-24492-1-git-send-email-pclouds@gmail.com","subject":"[PATCH 25/32] prune: strategies for linked checkouts","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-08-30T08:33:55Z","receivedAt":"2014-08-30T08:33:55Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"(alias R=$GIT_COMMON_DIR/repos/<id>)\n\n - linked checkouts are supposed to keep its location in $R/gitdir up\n   to date. The use case is auto fixup after a manual checkout move.\n\n - linked checkouts are supposed to update mtime of $R/gitdir. If\n   $R/gitdir's mtime is older than a limit, and it points to nowhere,\n   repos/<id> is to be pruned.\n\n - If $R/locked exists, repos/<id> is not supposed to be pruned. If\n   $R/locked exists and $R/gitdir's mtime is older than a really long\n   limit, warn about old unused repo.\n\n - \"git checkout --to\" is supposed to make a hard link named $R/link\n   pointing to the .git file on supported file systems to help detect\n   the user manually deleting the checkout. If $R/link exists and its\n   link count is greated than 1, the repo is kept.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n Documentation/git-prune.txt                |  3 +\n Documentation/gitrepository-layout.txt     | 19 ++++++\n builtin/checkout.c                         | 14 +++++\n builtin/prune.c                            | 95 ++++++++++++++++++++++++++++++\n setup.c                                    | 13 ++++\n t/t2026-prune-linked-checkouts.sh (new +x) | 84 ++++++++++++++++++++++++++\n 6 files changed, 228 insertions(+)\n create mode 100755 t/t2026-prune-linked-checkouts.sh\n\ndiff --git a/Documentation/git-prune.txt b/Documentation/git-prune.txt\nindex 7a493c8..50e39ec 100644\n--- a/Documentation/git-prune.txt\n+++ b/Documentation/git-prune.txt\n@@ -48,6 +48,9 @@ OPTIONS\n --expire <time>::\n \tOnly expire loose objects older than <time>.\n \n+--repos::\n+\tPrune directories in $GIT_DIR/repos.\n+\n <head>...::\n \tIn addition to objects\n \treachable from any of our references, keep objects\ndiff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt\nindex fab398a..c9a7d1a 100644\n--- a/Documentation/gitrepository-layout.txt\n+++ b/Documentation/gitrepository-layout.txt\n@@ -255,6 +255,25 @@ repos::\n \t$GIT_COMMON_DIR is set and \"$GIT_COMMON_DIR/repos\" will be\n \tused instead.\n \n+repos/<id>/gitdir::\n+\tA text file containing the absolute path back to the .git file\n+\tthat points to here. This is used to check if the linked\n+\trepository has been manually removed and there is no need to\n+\tkeep this directory any more. mtime of this file should be\n+\tupdated every time the linked repository is accessed.\n+\n+repos/<id>/locked::\n+\tIf this file exists, the linked repository may be on a\n+\tportable device and not available. It does not mean that the\n+\tlinked repository is gone and `repos/<id>` could be\n+\tremoved. The file's content contains a reason string on why\n+\tthe repository is locked.\n+\n+repos/<id>/link::\n+\tIf this file exists, it is a hard link to the linked .git\n+\tfile. It is used to detect if the linked repository is\n+\tmanually removed.\n+\n SEE ALSO\n --------\n linkgit:git-init[1],\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 614db34..0f28b4a 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -895,12 +895,22 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,\n \tjunk_git_dir = xstrdup(sb_repo.buf);\n \tis_junk = 1;\n \n+\t/*\n+\t * lock the incomplete repo so prune won't delete it, unlock\n+\t * after the preparation is over.\n+\t */\n+\tstrbuf_addf(&sb, \"%s/locked\", sb_repo.buf);\n+\twrite_file(sb.buf, 1, \"initializing\\n\");\n+\n \tstrbuf_addf(&sb_git, \"%s/.git\", path);\n \tif (safe_create_leading_directories_const(sb_git.buf))\n \t\tdie_errno(_(\"could not create leading directories of '%s'\"),\n \t\t\t  sb_git.buf);\n \tjunk_work_tree = xstrdup(path);\n \n+\tstrbuf_reset(&sb);\n+\tstrbuf_addf(&sb, \"%s/gitdir\", sb_repo.buf);\n+\twrite_file(sb.buf, 1, \"%s\\n\", real_path(sb_git.buf));\n \twrite_file(sb_git.buf, 1, \"gitdir: %s/repos/%s\\n\",\n \t\t   real_path(get_git_common_dir()), name);\n \t/*\n@@ -909,6 +919,7 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,\n \t * value would do because this value will be ignored and\n \t * replaced at the next (real) checkout.\n \t */\n+\tstrbuf_reset(&sb);\n \tstrbuf_addf(&sb, \"%s/HEAD\", sb_repo.buf);\n \twrite_file(sb.buf, 1, \"%s\\n\", sha1_to_hex(new->commit->object.sha1));\n \tstrbuf_reset(&sb);\n@@ -932,6 +943,9 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,\n \t\tjunk_work_tree = NULL;\n \t\tjunk_git_dir = NULL;\n \t}\n+\tstrbuf_reset(&sb);\n+\tstrbuf_addf(&sb, \"%s/locked\", sb_repo.buf);\n+\tunlink_or_warn(sb.buf);\n \tstrbuf_release(&sb);\n \tstrbuf_release(&sb_repo);\n \tstrbuf_release(&sb_git);\ndiff --git a/builtin/prune.c b/builtin/prune.c\nindex 144a3bd..e72c391 100644\n--- a/builtin/prune.c\n+++ b/builtin/prune.c\n@@ -112,6 +112,91 @@ static void prune_object_dir(const char *path)\n \t}\n }\n \n+static int prune_repo_dir(const char *id, struct strbuf *reason)\n+{\n+\tstruct stat st;\n+\tchar *path;\n+\tint fd, len;\n+\n+\tif (!is_directory(git_path(\"repos/%s\", id))) {\n+\t\tstrbuf_addf(reason, _(\"Removing repos/%s: not a valid directory\"), id);\n+\t\treturn 1;\n+\t}\n+\tif (file_exists(git_path(\"repos/%s/locked\", id)))\n+\t\treturn 0;\n+\tif (stat(git_path(\"repos/%s/gitdir\", id), &st)) {\n+\t\tstrbuf_addf(reason, _(\"Removing repos/%s: gitdir file does not exist\"), id);\n+\t\treturn 1;\n+\t}\n+\tfd = open(git_path(\"repos/%s/gitdir\", id), O_RDONLY);\n+\tif (fd < 0) {\n+\t\tstrbuf_addf(reason, _(\"Removing repos/%s: unable to read gitdir file (%s)\"),\n+\t\t\t    id, strerror(errno));\n+\t\treturn 1;\n+\t}\n+\tlen = st.st_size;\n+\tpath = xmalloc(len + 1);\n+\tread_in_full(fd, path, len);\n+\tclose(fd);\n+\twhile (len && (path[len - 1] == '\\n' || path[len - 1] == '\\r'))\n+\t\tlen--;\n+\tif (!len) {\n+\t\tstrbuf_addf(reason, _(\"Removing repos/%s: invalid gitdir file\"), id);\n+\t\tfree(path);\n+\t\treturn 1;\n+\t}\n+\tpath[len] = '\\0';\n+\tif (!file_exists(path)) {\n+\t\tstruct stat st_link;\n+\t\tfree(path);\n+\t\t/*\n+\t\t * the repo is moved manually and has not been\n+\t\t * accessed since?\n+\t\t */\n+\t\tif (!stat(git_path(\"repos/%s/link\", id), &st_link) &&\n+\t\t    st_link.st_nlink > 1)\n+\t\t\treturn 0;\n+\t\tstrbuf_addf(reason, _(\"Removing repos/%s: gitdir file points to non-existent location\"), id);\n+\t\treturn 1;\n+\t}\n+\tfree(path);\n+\treturn st.st_mtime <= expire;\n+}\n+\n+static void prune_repos_dir(void)\n+{\n+\tstruct strbuf reason = STRBUF_INIT;\n+\tstruct strbuf path = STRBUF_INIT;\n+\tDIR *dir = opendir(git_path(\"repos\"));\n+\tstruct dirent *d;\n+\tint ret;\n+\tif (!dir)\n+\t\treturn;\n+\twhile ((d = readdir(dir)) != NULL) {\n+\t\tif (!strcmp(d->d_name, \".\") || !strcmp(d->d_name, \"..\"))\n+\t\t\tcontinue;\n+\t\tstrbuf_reset(&reason);\n+\t\tif (!prune_repo_dir(d->d_name, &reason))\n+\t\t\tcontinue;\n+\t\tif (show_only || verbose)\n+\t\t\tprintf(\"%s\\n\", reason.buf);\n+\t\tif (show_only)\n+\t\t\tcontinue;\n+\t\tstrbuf_reset(&path);\n+\t\tstrbuf_addstr(&path, git_path(\"repos/%s\", d->d_name));\n+\t\tret = remove_dir_recursively(&path, 0);\n+\t\tif (ret < 0 && errno == ENOTDIR)\n+\t\t\tret = unlink(path.buf);\n+\t\tif (ret)\n+\t\t\terror(_(\"failed to remove: %s\"), strerror(errno));\n+\t}\n+\tclosedir(dir);\n+\tif (!show_only)\n+\t\trmdir(git_path(\"repos\"));\n+\tstrbuf_release(&reason);\n+\tstrbuf_release(&path);\n+}\n+\n /*\n  * Write errors (particularly out of space) can result in\n  * failed temporary packs (and more rarely indexes and other\n@@ -138,10 +223,12 @@ int cmd_prune(int argc, const char **argv, const char *prefix)\n {\n \tstruct rev_info revs;\n \tstruct progress *progress = NULL;\n+\tint prune_repos = 0;\n \tconst struct option options[] = {\n \t\tOPT__DRY_RUN(&show_only, N_(\"do not remove, show only\")),\n \t\tOPT__VERBOSE(&verbose, N_(\"report pruned objects\")),\n \t\tOPT_BOOL(0, \"progress\", &show_progress, N_(\"show progress\")),\n+\t\tOPT_BOOL(0, \"repos\", &prune_repos, N_(\"prune .git/repos/\")),\n \t\tOPT_EXPIRY_DATE(0, \"expire\", &expire,\n \t\t\t\tN_(\"expire objects older than <time>\")),\n \t\tOPT_END()\n@@ -154,6 +241,14 @@ int cmd_prune(int argc, const char **argv, const char *prefix)\n \tinit_revisions(&revs, prefix);\n \n \targc = parse_options(argc, argv, prefix, options, prune_usage, 0);\n+\n+\tif (prune_repos) {\n+\t\tif (argc)\n+\t\t\tdie(_(\"--repos does not take extra arguments\"));\n+\t\tprune_repos_dir();\n+\t\treturn 0;\n+\t}\n+\n \twhile (argc--) {\n \t\tunsigned char sha1[20];\n \t\tconst char *name = *argv++;\ndiff --git a/setup.c b/setup.c\nindex 8f90bc3..da2d669 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -390,6 +390,17 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)\n \treturn ret;\n }\n \n+static void update_linked_gitdir(const char *gitfile, const char *gitdir)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\tstruct stat st;\n+\n+\tstrbuf_addf(&path, \"%s/gitfile\", gitdir);\n+\tif (stat(path.buf, &st) || st.st_mtime + 24 * 3600 < time(NULL))\n+\t\twrite_file(path.buf, 0, \"%s\\n\", gitfile);\n+\tstrbuf_release(&path);\n+}\n+\n /*\n  * Try to read the location of the git directory from the .git file,\n  * return path to git directory if found.\n@@ -438,6 +449,8 @@ const char *read_gitfile(const char *path)\n \n \tif (!is_git_directory(dir))\n \t\tdie(\"Not a git repository: %s\", dir);\n+\n+\tupdate_linked_gitdir(path, dir);\n \tpath = real_path(dir);\n \n \tfree(buf);\ndiff --git a/t/t2026-prune-linked-checkouts.sh b/t/t2026-prune-linked-checkouts.sh\nnew file mode 100755\nindex 0000000..79d84cb\n--- /dev/null\n+++ b/t/t2026-prune-linked-checkouts.sh\n@@ -0,0 +1,84 @@\n+#!/bin/sh\n+\n+test_description='prune $GIT_DIR/repos'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'prune --repos on normal repo' '\n+\tgit prune --repos &&\n+\ttest_must_fail git prune --repos abc\n+'\n+\n+test_expect_success 'prune files inside $GIT_DIR/repos' '\n+\tmkdir .git/repos &&\n+\t: >.git/repos/abc &&\n+\tgit prune --repos --verbose >actual &&\n+\tcat >expect <<EOF &&\n+Removing repos/abc: not a valid directory\n+EOF\n+\ttest_i18ncmp expect actual &&\n+\t! test -f .git/repos/abc &&\n+\t! test -d .git/repos\n+'\n+\n+test_expect_success 'prune directories without gitdir' '\n+\tmkdir -p .git/repos/def/abc &&\n+\t: >.git/repos/def/def &&\n+\tcat >expect <<EOF &&\n+Removing repos/def: gitdir file does not exist\n+EOF\n+\tgit prune --repos --verbose >actual &&\n+\ttest_i18ncmp expect actual &&\n+\t! test -d .git/repos/def &&\n+\t! test -d .git/repos\n+'\n+\n+test_expect_success POSIXPERM 'prune directories with unreadable gitdir' '\n+\tmkdir -p .git/repos/def/abc &&\n+\t: >.git/repos/def/def &&\n+\t: >.git/repos/def/gitdir &&\n+\tchmod u-r .git/repos/def/gitdir &&\n+\tgit prune --repos --verbose >actual &&\n+\ttest_i18ngrep \"Removing repos/def: unable to read gitdir file\" actual &&\n+\t! test -d .git/repos/def &&\n+\t! test -d .git/repos\n+'\n+\n+test_expect_success 'prune directories with invalid gitdir' '\n+\tmkdir -p .git/repos/def/abc &&\n+\t: >.git/repos/def/def &&\n+\t: >.git/repos/def/gitdir &&\n+\tgit prune --repos --verbose >actual &&\n+\ttest_i18ngrep \"Removing repos/def: invalid gitdir file\" actual &&\n+\t! test -d .git/repos/def &&\n+\t! test -d .git/repos\n+'\n+\n+test_expect_success 'prune directories with gitdir pointing to nowhere' '\n+\tmkdir -p .git/repos/def/abc &&\n+\t: >.git/repos/def/def &&\n+\techo \"$TRASH_DIRECTORY\"/nowhere >.git/repos/def/gitdir &&\n+\tgit prune --repos --verbose >actual &&\n+\ttest_i18ngrep \"Removing repos/def: gitdir file points to non-existent location\" actual &&\n+\t! test -d .git/repos/def &&\n+\t! test -d .git/repos\n+'\n+\n+test_expect_success 'not prune locked checkout' '\n+\ttest_when_finished rm -r .git/repos\n+\tmkdir -p .git/repos/ghi &&\n+\t: >.git/repos/ghi/locked &&\n+\tgit prune --repos &&\n+\ttest -d .git/repos/ghi\n+'\n+\n+test_expect_success 'not prune recent checkouts' '\n+\ttest_when_finished rm -r .git/repos\n+\tmkdir zz &&\n+\tmkdir -p .git/repos/jlm &&\n+\techo \"$TRASH_DIRECTORY\"/zz >.git/repos/jlm/gitdir &&\n+\tgit prune --repos --verbose --expire=2.days.ago &&\n+\ttest -d .git/repos/jlm\n+'\n+\n+test_done\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"248632","messageId":"1409387642-24492-27-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1409387642-24492-1-git-send-email-pclouds@gmail.com","subject":"[PATCH 26/32] gc: style change -- no SP before closing parenthesis","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-08-30T08:33:56Z","receivedAt":"2014-08-30T08:33:56Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n builtin/gc.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 8d219d8..3bfb990 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -298,7 +298,7 @@ int cmd_gc(int argc, const char **argv, const char *prefix)\n \targv_array_pushl(&pack_refs_cmd, \"pack-refs\", \"--all\", \"--prune\", NULL);\n \targv_array_pushl(&reflog, \"reflog\", \"expire\", \"--all\", NULL);\n \targv_array_pushl(&repack, \"repack\", \"-d\", \"-l\", NULL);\n-\targv_array_pushl(&prune, \"prune\", \"--expire\", NULL );\n+\targv_array_pushl(&prune, \"prune\", \"--expire\", NULL);\n \targv_array_pushl(&rerere, \"rerere\", \"gc\", NULL);\n \n \tgit_config(gc_config, NULL);\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"248635","messageId":"1409387642-24492-28-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1409387642-24492-1-git-send-email-pclouds@gmail.com","subject":"[PATCH 27/32] gc: factor out gc.pruneexpire parsing code","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-08-30T08:33:57Z","receivedAt":"2014-08-30T08:33:57Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n builtin/gc.c | 21 +++++++++++++--------\n 1 file changed, 13 insertions(+), 8 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 3bfb990..e38c902 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -55,6 +55,17 @@ static void remove_pidfile_on_signal(int signo)\n \traise(signo);\n }\n \n+static int git_config_date_string(const char **output,\n+\t\t\t\t  const char *var, const char *value)\n+{\n+\tif (value && strcmp(value, \"now\")) {\n+\t\tunsigned long now = approxidate(\"now\");\n+\t\tif (approxidate(value) >= now)\n+\t\t\treturn error(_(\"Invalid %s: '%s'\"), var, value);\n+\t}\n+\treturn git_config_string(output, var, value);\n+}\n+\n static int gc_config(const char *var, const char *value, void *cb)\n {\n \tif (!strcmp(var, \"gc.packrefs\")) {\n@@ -84,14 +95,8 @@ static int gc_config(const char *var, const char *value, void *cb)\n \t\tdetach_auto = git_config_bool(var, value);\n \t\treturn 0;\n \t}\n-\tif (!strcmp(var, \"gc.pruneexpire\")) {\n-\t\tif (value && strcmp(value, \"now\")) {\n-\t\t\tunsigned long now = approxidate(\"now\");\n-\t\t\tif (approxidate(value) >= now)\n-\t\t\t\treturn error(_(\"Invalid %s: '%s'\"), var, value);\n-\t\t}\n-\t\treturn git_config_string(&prune_expire, var, value);\n-\t}\n+\tif (!strcmp(var, \"gc.pruneexpire\"))\n+\t\treturn git_config_date_string(&prune_expire, var, value);\n \treturn git_default_config(var, value, cb);\n }\n \n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"248634","messageId":"1409387642-24492-29-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1409387642-24492-1-git-send-email-pclouds@gmail.com","subject":"[PATCH 28/32] gc: support prune --repos","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-08-30T08:33:58Z","receivedAt":"2014-08-30T08:33:58Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n Documentation/config.txt |  7 +++++++\n builtin/gc.c             | 11 +++++++++++\n 2 files changed, 18 insertions(+)\n\ndiff --git a/Documentation/config.txt b/Documentation/config.txt\nindex 286e539..57999fa 100644\n--- a/Documentation/config.txt\n+++ b/Documentation/config.txt\n@@ -1211,6 +1211,13 @@ gc.pruneexpire::\n \t\"now\" may be used to disable this  grace period and always prune\n \tunreachable objects immediately.\n \n+gc.prunereposexpire::\n+\tWhen 'git gc' is run, it will call\n+\t'prune --repos --expire 3.months.ago'.\n+\tOverride the grace period with this config variable. The value\n+\t\"now\" may be used to disable the grace period and prune\n+\t$GIT_DIR/repos immediately.\n+\n gc.reflogexpire::\n gc.<pattern>.reflogexpire::\n \t'git reflog expire' removes reflog entries older than\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex e38c902..0c65808 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -33,11 +33,13 @@ static int gc_auto_threshold = 6700;\n static int gc_auto_pack_limit = 50;\n static int detach_auto = 1;\n static const char *prune_expire = \"2.weeks.ago\";\n+static const char *prune_repos_expire = \"3.months.ago\";\n \n static struct argv_array pack_refs_cmd = ARGV_ARRAY_INIT;\n static struct argv_array reflog = ARGV_ARRAY_INIT;\n static struct argv_array repack = ARGV_ARRAY_INIT;\n static struct argv_array prune = ARGV_ARRAY_INIT;\n+static struct argv_array prune_repos = ARGV_ARRAY_INIT;\n static struct argv_array rerere = ARGV_ARRAY_INIT;\n \n static char *pidfile;\n@@ -97,6 +99,8 @@ static int gc_config(const char *var, const char *value, void *cb)\n \t}\n \tif (!strcmp(var, \"gc.pruneexpire\"))\n \t\treturn git_config_date_string(&prune_expire, var, value);\n+\tif (!strcmp(var, \"gc.prunereposexpire\"))\n+\t\treturn git_config_date_string(&prune_repos_expire, var, value);\n \treturn git_default_config(var, value, cb);\n }\n \n@@ -304,6 +308,7 @@ int cmd_gc(int argc, const char **argv, const char *prefix)\n \targv_array_pushl(&reflog, \"reflog\", \"expire\", \"--all\", NULL);\n \targv_array_pushl(&repack, \"repack\", \"-d\", \"-l\", NULL);\n \targv_array_pushl(&prune, \"prune\", \"--expire\", NULL);\n+\targv_array_pushl(&prune_repos, \"prune\", \"--repos\", \"--expire\", NULL);\n \targv_array_pushl(&rerere, \"rerere\", \"gc\", NULL);\n \n \tgit_config(gc_config, NULL);\n@@ -373,6 +378,12 @@ int cmd_gc(int argc, const char **argv, const char *prefix)\n \t\t\treturn error(FAILED_RUN, prune.argv[0]);\n \t}\n \n+\tif (prune_repos_expire) {\n+\t\targv_array_push(&prune_repos, prune_repos_expire);\n+\t\tif (run_command_v_opt(prune_repos.argv, RUN_GIT_CMD))\n+\t\t\treturn error(FAILED_RUN, prune_repos.argv[0]);\n+\t}\n+\n \tif (run_command_v_opt(rerere.argv, RUN_GIT_CMD))\n \t\treturn error(FAILED_RUN, rerere.argv[0]);\n \n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"248636","messageId":"1409387642-24492-30-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1409387642-24492-1-git-send-email-pclouds@gmail.com","subject":"[PATCH 29/32] count-objects: report unused files in $GIT_DIR/repos/...","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-08-30T08:33:59Z","receivedAt":"2014-08-30T08:33:59Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"In linked checkouts, borrowed parts like config is taken from\n$GIT_COMMON_DIR. $GIT_DIR/config is never used. Report them as\ngarbage.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n builtin/count-objects.c |  4 +++-\n cache.h                 |  1 +\n path.c                  | 29 +++++++++++++++++++++++++++--\n 3 files changed, 31 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/count-objects.c b/builtin/count-objects.c\nindex a7f70cb..d3a1620 100644\n--- a/builtin/count-objects.c\n+++ b/builtin/count-objects.c\n@@ -102,8 +102,10 @@ int cmd_count_objects(int argc, const char **argv, const char *prefix)\n \t/* we do not take arguments other than flags for now */\n \tif (argc)\n \t\tusage_with_options(count_objects_usage, opts);\n-\tif (verbose)\n+\tif (verbose) {\n \t\treport_garbage = real_report_garbage;\n+\t\treport_linked_checkout_garbage();\n+\t}\n \tmemcpy(path, objdir, len);\n \tif (len && objdir[len-1] != '/')\n \t\tpath[len++] = '/';\ndiff --git a/cache.h b/cache.h\nindex bf4d15e..12f0fc0 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -690,6 +690,7 @@ extern const char *mkpath(const char *fmt, ...) __attribute__((format (printf, 1\n extern const char *git_path(const char *fmt, ...) __attribute__((format (printf, 1, 2)));\n extern const char *git_path_submodule(const char *path, const char *fmt, ...)\n \t__attribute__((format (printf, 2, 3)));\n+extern void report_linked_checkout_garbage(void);\n \n /*\n  * Return the name of the file in the local object database that would\ndiff --git a/path.c b/path.c\nindex e41d6b3..b5af137 100644\n--- a/path.c\n+++ b/path.c\n@@ -4,6 +4,7 @@\n #include \"cache.h\"\n #include \"strbuf.h\"\n #include \"string-list.h\"\n+#include \"dir.h\"\n \n static int get_st_mode_bits(const char *path, int *mode)\n {\n@@ -91,9 +92,9 @@ static void replace_dir(struct strbuf *buf, int len, const char *newdir)\n }\n \n static const char *common_list[] = {\n-\t\"/branches\", \"/hooks\", \"/info\", \"/logs\", \"/lost-found\", \"/modules\",\n+\t\"/branches\", \"/hooks\", \"/info\", \"!/logs\", \"/lost-found\", \"/modules\",\n \t\"/objects\", \"/refs\", \"/remotes\", \"/repos\", \"/rr-cache\", \"/svn\",\n-\t\"config\", \"gc.pid\", \"packed-refs\", \"shallow\",\n+\t\"config\", \"!gc.pid\", \"packed-refs\", \"shallow\",\n \tNULL\n };\n \n@@ -107,6 +108,8 @@ static void update_common_dir(struct strbuf *buf, int git_dir_len)\n \tfor (p = common_list; *p; p++) {\n \t\tconst char *path = *p;\n \t\tint is_dir = 0;\n+\t\tif (*path == '!')\n+\t\t\tpath++;\n \t\tif (*path == '/') {\n \t\t\tpath++;\n \t\t\tis_dir = 1;\n@@ -122,6 +125,28 @@ static void update_common_dir(struct strbuf *buf, int git_dir_len)\n \t}\n }\n \n+void report_linked_checkout_garbage(void)\n+{\n+\tstruct strbuf sb = STRBUF_INIT;\n+\tconst char **p;\n+\tint len;\n+\n+\tif (!git_common_dir_env)\n+\t\treturn;\n+\tstrbuf_addf(&sb, \"%s/\", get_git_dir());\n+\tlen = sb.len;\n+\tfor (p = common_list; *p; p++) {\n+\t\tconst char *path = *p;\n+\t\tif (*path == '!')\n+\t\t\tcontinue;\n+\t\tstrbuf_setlen(&sb, len);\n+\t\tstrbuf_addstr(&sb, path);\n+\t\tif (file_exists(sb.buf))\n+\t\t\treport_garbage(\"unused in linked checkout\", sb.buf);\n+\t}\n+\tstrbuf_release(&sb);\n+}\n+\n static void adjust_git_path(struct strbuf *buf, int git_dir_len)\n {\n \tconst char *base = buf->buf + git_dir_len;\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"248637","messageId":"1409387642-24492-31-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1409387642-24492-1-git-send-email-pclouds@gmail.com","subject":"[PATCH 30/32] git_path(): keep \"info/sparse-checkout\" per work-tree","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-08-30T08:34:00Z","receivedAt":"2014-08-30T08:34:00Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Currently git_path(\"info/sparse-checkout\") resolves to\n$GIT_COMMON_DIR/info/sparse-checkout in multiple worktree mode. It\nmakes more sense for the sparse checkout patterns to be per worktree,\nso you can have multiple checkouts with different parts of the tree.\n\nWith this, \"git checkout --to <new>\" on a sparse checkout will create\n<new> as a full checkout. Which is expected, it's how a new checkout\nis made. The user can reshape the worktree afterwards.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n path.c                | 3 ++-\n t/t0060-path-utils.sh | 1 +\n 2 files changed, 3 insertions(+), 1 deletion(-)\n\ndiff --git a/path.c b/path.c\nindex b5af137..b1dd2bc 100644\n--- a/path.c\n+++ b/path.c\n@@ -103,7 +103,8 @@ static void update_common_dir(struct strbuf *buf, int git_dir_len)\n \tchar *base = buf->buf + git_dir_len;\n \tconst char **p;\n \n-\tif (is_dir_file(base, \"logs\", \"HEAD\"))\n+\tif (is_dir_file(base, \"logs\", \"HEAD\") ||\n+\t    is_dir_file(base, \"info\", \"sparse-checkout\"))\n \t\treturn;\t/* keep this in $GIT_DIR */\n \tfor (p = common_list; *p; p++) {\n \t\tconst char *path = *p;\ndiff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh\nindex f5d6f80..93605f4 100755\n--- a/t/t0060-path-utils.sh\n+++ b/t/t0060-path-utils.sh\n@@ -270,6 +270,7 @@ test_git_path GIT_COMMON_DIR=bar objects                  bar/objects\n test_git_path GIT_COMMON_DIR=bar objects/bar              bar/objects/bar\n test_git_path GIT_COMMON_DIR=bar info/exclude             bar/info/exclude\n test_git_path GIT_COMMON_DIR=bar info/grafts              bar/info/grafts\n+test_git_path GIT_COMMON_DIR=bar info/sparse-checkout     .git/info/sparse-checkout\n test_git_path GIT_COMMON_DIR=bar remotes/bar              bar/remotes/bar\n test_git_path GIT_COMMON_DIR=bar branches/bar             bar/branches/bar\n test_git_path GIT_COMMON_DIR=bar logs/refs/heads/master   bar/logs/refs/heads/master\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"248638","messageId":"1409387642-24492-32-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1409387642-24492-1-git-send-email-pclouds@gmail.com","subject":"[PATCH 31/32] checkout: don't require a work tree when checking out into a new one","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-08-30T08:34:01Z","receivedAt":"2014-08-30T08:34:01Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"From: Dennis Kaarsemaker <dennis@kaarsemaker.net>\n\nFor normal use cases, it does not make sense for 'checkout' to work on\na bare repository, without a worktree. But \"checkout --to\" is an\nexception because it _creates_ a new worktree. Allow this option to\nrun on bare repositories.\n\nPeople who check out from a bare repository should remember that\ncore.logallrefupdates is off by default and it should be turned back\non. `--to` cannot do this automatically behind the user's back because\nsome user may deliberately want no reflog.\n\nFor people interested in repository setup/discovery code,\nis_bare_repository_cfg (aka \"core.bare\") is unchanged by this patch,\nwhich means 'true' by default for bare repos. Fortunately when we get\nthe repo through a linked checkout, is_bare_repository_cfg is never\nused. So all is still good.\n\n[nd: commit message]\n\nSigned-off-by: Dennis Kaarsemaker <dennis@kaarsemaker.net>\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n builtin/checkout.c     |  3 +++\n git.c                  |  2 +-\n t/t2025-checkout-to.sh | 15 +++++++++++++++\n 3 files changed, 19 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 0f28b4a..b88c646 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -1361,6 +1361,9 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)\n \tif (opts.new_worktree_mode)\n \t\topts.new_worktree = NULL;\n \n+\tif (!opts.new_worktree)\n+\t\tsetup_work_tree();\n+\n \tif (conflict_style) {\n \t\topts.merge = 1; /* implied */\n \t\tgit_xmerge_config(\"merge.conflictstyle\", conflict_style, NULL);\ndiff --git a/git.c b/git.c\nindex 5b6c761..7426651 100644\n--- a/git.c\n+++ b/git.c\n@@ -383,7 +383,7 @@ static struct cmd_struct commands[] = {\n \t{ \"check-ignore\", cmd_check_ignore, RUN_SETUP | NEED_WORK_TREE },\n \t{ \"check-mailmap\", cmd_check_mailmap, RUN_SETUP },\n \t{ \"check-ref-format\", cmd_check_ref_format },\n-\t{ \"checkout\", cmd_checkout, RUN_SETUP | NEED_WORK_TREE },\n+\t{ \"checkout\", cmd_checkout, RUN_SETUP },\n \t{ \"checkout-index\", cmd_checkout_index,\n \t\tRUN_SETUP | NEED_WORK_TREE},\n \t{ \"cherry\", cmd_cherry, RUN_SETUP },\ndiff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh\nindex 796472b..9f701a1 100755\n--- a/t/t2025-checkout-to.sh\n+++ b/t/t2025-checkout-to.sh\n@@ -81,4 +81,19 @@ test_expect_success 'not die on re-checking out current branch' '\n \t)\n '\n \n+test_expect_success 'checkout --to from a bare repo' '\n+\t(\n+\t\tgit clone --bare . bare &&\n+\t\tcd bare &&\n+\t\tgit checkout --to ../there2 -b bare-master master\n+\t)\n+'\n+\n+test_expect_success 'checkout from a bare repo without --to' '\n+\t(\n+\t\tcd bare &&\n+\t\ttest_must_fail git checkout master\n+\t)\n+'\n+\n test_done\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"248639","messageId":"1409387642-24492-33-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1409387642-24492-1-git-send-email-pclouds@gmail.com","subject":"[PATCH 32/32] t2025: add a test to make sure grafts is working from a linked checkout","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-08-30T08:34:02Z","receivedAt":"2014-08-30T08:34:02Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n t/t2025-checkout-to.sh | 18 ++++++++++++++++++\n 1 file changed, 18 insertions(+)\n\ndiff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh\nindex 9f701a1..aa24ea8 100755\n--- a/t/t2025-checkout-to.sh\n+++ b/t/t2025-checkout-to.sh\n@@ -96,4 +96,22 @@ test_expect_success 'checkout from a bare repo without --to' '\n \t)\n '\n \n+test_expect_success 'checkout with grafts' '\n+\ttest_when_finished rm .git/info/grafts &&\n+\ttest_commit abc &&\n+\tSHA1=`git rev-parse HEAD` &&\n+\ttest_commit def &&\n+\ttest_commit xyz &&\n+\techo \"`git rev-parse HEAD` $SHA1\" >.git/info/grafts &&\n+\tcat >expected <<-\\EOF &&\n+\txyz\n+\tabc\n+\tEOF\n+\tgit log --format=%s -2 >actual &&\n+\ttest_cmp expected actual &&\n+\tgit checkout --detach --to grafted master &&\n+\tgit --git-dir=grafted/.git log --format=%s -2 >actual &&\n+\ttest_cmp expected actual\n+'\n+\n test_done\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"248643","messageId":"CAPig+cT6J-qts-KqYK-quy+zLmWOfk6eeMfuoXBPKN4he=bncg@mail.gmail.com","threadId":"37450","inReplyTo":"1409387642-24492-1-git-send-email-pclouds@gmail.com","subject":"Re: [PATCH 00/32] nd/multiple-work-trees cleanup","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2014-08-30T11:11:08Z","receivedAt":"2014-08-30T11:11:08Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Sat, Aug 30, 2014 at 4:33 AM, Nguyễn Thái Ngọc Duy <pclouds@gmail.com> wrote:\n> This collapes some bug fix patches into the main ones, adds a few more\n> tests to cover recent changes, and removes advice.checkoutLocked (when\n> things are controversal, probably best to go without them until they\n> are settled). Diff against current version in 'pu'\n>\n> diff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt\n> index 6bd82af..c9a7d1a 100644\n> --- a/Documentation/gitrepository-layout.txt\n> +++ b/Documentation/gitrepository-layout.txt\n> @@ -218,11 +218,10 @@ remotes::\n>         \"$GIT_COMMON_DIR/remotes\" will be used instead.\n>\n>  logs::\n> -       Records of changes made to refs are stored in this\n> -       directory.  See linkgit:git-update-ref[1]\n> -       for more information. This directory is ignored if\n> -       $GIT_COMMON_DIR is set and \"$GIT_COMMON_DIR/logs\" will be used\n> -       instead.\n> +       Records of changes made to refs are stored in this directory.\n> +       See linkgit:git-update-ref[1] for more information. This\n> +       directory is ignored $GIT_COMMON_DIR is set and\n\nThe 'if' got lost again: s/ignored/ignored if/\n\n> +       \"$GIT_COMMON_DIR/logs\" will be used instead.\n>\n>  logs/refs/heads/`name`::\n>         Records all changes made to the branch tip named `name`.\n"},{"id":"248644","messageId":"CACsJy8AsFuxBcWcb0BvKKKnmN-9tWq=qYjOTKEWZpyQP_8nseQ@mail.gmail.com","threadId":"37450","inReplyTo":"CAPig+cT6J-qts-KqYK-quy+zLmWOfk6eeMfuoXBPKN4he=bncg@mail.gmail.com","subject":"Re: [PATCH 00/32] nd/multiple-work-trees cleanup","fromName":"Duy Nguyen","fromEmail":"pclouds@gmail.com","sentAt":"2014-08-30T11:14:57Z","receivedAt":"2014-08-30T11:14:57Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"On Sat, Aug 30, 2014 at 6:11 PM, Eric Sunshine <sunshine@sunshineco.com> wrote:\n> On Sat, Aug 30, 2014 at 4:33 AM, Nguyễn Thái Ngọc Duy <pclouds@gmail.com> wrote:\n>> This collapes some bug fix patches into the main ones, adds a few more\n>> tests to cover recent changes, and removes advice.checkoutLocked (when\n>> things are controversal, probably best to go without them until they\n>> are settled). Diff against current version in 'pu'\n>>\n>> diff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt\n>> index 6bd82af..c9a7d1a 100644\n>> --- a/Documentation/gitrepository-layout.txt\n>> +++ b/Documentation/gitrepository-layout.txt\n>> @@ -218,11 +218,10 @@ remotes::\n>>         \"$GIT_COMMON_DIR/remotes\" will be used instead.\n>>\n>>  logs::\n>> -       Records of changes made to refs are stored in this\n>> -       directory.  See linkgit:git-update-ref[1]\n>> -       for more information. This directory is ignored if\n>> -       $GIT_COMMON_DIR is set and \"$GIT_COMMON_DIR/logs\" will be used\n>> -       instead.\n>> +       Records of changes made to refs are stored in this directory.\n>> +       See linkgit:git-update-ref[1] for more information. This\n>> +       directory is ignored $GIT_COMMON_DIR is set and\n>\n> The 'if' got lost again: s/ignored/ignored if/\n\nArgghh.. 'if' hates me, or I hate it. Junio please put it back. I\ndon't think I should send another 32 mails for one 'if'.\n-- \nDuy\n"},{"id":"248660","messageId":"70985AC885404243A2B95F534083A0E9@PhilipOakley","threadId":"37450","inReplyTo":"1409387642-24492-23-git-send-email-pclouds@gmail.com","subject":"Re: [PATCH 22/32] checkout: support checking out into a new working directory","fromName":"Philip Oakley","fromEmail":"philipoakley@iee.org","sentAt":"2014-08-30T20:50:15Z","receivedAt":"2014-08-30T20:50:15Z","isPatch":true,"sender":{"key":"philipoakley@iee.email","avatar":"https://avatars.githubusercontent.com/u/914343?v=4"},"body":"From: \"Nguyễn Thái Ngọc Duy\" <pclouds@gmail.com>\n> \"git checkout --to\" sets up a new working directory with a .git file\n> pointing to $GIT_DIR/repos/<id>. It then executes \"git checkout\" again\n> on the new worktree with the same arguments except \"--to\" is taken\n> out. The second checkout execution, which is not contaminated with any\n> info from the current repository, will actually check out and\n> everything that normal \"git checkout\" does.\n>\n> Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n> ---\n> Documentation/git-checkout.txt         | 34 ++++++++++++\n> Documentation/git.txt                  |  3 +-\n> Documentation/gitrepository-layout.txt |  7 +++\n> builtin/checkout.c                     | 95 \n> +++++++++++++++++++++++++++++++++-\n> path.c                                 |  2 +-\n> t/t2025-checkout-to.sh (new +x)        | 63 ++++++++++++++++++++++\n> 6 files changed, 200 insertions(+), 4 deletions(-)\n> create mode 100755 t/t2025-checkout-to.sh\n>\n> diff --git a/Documentation/git-checkout.txt \n> b/Documentation/git-checkout.txt\n> index 33ad2ad..38c70c5 100644\n> --- a/Documentation/git-checkout.txt\n> +++ b/Documentation/git-checkout.txt\n> @@ -225,6 +225,13 @@ This means that you can use `git checkout -p` to \n> selectively discard\n> edits from your current working tree. See the ``Interactive Mode''\n> section of linkgit:git-add[1] to learn how to operate the `--patch` \n> mode.\n>\n> +--to=<path>::\n> + Check out a new branch in a separate working directory at\n\nIs this actually a 'new' branch? Perhaps s/new //. More below.\n\n> + `<path>`. A new working directory is linked to the current\n> + repository, sharing everything except working directory\n> + specific files such as HEAD, index... See \"MULTIPLE CHECKOUT\n> + MODE\" section for more information.\n> +\n> <branch>::\n>  Branch to checkout; if it refers to a branch (i.e., a name that,\n>  when prepended with \"refs/heads/\", is a valid ref), then that\n> @@ -388,6 +395,33 @@ $ git reflog -2 HEAD # or\n> $ git log -g -2 HEAD\n> ------------\n>\n> +MULTIPLE CHECKOUT MODE\n> +----------------------\n> +Normally a working directory is attached to repository. When \"git\n> +checkout --to\" is used, a new working directory is attached to the\n> +current repository. This new working directory is called \"linked\n> +checkout\" as compared to the \"main checkout\" prepared by \"git init\" \n> or\n> +\"git clone\". A repository has one main checkout and zero or more\n> +linked checkouts.\n> +\n> +All checkouts share the same repository. Linked checkouts see the\n> +repository a bit different from the main checkout. When the checkout\n> +\"new\" reads the path $GIT_DIR/HEAD for example, the actual path\n\nI was caught out by \"new\" (in quotes) here. I see that the commit msg in \npatch 24 uses 'new-branch', while in patch 30 <new> is used. Is this \n\"new\" (above) refering to the branch name (or detatched head), or a \nnmemonic for the path? The use of \"new working directory\" in the \nprevious paragraph is a well qualified use of new, so I'm looking to \nensure that it's well qualified here.\n\nMy immediate reaction was that a consistent use of <new> in the three \npatches may be all that's needed\n\n> +returned could be $GIT_DIR/repos/new/HEAD. This ensures checkouts\n\nsurely s/could/would/? or have I misunderstood?\nAlso maybe s/new/<new-name>/, given the 'usually named' below, or \ns/new/<id>/ based on the commit message.\n\n> +won't step on each other.\n> +\n> +Each linked checkout has a private space in $GIT_DIR/repos, usually\n> +named after the base name of the working directory with a number \n> added\n> +to make it unique. The linked checkout's $GIT_DIR points to this\n> +private space while $GIT_COMMON_DIR points to the main checkout's\n> +$GIT_DIR. These settings are done by \"git checkout --to\".\n> +\n> +Because in this mode $GIT_DIR becomes a lightweight virtual file\n> +system where a path could be rewritten to some place else, accessing\n> +$GIT_DIR from scripts should use `git rev-parse --git-path` to \n> resolve\n> +a path instead of using it directly unless the path is known to be\n> +private to the working directory.\n> +\n> EXAMPLES\n> --------\n>\n> diff --git a/Documentation/git.txt b/Documentation/git.txt\n> index 749052f..c0a4940 100644\n> --- a/Documentation/git.txt\n> +++ b/Documentation/git.txt\n> @@ -792,7 +792,8 @@ Git so take care if using Cogito etc.\n>  If this variable is set to a path, non-worktree files that are\n>  normally in $GIT_DIR will be taken from this path\n>  instead. Worktree-specific files such as HEAD or index are\n> - taken from $GIT_DIR. See linkgit:gitrepository-layout[5] for\n> + taken from $GIT_DIR. See linkgit:gitrepository-layout[5] and\n> + the section 'MULTIPLE CHECKOUT MODE' in linkgit:checkout[1]\n>  details. This variable has lower precedence than other path\n>  variables such as GIT_INDEX_FILE, GIT_OBJECT_DIRECTORY...\n>\n> diff --git a/Documentation/gitrepository-layout.txt \n> b/Documentation/gitrepository-layout.txt\n> index 58d1087..fab398a 100644\n> --- a/Documentation/gitrepository-layout.txt\n> +++ b/Documentation/gitrepository-layout.txt\n> @@ -248,6 +248,13 @@ modules::\n>  directory is ignored if $GIT_COMMON_DIR is set and\n>  \"$GIT_COMMON_DIR/modules\" will be used instead.\n>\n> +repos::\n> + Contains worktree specific information of linked\n> + checkouts. Each subdirectory contains the worktree-related\n> + part of a linked checkout. This directory is ignored if\n> + $GIT_COMMON_DIR is set and \"$GIT_COMMON_DIR/repos\" will be\n> + used instead.\n> +\n> SEE ALSO\n> --------\n> linkgit:git-init[1],\n> diff --git a/builtin/checkout.c b/builtin/checkout.c\n> index 8023987..6373823 100644\n> --- a/builtin/checkout.c\n> +++ b/builtin/checkout.c\n> @@ -48,6 +48,10 @@ struct checkout_opts {\n>  const char *prefix;\n>  struct pathspec pathspec;\n>  struct tree *source_tree;\n> +\n> + const char *new_worktree;\n> + const char **saved_argv;\n> + int new_worktree_mode;\n> };\n>\n> static int post_checkout_hook(struct commit *old, struct commit *new,\n> @@ -250,6 +254,9 @@ static int checkout_paths(const struct \n> checkout_opts *opts,\n>  die(_(\"Cannot update paths and switch to branch '%s' at the same \n> time.\"),\n>      opts->new_branch);\n>\n> + if (opts->new_worktree)\n> + die(_(\"'%s' cannot be used with updating paths\"), \"--to\");\n> +\n>  if (opts->patch_mode)\n>  return run_add_interactive(revision, \"--patch=checkout\",\n>     &opts->pathspec);\n> @@ -485,7 +492,7 @@ static int merge_working_tree(const struct \n> checkout_opts *opts,\n>  topts.dir->flags |= DIR_SHOW_IGNORED;\n>  setup_standard_excludes(topts.dir);\n>  }\n> - tree = parse_tree_indirect(old->commit ?\n> + tree = parse_tree_indirect(old->commit && !opts->new_worktree_mode ?\n>     old->commit->object.sha1 :\n>     EMPTY_TREE_SHA1_BIN);\n>  init_tree_desc(&trees[0], tree->buffer, tree->size);\n> @@ -796,7 +803,8 @@ static int switch_branches(const struct \n> checkout_opts *opts,\n>  return ret;\n>  }\n>\n> - if (!opts->quiet && !old.path && old.commit && new->commit != \n> old.commit)\n> + if (!opts->quiet && !old.path && old.commit &&\n> +     new->commit != old.commit && !opts->new_worktree_mode)\n>  orphaned_commit_warning(old.commit, new->commit);\n>\n>  update_refs_for_switch(opts, &old, new);\n> @@ -806,6 +814,76 @@ static int switch_branches(const struct \n> checkout_opts *opts,\n>  return ret || writeout_error;\n> }\n>\n> +static int prepare_linked_checkout(const struct checkout_opts *opts,\n> +    struct branch_info *new)\n> +{\n> + struct strbuf sb_git = STRBUF_INIT, sb_repo = STRBUF_INIT;\n> + struct strbuf sb = STRBUF_INIT;\n> + const char *path = opts->new_worktree, *name;\n> + struct stat st;\n> + struct child_process cp;\n> + int counter = 0, len;\n> +\n> + if (!new->commit)\n> + die(_(\"no branch specified\"));\n> + if (file_exists(path))\n> + die(_(\"'%s' already exists\"), path);\n> +\n> + len = strlen(path);\n> + while (len && is_dir_sep(path[len - 1]))\n> + len--;\n> +\n> + for (name = path + len - 1; name > path; name--)\n> + if (is_dir_sep(*name)) {\n> + name++;\n> + break;\n> + }\n> + strbuf_addstr(&sb_repo,\n> +       git_path(\"repos/%.*s\", (int)(path + len - name), name));\n> + len = sb_repo.len;\n> + if (safe_create_leading_directories_const(sb_repo.buf))\n> + die_errno(_(\"could not create leading directories of '%s'\"),\n> +   sb_repo.buf);\n> + while (!stat(sb_repo.buf, &st)) {\n> + counter++;\n> + strbuf_setlen(&sb_repo, len);\n> + strbuf_addf(&sb_repo, \"%d\", counter);\n> + }\n> + name = strrchr(sb_repo.buf, '/') + 1;\n> + if (mkdir(sb_repo.buf, 0777))\n> + die_errno(_(\"could not create directory of '%s'\"), sb_repo.buf);\n> +\n> + strbuf_addf(&sb_git, \"%s/.git\", path);\n> + if (safe_create_leading_directories_const(sb_git.buf))\n> + die_errno(_(\"could not create leading directories of '%s'\"),\n> +   sb_git.buf);\n> +\n> + write_file(sb_git.buf, 1, \"gitdir: %s/repos/%s\\n\",\n> +    real_path(get_git_common_dir()), name);\n> + /*\n> + * This is to keep resolve_ref() happy. We need a valid HEAD\n> + * or is_git_directory() will reject the directory. Any valid\n> + * value would do because this value will be ignored and\n> + * replaced at the next (real) checkout.\n> + */\n> + strbuf_addf(&sb, \"%s/HEAD\", sb_repo.buf);\n> + write_file(sb.buf, 1, \"%s\\n\", \n> sha1_to_hex(new->commit->object.sha1));\n> + strbuf_reset(&sb);\n> + strbuf_addf(&sb, \"%s/commondir\", sb_repo.buf);\n> + write_file(sb.buf, 1, \"../..\\n\");\n> +\n> + if (!opts->quiet)\n> + fprintf_ln(stderr, _(\"Enter %s (identifier %s)\"), path, name);\n> +\n> + setenv(\"GIT_CHECKOUT_NEW_WORKTREE\", \"1\", 1);\n> + setenv(GIT_DIR_ENVIRONMENT, sb_git.buf, 1);\n> + setenv(GIT_WORK_TREE_ENVIRONMENT, path, 1);\n> + memset(&cp, 0, sizeof(cp));\n> + cp.git_cmd = 1;\n> + cp.argv = opts->saved_argv;\n> + return run_command(&cp);\n> +}\n> +\n> static int git_checkout_config(const char *var, const char *value, \n> void *cb)\n> {\n>  if (!strcmp(var, \"diff.ignoresubmodules\")) {\n> @@ -1067,6 +1145,9 @@ static int checkout_branch(struct checkout_opts \n> *opts,\n>  die(_(\"Cannot switch branch to a non-commit '%s'\"),\n>      new->name);\n>\n> + if (opts->new_worktree)\n> + return prepare_linked_checkout(opts, new);\n> +\n>  if (!new->commit && opts->new_branch) {\n>  unsigned char rev[20];\n>  int flag;\n> @@ -1109,6 +1190,8 @@ int cmd_checkout(int argc, const char **argv, \n> const char *prefix)\n>  N_(\"do not limit pathspecs to sparse entries only\")),\n>  OPT_HIDDEN_BOOL(0, \"guess\", &dwim_new_local_branch,\n>  N_(\"second guess 'git checkout no-such-branch'\")),\n> + OPT_FILENAME(0, \"to\", &opts.new_worktree,\n> +    N_(\"check a branch out in a separate working directory\")),\n>  OPT_END(),\n>  };\n>\n> @@ -1117,6 +1200,9 @@ int cmd_checkout(int argc, const char **argv, \n> const char *prefix)\n>  opts.overwrite_ignore = 1;\n>  opts.prefix = prefix;\n>\n> + opts.saved_argv = xmalloc(sizeof(const char *) * (argc + 2));\n> + memcpy(opts.saved_argv, argv, sizeof(const char *) * (argc + 1));\n> +\n>  gitmodules_config();\n>  git_config(git_checkout_config, &opts);\n>\n> @@ -1125,6 +1211,11 @@ int cmd_checkout(int argc, const char **argv, \n> const char *prefix)\n>  argc = parse_options(argc, argv, prefix, options, checkout_usage,\n>       PARSE_OPT_KEEP_DASHDASH);\n>\n> + /* recursive execution from checkout_new_worktree() */\n> + opts.new_worktree_mode = getenv(\"GIT_CHECKOUT_NEW_WORKTREE\") != \n> NULL;\n> + if (opts.new_worktree_mode)\n> + opts.new_worktree = NULL;\n> +\n>  if (conflict_style) {\n>  opts.merge = 1; /* implied */\n>  git_xmerge_config(\"merge.conflictstyle\", conflict_style, NULL);\n> diff --git a/path.c b/path.c\n> index 8a6586c..e41d6b3 100644\n> --- a/path.c\n> +++ b/path.c\n> @@ -92,7 +92,7 @@ static void replace_dir(struct strbuf *buf, int len, \n> const char *newdir)\n>\n> static const char *common_list[] = {\n>  \"/branches\", \"/hooks\", \"/info\", \"/logs\", \"/lost-found\", \"/modules\",\n> - \"/objects\", \"/refs\", \"/remotes\", \"/rr-cache\", \"/svn\",\n> + \"/objects\", \"/refs\", \"/remotes\", \"/repos\", \"/rr-cache\", \"/svn\",\n>  \"config\", \"gc.pid\", \"packed-refs\", \"shallow\",\n>  NULL\n> };\n> diff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh\n> new file mode 100755\n> index 0000000..8c73b18\n> --- /dev/null\n> +++ b/t/t2025-checkout-to.sh\n> @@ -0,0 +1,63 @@\n> +#!/bin/sh\n> +\n> +test_description='test git checkout --to'\n> +\n> +. ./test-lib.sh\n> +\n> +test_expect_success 'setup' '\n> + test_commit init\n> +'\n> +\n> +test_expect_success 'checkout --to not updating paths' '\n> + test_must_fail git checkout --to -- init.t\n> +'\n> +\n> +test_expect_success 'checkout --to an existing worktree' '\n> + mkdir existing &&\n> + test_must_fail git checkout --detach --to existing master\n> +'\n> +\n> +test_expect_success 'checkout --to a new worktree' '\n> + git checkout --to here master &&\n> + (\n> + cd here &&\n> + test_cmp ../init.t init.t &&\n> + git symbolic-ref HEAD >actual &&\n> + echo refs/heads/master >expect &&\n> + test_cmp expect actual &&\n> + git fsck\n> + )\n> +'\n> +\n> +test_expect_success 'checkout --to a new worktree from a subdir' '\n> + (\n> + mkdir sub &&\n> + cd sub &&\n> + git checkout --detach --to here master &&\n> + cd here &&\n> + test_cmp ../../init.t init.t\n> + )\n> +'\n> +\n> +test_expect_success 'checkout --to from a linked checkout' '\n> + (\n> + cd here &&\n> + git checkout --to nested-here master\n> + cd nested-here &&\n> + git fsck\n> + )\n> +'\n> +\n> +test_expect_success 'checkout --to a new worktree creating new \n> branch' '\n> + git checkout --to there -b newmaster master &&\n> + (\n> + cd there &&\n> + test_cmp ../init.t init.t &&\n> + git symbolic-ref HEAD >actual &&\n> + echo refs/heads/newmaster >expect &&\n> + test_cmp expect actual &&\n> + git fsck\n> + )\n> +'\n> +\n> +test_done\n> -- \n> 2.1.0.rc0.78.gc0d8480\n>\n> --\nPhilip \n"},{"id":"248674","messageId":"CACsJy8CpGhLN58GyJa_3-PDqNqYUCshPEGDr8-pKngad+-oAdg@mail.gmail.com","threadId":"37450","inReplyTo":"70985AC885404243A2B95F534083A0E9@PhilipOakley","subject":"Re: [PATCH 22/32] checkout: support checking out into a new working directory","fromName":"Duy Nguyen","fromEmail":"pclouds@gmail.com","sentAt":"2014-08-31T04:49:17Z","receivedAt":"2014-08-31T04:49:17Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"On Sun, Aug 31, 2014 at 3:50 AM, Philip Oakley <philipoakley@iee.org> wrote:\n>> @@ -225,6 +225,13 @@ This means that you can use `git checkout -p` to\n>> selectively discard\n>> edits from your current working tree. See the ``Interactive Mode''\n>> section of linkgit:git-add[1] to learn how to operate the `--patch` mode.\n>>\n>> +--to=<path>::\n>> + Check out a new branch in a separate working directory at\n>\n>\n> Is this actually a 'new' branch? Perhaps s/new //. More below.\n\nOnly when -b is also used. Will fix.\n\n>> +All checkouts share the same repository. Linked checkouts see the\n>> +repository a bit different from the main checkout. When the checkout\n>> +\"new\" reads the path $GIT_DIR/HEAD for example, the actual path\n>\n>\n> I was caught out by \"new\" (in quotes) here. I see that the commit msg in\n> patch 24 uses 'new-branch', while in patch 30 <new> is used. Is this \"new\"\n> (above) refering to the branch name (or detatched head), or a nmemonic for\n> the path? The use of \"new working directory\" in the previous paragraph is a\n> well qualified use of new, so I'm looking to ensure that it's well qualified\n> here.\n>\n> My immediate reaction was that a consistent use of <new> in the three\n> patches may be all that's needed\n\nIt's probably clearer if I start that paragraph with \"git checkout\n--to <somewhere> <branch>\" then \"new\" is replaced with \"<branch>\". The\nthing is, each checkout needs a unique id, and this id is usually the\nbranch name to be checked out. But after worktree creation, even if\nthe user switches branch, the id remains the same.\n\n>> +returned could be $GIT_DIR/repos/new/HEAD. This ensures checkouts\n>\n>\n> surely s/could/would/? or have I misunderstood?\n\nIt's ../repos/new/...if the directory with the same name does not\nexist. If it does, a number will follow after 'new' to make it unique,\ne.g. ../repos/new1/... Perhaps this paragraph could be rewritten like\nthis\n\nAll checkouts share the same repository. Linked checkouts see the\nrepository a bit different from the main checkout. When you perform\nthe command\n\n------------\ngit checkout --to <some-location> <some-branch>\n------------\n\nThe checkout at <some-location> will have a unique id that is also the\nbranch name (e.g. \"<some-branch>\"). A number may be appended to the id\nto make it unique. All worktree-specific files of this new checkout\nare in $GIT_DIR/repos/<unique-id> of the main checkout. So \"HEAD\"\ninside the linked checkout will be resolved to\n\"$GIT_DIR/repos/<some-branch>/HEAD\", while \"HEAD\" from the main\ncheckout remains \"$GIT_DIR/HEAD\".\n-- \nDuy\n"},{"id":"248675","messageId":"CACsJy8ASW0xO3WzU7f+T4iDhjL=Y=C6K+Cvi-OWOnS16r=m+9Q@mail.gmail.com","threadId":"37450","inReplyTo":"CACsJy8CpGhLN58GyJa_3-PDqNqYUCshPEGDr8-pKngad+-oAdg@mail.gmail.com","subject":"Re: [PATCH 22/32] checkout: support checking out into a new working directory","fromName":"Duy Nguyen","fromEmail":"pclouds@gmail.com","sentAt":"2014-08-31T04:52:50Z","receivedAt":"2014-08-31T04:52:50Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"On Sun, Aug 31, 2014 at 11:49 AM, Duy Nguyen <pclouds@gmail.com> wrote:\n> All checkouts share the same repository. Linked checkouts see the\n> repository a bit different from the main checkout. When you perform\n> the command\n>\n> ------------\n> git checkout --to <some-location> <some-branch>\n> ------------\n>\n> The checkout at <some-location> will have a unique id that is also the\n> branch name (e.g. \"<some-branch>\"). A number may be appended to the id\n\n.. and I got my facts wrong. The above line should be:\n\nlast component of <some-location>. A number may be appended to the id\n\n> to make it unique. All worktree-specific files of this new checkout\n> are in $GIT_DIR/repos/<unique-id> of the main checkout. So \"HEAD\"\n> inside the linked checkout will be resolved to\n> \"$GIT_DIR/repos/<some-branch>/HEAD\", while \"HEAD\" from the main\n\ns/<some-branch>/<unique-id>/\n\n> checkout remains \"$GIT_DIR/HEAD\".\n\n\n\n-- \nDuy\n"},{"id":"248679","messageId":"64D6AA311E524C27A8B6EDE7A63489D7@PhilipOakley","threadId":"37450","inReplyTo":"CACsJy8ASW0xO3WzU7f+T4iDhjL=Y=C6K+Cvi-OWOnS16r=m+9Q@mail.gmail.com","subject":"Re: [PATCH 22/32] checkout: support checking out into a new working directory","fromName":"Philip Oakley","fromEmail":"philipoakley@iee.org","sentAt":"2014-08-31T11:08:49Z","receivedAt":"2014-08-31T11:08:49Z","isPatch":true,"sender":{"key":"philipoakley@iee.email","avatar":"https://avatars.githubusercontent.com/u/914343?v=4"},"body":"From: \"Duy Nguyen\" <pclouds@gmail.com>\n> On Sun, Aug 31, 2014 at 11:49 AM, Duy Nguyen <pclouds@gmail.com> \n> wrote:\n>> All checkouts share the same repository. Linked checkouts see the\n>> repository a bit different from the main checkout. When you perform\n>> the command\n>>\n>> ------------\n>> git checkout --to <some-location> <some-branch>\n>> ------------\n>>\n>> The checkout at <some-location> will have a unique id that is also \n>> the\n>> branch name (e.g. \"<some-branch>\"). A number may be appended to the \n>> id\n>\n> .. and I got my facts wrong. The above line should be:\n>\n> last component of <some-location>. A number may be appended to the id\n>\n\nThat's fine. Glad that we're on the same wavelength for the \ndocumentation.\n\n>> to make it unique. All worktree-specific files of this new checkout\n>> are in $GIT_DIR/repos/<unique-id> of the main checkout. So \"HEAD\"\n>> inside the linked checkout will be resolved to\n>> \"$GIT_DIR/repos/<some-branch>/HEAD\", while \"HEAD\" from the main\n>\n> s/<some-branch>/<unique-id>/\n>\n>> checkout remains \"$GIT_DIR/HEAD\".\n>\n>\n>\n> -- \nPhilip \n"},{"id":"248712","messageId":"CACsJy8CTLRpjmh+0K6ypLKUgi52gsFEqnNsha2yfOvRVCdhdMQ@mail.gmail.com","threadId":"37450","inReplyTo":"64D6AA311E524C27A8B6EDE7A63489D7@PhilipOakley","subject":"Re: [PATCH 22/32] checkout: support checking out into a new working directory","fromName":"Duy Nguyen","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-02T12:27:41Z","receivedAt":"2014-09-02T12:27:41Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"After reading this \"multiple checkout mode\" in git-checkout.txt, I'm\ntempted to rewrite it like this. I think the example makes it clearer\nwhat I mean. If nobody has any comments, I'm going to send v2 with\nthis (and other comments collected so far)\n\nMULTIPLE CHECKOUT MODE\n----------------------\nNormally a working directory is attached to repository. When \"git\ncheckout --to\" is used, a new working directory is attached to the\ncurrent repository. This new working directory is called \"linked\ncheckout\" as compared to the \"main checkout\" prepared by \"git init\" or\n\"git clone\". A repository has one main checkout and zero or more\nlinked checkouts.\n\nEach linked checkout has a private directory in $GIT_DIR/repos in the\nmain checkout, usually named after the base name of the new working\ndirectory, optionally with a number added to make it unique. For\nexample, the command `git checkout --to ../test-next next` running\nwith $GIT_DIR=/path/main may create the directory\n`$GIT_DIR/repos/test-next` (or `$GIT_DIR/repos/test-next1` if\n`test-next` is already taken).\n\nWithin a linked checkout, $GIT_DIR is set to point to this private\ndirectory (e.g. `/path/main/repos/test-next` in the example) and\n$GIT_COMMON_DIR is set to point back to the main checkout's $GIT_DIR\n(e.g. `/path/main`). Setting is done via a .git file located at the\ntop directory of the linked checkout.\n\nPath resolution via `git rev-parse --git-path` would use either\n$GIT_DIR or $GIT_COMMON_DIR depending on the path. For example, the\nlinked checkout's `$GIT_DIR/HEAD` resolve to\n`/path/main/repos/test-next/HEAD` (not `/path/main/HEAD` which is the\nmain checkout's HEAD) while `$GIT_DIR/refs/heads/master` would use\n$GIT_COMMON_DIR and resolve to `/path/main/refs/heads/master`,\nwhich is shared across checkouts.\n\nSee linkgit:gitrepository-layout[5] for more information. The rule of\nthumb is do not make any assumption about whether a path belongs to\n$GIT_DIR or $GIT_COMMON_DIR when you need to directly access something\ninside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.\n--\nDuy\n"},{"id":"248715","messageId":"5405D983.3050707@xiplink.com","threadId":"37450","inReplyTo":"CACsJy8CTLRpjmh+0K6ypLKUgi52gsFEqnNsha2yfOvRVCdhdMQ@mail.gmail.com","subject":"Re: [PATCH 22/32] checkout: support checking out into a new working directory","fromName":"Marc Branchaud","fromEmail":"marcnarc@xiplink.com","sentAt":"2014-09-02T14:51:47Z","receivedAt":"2014-09-02T14:51:47Z","isPatch":true,"sender":{"key":"marcnarc@xiplink.com","avatar":"https://avatars.githubusercontent.com/u/14980203?v=4"},"body":"On 14-09-02 08:27 AM, Duy Nguyen wrote:\n> After reading this \"multiple checkout mode\" in git-checkout.txt, I'm\n> tempted to rewrite it like this. I think the example makes it clearer\n> what I mean. If nobody has any comments, I'm going to send v2 with\n> this (and other comments collected so far)\n\nOverall I think focusing on the word \"checkout\" for this feature makes the\ndocumentation confusing.  It's also not a \"mode\" but just another git\nfeature.  Since this is all about multiple working directories (the phrase is\nactually \"working tree\" in the existing docs) we should just stick to that\nrather than introduce new terminology.\n\nFinally, a bit of bikeshedding, but I think \"$GIT_DIR/repos\" is also an\nunfortunate choice and that \"$GIT_DIR/worktrees\" would be better.\n\nSo I suggest the following for the new section:\n\n\nMULTIPLE WORKING TREES\n----------------------\n\nA git repository can support multiple working trees, allowing you to check\nout more than one branch at a time.  With `git checkout --to` a new working\ntree is associated with the repository.  This new working tree is called a\n\"linked working tree\" as opposed to the \"main working tree\" prepared by \"git\ninit\" or \"git clone\".  A repository has one main working tree (if it's not a\nbare repository) and zero or more linked working trees.\n\nEach linked working tree has a private sub-directory in the repository's\n$GIT_DIR/worktrees directory.  The private sub-directory's name is usually\nthe base name of the linked working tree's path, possibly appended with a\nnumber to make it unique.  For example, when `$GIT_DIR=/path/main/.git` the\ncommand `git checkout --to /path/other/test-next next` creates the linked\nworking tree in `/path/other/test-next` and also creates a\n`$GIT_DIR/worktrees/test-next` directory (or `$GIT_DIR/worktrees/test-next1`\nif `test-next` is already taken).\n\nWithin a linked working tree, $GIT_DIR is set to point to this private\ndirectory (e.g. `/path/main/.git/worktrees/test-next` in the example) and\n$GIT_COMMON_DIR is set to point back to the main working tree's $GIT_DIR\n(e.g. `/path/main/.git`). These settings are made in a `.git` file located at\nthe top directory of the linked working tree.\n\nPath resolution via `git rev-parse --git-path` uses either\n$GIT_DIR or $GIT_COMMON_DIR depending on the path. For example, in the\nlinked working tree `git rev-parse --git-path HEAD` returns\n`/path/main/.git/worktrees/test-next/HEAD` (not\n`/path/other/test-next/.git/HEAD` or `/path/main/.git/HEAD`) while `git\nrev-parse --git-path refs/heads/master` uses\n$GIT_COMMON_DIR and returns `/path/main/.git/refs/heads/master`,\nsince refs are shared across all working trees.\n\nSee linkgit:gitrepository-layout[5] for more information. The rule of\nthumb is do not make any assumption about whether a path belongs to\n$GIT_DIR or $GIT_COMMON_DIR when you need to directly access something\ninside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.\n\n\n\t\tM.\n"},{"id":"248718","messageId":"xmqqk35m2aoh.fsf@gitster.dls.corp.google.com","threadId":"37450","inReplyTo":"1409387642-24492-1-git-send-email-pclouds@gmail.com","subject":"Re: [PATCH 00/32] nd/multiple-work-trees cleanup","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2014-09-02T17:29:18Z","receivedAt":"2014-09-02T17:29:18Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Nguyễn Thái Ngọc Duy  <pclouds@gmail.com> writes:\n\n> This collapes some bug fix patches into the main ones, adds a few more\n> tests to cover recent changes, and removes advice.checkoutLocked (when\n> things are controversal, probably best to go without them until they\n> are settled).\n\nThis essentially replaces how the refs (and other things, but the\nmost invasive part of the change is about refs) in $GIT_DIR/ are\naccessed from other working trees via symbolic links in the\ncontrib/workdir script with a built-in logic.\n\nUnfortunately the way $GIT_DIR/refs/ and $GIT_DIR/packed-refs are\naccessed even in the main tree is being updated with a series of\nlarge topics that are in flight X-<.\n\nHow well would they play together?\n\nWill drop the one in 'pu' and replace with this round, but it may\nturn out that I may have to ask you and Ronnie to coordinate these\ntopics.  We'll see.\n\nThanks.\n"},{"id":"248719","messageId":"xmqqfvga2ai9.fsf@gitster.dls.corp.google.com","threadId":"37450","inReplyTo":"5405D983.3050707@xiplink.com","subject":"Re: [PATCH 22/32] checkout: support checking out into a new working directory","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2014-09-02T17:33:02Z","receivedAt":"2014-09-02T17:33:02Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Marc Branchaud <marcnarc@xiplink.com> writes:\n\n> On 14-09-02 08:27 AM, Duy Nguyen wrote:\n>> After reading this \"multiple checkout mode\" in git-checkout.txt, I'm\n>> tempted to rewrite it like this. I think the example makes it clearer\n>> what I mean. If nobody has any comments, I'm going to send v2 with\n>> this (and other comments collected so far)\n>\n> Overall I think focusing on the word \"checkout\" for this feature makes the\n> documentation confusing.  It's also not a \"mode\" but just another git\n> feature.  Since this is all about multiple working directories (the phrase is\n> actually \"working tree\" in the existing docs) we should just stick to that\n> rather than introduce new terminology.\n>\n> Finally, a bit of bikeshedding, but I think \"$GIT_DIR/repos\" is also an\n> unfortunate choice and that \"$GIT_DIR/worktrees\" would be better.\n\nI tried to stay away from bikeshedding, but a good phrasing is\nimportant.  I think $GIT_DIR/worktrees captures what they are trying\nto represent better in that they are not storing repository\ninformation, but they are about storing per-worktree state.\n\n> So I suggest the following for the new section:\n>\n>\n> MULTIPLE WORKING TREES\n> ----------------------\n>\n> A git repository can support multiple working trees, allowing you to check\n> out more than one branch at a time.  With `git checkout --to` a new working\n> tree is associated with the repository.  This new working tree is called a\n> \"linked working tree\" as opposed to the \"main working tree\" prepared by \"git\n> init\" or \"git clone\".  A repository has one main working tree (if it's not a\n> bare repository) and zero or more linked working trees.\n>\n> Each linked working tree has a private sub-directory in the repository's\n> $GIT_DIR/worktrees directory.  The private sub-directory's name is usually\n> the base name of the linked working tree's path, possibly appended with a\n> number to make it unique.  For example, when `$GIT_DIR=/path/main/.git` the\n> command `git checkout --to /path/other/test-next next` creates the linked\n> working tree in `/path/other/test-next` and also creates a\n> `$GIT_DIR/worktrees/test-next` directory (or `$GIT_DIR/worktrees/test-next1`\n> if `test-next` is already taken).\n>\n> Within a linked working tree, $GIT_DIR is set to point to this private\n> directory (e.g. `/path/main/.git/worktrees/test-next` in the example) and\n> $GIT_COMMON_DIR is set to point back to the main working tree's $GIT_DIR\n> (e.g. `/path/main/.git`). These settings are made in a `.git` file located at\n> the top directory of the linked working tree.\n>\n> Path resolution via `git rev-parse --git-path` uses either\n> $GIT_DIR or $GIT_COMMON_DIR depending on the path. For example, in the\n> linked working tree `git rev-parse --git-path HEAD` returns\n> `/path/main/.git/worktrees/test-next/HEAD` (not\n> `/path/other/test-next/.git/HEAD` or `/path/main/.git/HEAD`) while `git\n> rev-parse --git-path refs/heads/master` uses\n> $GIT_COMMON_DIR and returns `/path/main/.git/refs/heads/master`,\n> since refs are shared across all working trees.\n>\n> See linkgit:gitrepository-layout[5] for more information. The rule of\n> thumb is do not make any assumption about whether a path belongs to\n> $GIT_DIR or $GIT_COMMON_DIR when you need to directly access something\n> inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.\n>\n>\n> \t\tM.\n"},{"id":"248837","messageId":"CACsJy8B4BFMh692KJ5eVS6FsP_mCCMNCZcBdxyOMxfxEFBsQXw@mail.gmail.com","threadId":"37450","inReplyTo":"xmqqfvga2ai9.fsf@gitster.dls.corp.google.com","subject":"Re: [PATCH 22/32] checkout: support checking out into a new working directory","fromName":"Duy Nguyen","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-04T14:19:29Z","receivedAt":"2014-09-04T14:19:29Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"On Wed, Sep 3, 2014 at 12:33 AM, Junio C Hamano <gitster@pobox.com> wrote:\n> Marc Branchaud <marcnarc@xiplink.com> writes:\n>\n>> On 14-09-02 08:27 AM, Duy Nguyen wrote:\n>>> After reading this \"multiple checkout mode\" in git-checkout.txt, I'm\n>>> tempted to rewrite it like this. I think the example makes it clearer\n>>> what I mean. If nobody has any comments, I'm going to send v2 with\n>>> this (and other comments collected so far)\n>>\n>> Overall I think focusing on the word \"checkout\" for this feature makes the\n>> documentation confusing.  It's also not a \"mode\" but just another git\n>> feature.  Since this is all about multiple working directories (the phrase is\n>> actually \"working tree\" in the existing docs) we should just stick to that\n>> rather than introduce new terminology.\n>>\n>> Finally, a bit of bikeshedding, but I think \"$GIT_DIR/repos\" is also an\n>> unfortunate choice and that \"$GIT_DIR/worktrees\" would be better.\n>\n> I tried to stay away from bikeshedding, but a good phrasing is\n> important.  I think $GIT_DIR/worktrees captures what they are trying\n> to represent better in that they are not storing repository\n> information, but they are about storing per-worktree state.\n\nGood thing this topic is in pu. Will s/repos/worktrees/ then.\n\n>> So I suggest the following for the new section:\n\nWill replace my text with yours. Thanks.\n-- \nDuy\n"},{"id":"248889","messageId":"20140905032633.GB25339@odin.ulthar.us","threadId":"37450","inReplyTo":"5405D983.3050707@xiplink.com","subject":"Re: [PATCH 22/32] checkout: support checking out into a new working directory","fromName":"Scott Schmit","fromEmail":"i.grok@comcast.net","sentAt":"2014-09-05T03:26:34Z","receivedAt":"2014-09-05T03:26:34Z","isPatch":true,"sender":{"key":"i.grok@comcast.net","avatar":null},"body":"On Tue, Sep 02, 2014 at 10:51:47AM -0400, Marc Branchaud wrote:\n> MULTIPLE WORKING TREES\n> ----------------------\n> \n> A git repository can support multiple working trees, allowing you to check\n> out more than one branch at a time.  With `git checkout --to` a new working\n> tree is associated with the repository.  This new working tree is called a\n> \"linked working tree\" as opposed to the \"main working tree\" prepared by \"git\n> init\" or \"git clone\".  A repository has one main working tree (if it's not a\n> bare repository) and zero or more linked working trees.\n> \n> Each linked working tree has a private sub-directory in the repository's\n> $GIT_DIR/worktrees directory.  The private sub-directory's name is usually\n> the base name of the linked working tree's path, possibly appended with a\n> number to make it unique.  For example, when `$GIT_DIR=/path/main/.git` the\n> command `git checkout --to /path/other/test-next next` creates the linked\n> working tree in `/path/other/test-next` and also creates a\n> `$GIT_DIR/worktrees/test-next` directory (or `$GIT_DIR/worktrees/test-next1`\n> if `test-next` is already taken).\n\nAs a user, this leaves me with one other question -- what happens when\nI'm done with the test-next working tree and want to delete/rename it?\nIs that cleaned up automatically, or do I need to register that I'm\ngetting rid of/renaming it?  (Another use case is if I put the working\ntree on removable media for some reason.)\n"},{"id":"248899","messageId":"CACsJy8Ca3r9AXiyGtHx114VGH7k=wdZ_W1yvPc-uu125cRuM7w@mail.gmail.com","threadId":"37450","inReplyTo":"20140905032633.GB25339@odin.ulthar.us","subject":"Re: [PATCH 22/32] checkout: support checking out into a new working directory","fromName":"Duy Nguyen","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-05T13:42:32Z","receivedAt":"2014-09-05T13:42:32Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"On Fri, Sep 5, 2014 at 10:26 AM, Scott Schmit <i.grok@comcast.net> wrote:\n>> Each linked working tree has a private sub-directory in the repository's\n>> $GIT_DIR/worktrees directory.  The private sub-directory's name is usually\n>> the base name of the linked working tree's path, possibly appended with a\n>> number to make it unique.  For example, when `$GIT_DIR=/path/main/.git` the\n>> command `git checkout --to /path/other/test-next next` creates the linked\n>> working tree in `/path/other/test-next` and also creates a\n>> `$GIT_DIR/worktrees/test-next` directory (or `$GIT_DIR/worktrees/test-next1`\n>> if `test-next` is already taken).\n>\n> As a user, this leaves me with one other question -- what happens when\n> I'm done with the test-next working tree and want to delete/rename it?\n\nIf you rename it, the link remains and you can still continue to use\ntest-next with its new name. If you move it, or rename on a filesystem\nwithout hardlink support, then you may need to run something from the\nlinked worktree for it to fix up the link to main checkout. If you\nmove it on the same filesystem that supports hardlink, it's all good.\n\nIf you delete it, the part in $GIT_DIR/worktrees remains and can be\ncleaned up using \"git prune --repos\" (which I should rename to \"git\nprune --worktrees\" as well). At some point in future, this pruning\nshould be part of git-gc. See patch 25/32 [1] for detail. I know I\nhaven't added some user documentation about pruning. You're welcome to\nwrite up something about this ;)\n\nNote to self, you someone moves the _main_ checkout away, then they're\nscrewed. Should probably make a note about that somewhere in\ndocuments.\n\n> Is that cleaned up automatically, or do I need to register that I'm\n> getting rid of/renaming it?  (Another use case is if I put the working\n> tree on removable media for some reason.)\n\nRemovable media is covered in [1] as well. Though you'll need to\n\"lock\" the worktree in order to stop it from being pruned. In earlier\niterations, this locking could be done automatically at \"git checkout\n--to\" time, if it detects that the new worktree is on a different\nfilesystem than the main one. But that got dropped. We can add it back\nwhen this feature matures a bit.\n\n[1] http://article.gmane.org/gmane.comp.version-control.git/256236\n-- \nDuy\n"},{"id":"249028","messageId":"CACsJy8DxUguPohjYGTgnHjBc=0mqn20c67pTBN34VRz7oCjyGA@mail.gmail.com","threadId":"37450","inReplyTo":"xmqqfvga2ai9.fsf@gitster.dls.corp.google.com","subject":"Re: [PATCH 22/32] checkout: support checking out into a new working directory","fromName":"Duy Nguyen","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-08T10:52:47Z","receivedAt":"2014-09-08T10:52:47Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"On Wed, Sep 3, 2014 at 12:33 AM, Junio C Hamano <gitster@pobox.com> wrote:\n> Marc Branchaud <marcnarc@xiplink.com> writes:\n>\n>> On 14-09-02 08:27 AM, Duy Nguyen wrote:\n>>> After reading this \"multiple checkout mode\" in git-checkout.txt, I'm\n>>> tempted to rewrite it like this. I think the example makes it clearer\n>>> what I mean. If nobody has any comments, I'm going to send v2 with\n>>> this (and other comments collected so far)\n>>\n>> Overall I think focusing on the word \"checkout\" for this feature makes the\n>> documentation confusing.  It's also not a \"mode\" but just another git\n>> feature.  Since this is all about multiple working directories (the phrase is\n>> actually \"working tree\" in the existing docs) we should just stick to that\n>> rather than introduce new terminology.\n>>\n>> Finally, a bit of bikeshedding, but I think \"$GIT_DIR/repos\" is also an\n>> unfortunate choice and that \"$GIT_DIR/worktrees\" would be better.\n>\n> I tried to stay away from bikeshedding, but a good phrasing is\n> important.  I think $GIT_DIR/worktrees captures what they are trying\n> to represent better in that they are not storing repository\n> information, but they are about storing per-worktree state.\n\nWhile we're changing the terms, I wonder if \"primary working\ndirectory\" and \"secondary working directories\" are better than \"main\ncheckout\" and \"linked checkout\".\n-- \nDuy\n"},{"id":"249036","messageId":"540DB7E1.4090302@xiplink.com","threadId":"37450","inReplyTo":"CACsJy8DxUguPohjYGTgnHjBc=0mqn20c67pTBN34VRz7oCjyGA@mail.gmail.com","subject":"Re: [PATCH 22/32] checkout: support checking out into a new working directory","fromName":"Marc Branchaud","fromEmail":"marcnarc@xiplink.com","sentAt":"2014-09-08T14:06:25Z","receivedAt":"2014-09-08T14:06:25Z","isPatch":true,"sender":{"key":"marcnarc@xiplink.com","avatar":"https://avatars.githubusercontent.com/u/14980203?v=4"},"body":"On 14-09-08 06:52 AM, Duy Nguyen wrote:\n> \n> While we're changing the terms, I wonder if \"primary working\n> directory\" and \"secondary working directories\" are better than \"main\n> checkout\" and \"linked checkout\".\n\nI might have a slight preference for main/linked, because primary/secondary\ncan imply that there are further orders -- tertiary, quaternary, etc.  Also,\nat least in English, \"linked\" is commonly used and it doesn't necessarily\nimply an implementation (e.g. with hard or soft filesystem links).\n\n(How many angels can dance on the threshold of an open bikeshed door? :) )\n\n\t\tM.\n"},{"id":"249160","messageId":"1410388928-32265-1-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1409387642-24492-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v2 00/32] nd/multiple-work-trees","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-10T22:41:36Z","receivedAt":"2014-09-10T22:41:36Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Changes since last send (diff below):\n\n - rename $GIT_DIR/repos to $GIT_DIR/worktrees. update documents and\n   command options accordingly\n - rewrite the multiple checkout section in git-checkout.txt\n - reorder some patches to make sure the test suite always pass\n\ndiff --git a/Documentation/config.txt b/Documentation/config.txt\nindex 57999fa..a85f684 100644\n--- a/Documentation/config.txt\n+++ b/Documentation/config.txt\n@@ -1211,12 +1211,12 @@ gc.pruneexpire::\n \t\"now\" may be used to disable this  grace period and always prune\n \tunreachable objects immediately.\n \n-gc.prunereposexpire::\n+gc.pruneworktreesexpire::\n \tWhen 'git gc' is run, it will call\n-\t'prune --repos --expire 3.months.ago'.\n+\t'prune --worktrees --expire 3.months.ago'.\n \tOverride the grace period with this config variable. The value\n \t\"now\" may be used to disable the grace period and prune\n-\t$GIT_DIR/repos immediately.\n+\t$GIT_DIR/worktrees immediately.\n \n gc.reflogexpire::\n gc.<pattern>.reflogexpire::\ndiff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt\nindex 38c70c5..23f0c80 100644\n--- a/Documentation/git-checkout.txt\n+++ b/Documentation/git-checkout.txt\n@@ -226,7 +226,7 @@ edits from your current working tree. See the ``Interactive Mode''\n section of linkgit:git-add[1] to learn how to operate the `--patch` mode.\n \n --to=<path>::\n-\tCheck out a new branch in a separate working directory at\n+\tCheck out a branch in a separate working directory at\n \t`<path>`. A new working directory is linked to the current\n \trepository, sharing everything except working directory\n \tspecific files such as HEAD, index... See \"MULTIPLE CHECKOUT\n@@ -404,23 +404,50 @@ checkout\" as compared to the \"main checkout\" prepared by \"git init\" or\n \"git clone\". A repository has one main checkout and zero or more\n linked checkouts.\n \n-All checkouts share the same repository. Linked checkouts see the\n-repository a bit different from the main checkout. When the checkout\n-\"new\" reads the path $GIT_DIR/HEAD for example, the actual path\n-returned could be $GIT_DIR/repos/new/HEAD. This ensures checkouts\n-won't step on each other.\n-\n-Each linked checkout has a private space in $GIT_DIR/repos, usually\n-named after the base name of the working directory with a number added\n-to make it unique. The linked checkout's $GIT_DIR points to this\n-private space while $GIT_COMMON_DIR points to the main checkout's\n-$GIT_DIR. These settings are done by \"git checkout --to\".\n-\n-Because in this mode $GIT_DIR becomes a lightweight virtual file\n-system where a path could be rewritten to some place else, accessing\n-$GIT_DIR from scripts should use `git rev-parse --git-path` to resolve\n-a path instead of using it directly unless the path is known to be\n-private to the working directory.\n+Each linked checkout has a private directory in $GIT_DIR/worktrees in\n+the main checkout, usually named after the base name of the new\n+working directory, optionally with a number added to make it\n+unique. For example, the command `git checkout --to ../test-next next`\n+running with $GIT_DIR=/path/main may create the directory\n+`$GIT_DIR/worktrees/test-next` (or `$GIT_DIR/worktrees/test-next1` if\n+`test-next` is already taken).\n+\n+Within a linked checkout, $GIT_DIR is set to point to this private\n+directory (e.g. `/path/main/worktrees/test-next` in the example) and\n+$GIT_COMMON_DIR is set to point back to the main checkout's $GIT_DIR\n+(e.g. `/path/main`). Setting is done via a .git file located at the\n+top directory of the linked checkout.\n+\n+Path resolution via `git rev-parse --git-path` would use either\n+$GIT_DIR or $GIT_COMMON_DIR depending on the path. For example, the\n+linked checkout's `$GIT_DIR/HEAD` resolve to\n+`/path/main/worktrees/test-next/HEAD` (not `/path/main/HEAD` which is\n+the main checkout's HEAD) while `$GIT_DIR/refs/heads/master` would use\n+$GIT_COMMON_DIR and resolve to `/path/main/refs/heads/master`, which\n+is shared across checkouts.\n+\n+See linkgit:gitrepository-layout[5] for more information. The rule of\n+thumb is do not make any assumption about whether a path belongs to\n+$GIT_DIR or $GIT_COMMON_DIR when you need to directly access something\n+inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.\n+\n+When you are done, simply deleting the linked working directory would\n+suffice. $GIT_DIR/worktrees can be cleaned up using `git prune\n+--worktrees`, which is part of automated garbage collection.\n+\n+After you move a linked working directory to another file system, or\n+on a file system that does not support hard link, execute any git\n+command (e.g. `git status`) in the new working directory so that it\n+could update its location in $GIT_DIR/worktrees and not be\n+accidentally pruned.\n+\n+To stop `git prune --worktrees` from deleting a specific working\n+directory (e.g. because it's on a portable device), you could add the\n+file 'locked' to $GIT_DIR/worktrees. For example, if `.git` file of\n+the new working directory points to `/path/main/worktrees/test-next`,\n+the full path of the 'locked' file would be\n+`/path/main/worktrees/test-next/locked`. See\n+linkgit:gitrepository-layout[5] for details.\n \n EXAMPLES\n --------\ndiff --git a/Documentation/git-prune.txt b/Documentation/git-prune.txt\nindex 50e39ec..a0ea381 100644\n--- a/Documentation/git-prune.txt\n+++ b/Documentation/git-prune.txt\n@@ -48,8 +48,8 @@ OPTIONS\n --expire <time>::\n \tOnly expire loose objects older than <time>.\n \n---repos::\n-\tPrune directories in $GIT_DIR/repos.\n+--worktrees::\n+\tPrune dead worktree information in $GIT_DIR/worktrees.\n \n <head>...::\n \tIn addition to objects\ndiff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt\nindex c9a7d1a..5ceff51 100644\n--- a/Documentation/gitrepository-layout.txt\n+++ b/Documentation/gitrepository-layout.txt\n@@ -220,7 +220,7 @@ remotes::\n logs::\n \tRecords of changes made to refs are stored in this directory.\n \tSee linkgit:git-update-ref[1] for more information. This\n-\tdirectory is ignored $GIT_COMMON_DIR is set and\n+\tdirectory is ignored if $GIT_COMMON_DIR is set and\n \t\"$GIT_COMMON_DIR/logs\" will be used instead.\n \n logs/refs/heads/`name`::\n@@ -248,28 +248,28 @@ modules::\n \tdirectory is ignored if $GIT_COMMON_DIR is set and\n \t\"$GIT_COMMON_DIR/modules\" will be used instead.\n \n-repos::\n+worktrees::\n \tContains worktree specific information of linked\n \tcheckouts. Each subdirectory contains the worktree-related\n \tpart of a linked checkout. This directory is ignored if\n-\t$GIT_COMMON_DIR is set and \"$GIT_COMMON_DIR/repos\" will be\n+\t$GIT_COMMON_DIR is set and \"$GIT_COMMON_DIR/worktrees\" will be\n \tused instead.\n \n-repos/<id>/gitdir::\n+worktrees/<id>/gitdir::\n \tA text file containing the absolute path back to the .git file\n \tthat points to here. This is used to check if the linked\n \trepository has been manually removed and there is no need to\n \tkeep this directory any more. mtime of this file should be\n \tupdated every time the linked repository is accessed.\n \n-repos/<id>/locked::\n+worktrees/<id>/locked::\n \tIf this file exists, the linked repository may be on a\n \tportable device and not available. It does not mean that the\n-\tlinked repository is gone and `repos/<id>` could be\n+\tlinked repository is gone and `worktrees/<id>` could be\n \tremoved. The file's content contains a reason string on why\n \tthe repository is locked.\n \n-repos/<id>/link::\n+worktrees/<id>/link::\n \tIf this file exists, it is a hard link to the linked .git\n \tfile. It is used to detect if the linked repository is\n \tmanually removed.\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex b88c646..6106b81 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -874,7 +874,7 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,\n \t\t\tbreak;\n \t\t}\n \tstrbuf_addstr(&sb_repo,\n-\t\t      git_path(\"repos/%.*s\", (int)(path + len - name), name));\n+\t\t      git_path(\"worktrees/%.*s\", (int)(path + len - name), name));\n \tlen = sb_repo.len;\n \tif (safe_create_leading_directories_const(sb_repo.buf))\n \t\tdie_errno(_(\"could not create leading directories of '%s'\"),\n@@ -911,7 +911,7 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,\n \tstrbuf_reset(&sb);\n \tstrbuf_addf(&sb, \"%s/gitdir\", sb_repo.buf);\n \twrite_file(sb.buf, 1, \"%s\\n\", real_path(sb_git.buf));\n-\twrite_file(sb_git.buf, 1, \"gitdir: %s/repos/%s\\n\",\n+\twrite_file(sb_git.buf, 1, \"gitdir: %s/worktrees/%s\\n\",\n \t\t   real_path(get_git_common_dir()), name);\n \t/*\n \t * This is to keep resolve_ref() happy. We need a valid HEAD\n@@ -950,7 +950,6 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,\n \tstrbuf_release(&sb_repo);\n \tstrbuf_release(&sb_git);\n \treturn ret;\n-\n }\n \n static int git_checkout_config(const char *var, const char *value, void *cb)\n@@ -1016,7 +1015,7 @@ static void check_linked_checkout(struct branch_info *new, const char *id)\n \tconst char *start, *end;\n \n \tif (id)\n-\t\tstrbuf_addf(&path, \"%s/repos/%s/HEAD\", get_git_common_dir(), id);\n+\t\tstrbuf_addf(&path, \"%s/worktrees/%s/HEAD\", get_git_common_dir(), id);\n \telse\n \t\tstrbuf_addf(&path, \"%s/HEAD\", get_git_common_dir());\n \n@@ -1032,7 +1031,7 @@ static void check_linked_checkout(struct branch_info *new, const char *id)\n \t\tgoto done;\n \tif (id) {\n \t\tstrbuf_reset(&path);\n-\t\tstrbuf_addf(&path, \"%s/repos/%s/gitdir\", get_git_common_dir(), id);\n+\t\tstrbuf_addf(&path, \"%s/worktrees/%s/gitdir\", get_git_common_dir(), id);\n \t\tif (strbuf_read_file(&gitdir, path.buf, 0) <= 0)\n \t\t\tgoto done;\n \t\tstrbuf_rtrim(&gitdir);\n@@ -1051,7 +1050,7 @@ static void check_linked_checkouts(struct branch_info *new)\n \tDIR *dir;\n \tstruct dirent *d;\n \n-\tstrbuf_addf(&path, \"%s/repos\", get_git_common_dir());\n+\tstrbuf_addf(&path, \"%s/worktrees\", get_git_common_dir());\n \tif ((dir = opendir(path.buf)) == NULL) {\n \t\tstrbuf_release(&path);\n \t\treturn;\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 0c65808..00239ca 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -33,13 +33,13 @@ static int gc_auto_threshold = 6700;\n static int gc_auto_pack_limit = 50;\n static int detach_auto = 1;\n static const char *prune_expire = \"2.weeks.ago\";\n-static const char *prune_repos_expire = \"3.months.ago\";\n+static const char *prune_worktrees_expire = \"3.months.ago\";\n \n static struct argv_array pack_refs_cmd = ARGV_ARRAY_INIT;\n static struct argv_array reflog = ARGV_ARRAY_INIT;\n static struct argv_array repack = ARGV_ARRAY_INIT;\n static struct argv_array prune = ARGV_ARRAY_INIT;\n-static struct argv_array prune_repos = ARGV_ARRAY_INIT;\n+static struct argv_array prune_worktrees = ARGV_ARRAY_INIT;\n static struct argv_array rerere = ARGV_ARRAY_INIT;\n \n static char *pidfile;\n@@ -99,8 +99,8 @@ static int gc_config(const char *var, const char *value, void *cb)\n \t}\n \tif (!strcmp(var, \"gc.pruneexpire\"))\n \t\treturn git_config_date_string(&prune_expire, var, value);\n-\tif (!strcmp(var, \"gc.prunereposexpire\"))\n-\t\treturn git_config_date_string(&prune_repos_expire, var, value);\n+\tif (!strcmp(var, \"gc.pruneworktreesexpire\"))\n+\t\treturn git_config_date_string(&prune_worktrees_expire, var, value);\n \treturn git_default_config(var, value, cb);\n }\n \n@@ -308,7 +308,7 @@ int cmd_gc(int argc, const char **argv, const char *prefix)\n \targv_array_pushl(&reflog, \"reflog\", \"expire\", \"--all\", NULL);\n \targv_array_pushl(&repack, \"repack\", \"-d\", \"-l\", NULL);\n \targv_array_pushl(&prune, \"prune\", \"--expire\", NULL);\n-\targv_array_pushl(&prune_repos, \"prune\", \"--repos\", \"--expire\", NULL);\n+\targv_array_pushl(&prune_worktrees, \"prune\", \"--worktrees\", \"--expire\", NULL);\n \targv_array_pushl(&rerere, \"rerere\", \"gc\", NULL);\n \n \tgit_config(gc_config, NULL);\n@@ -378,10 +378,10 @@ int cmd_gc(int argc, const char **argv, const char *prefix)\n \t\t\treturn error(FAILED_RUN, prune.argv[0]);\n \t}\n \n-\tif (prune_repos_expire) {\n-\t\targv_array_push(&prune_repos, prune_repos_expire);\n-\t\tif (run_command_v_opt(prune_repos.argv, RUN_GIT_CMD))\n-\t\t\treturn error(FAILED_RUN, prune_repos.argv[0]);\n+\tif (prune_worktrees_expire) {\n+\t\targv_array_push(&prune_worktrees, prune_worktrees_expire);\n+\t\tif (run_command_v_opt(prune_worktrees.argv, RUN_GIT_CMD))\n+\t\t\treturn error(FAILED_RUN, prune_worktrees.argv[0]);\n \t}\n \n \tif (run_command_v_opt(rerere.argv, RUN_GIT_CMD))\ndiff --git a/builtin/prune.c b/builtin/prune.c\nindex e72c391..cf56110 100644\n--- a/builtin/prune.c\n+++ b/builtin/prune.c\n@@ -112,25 +112,25 @@ static void prune_object_dir(const char *path)\n \t}\n }\n \n-static int prune_repo_dir(const char *id, struct strbuf *reason)\n+static int prune_worktree(const char *id, struct strbuf *reason)\n {\n \tstruct stat st;\n \tchar *path;\n \tint fd, len;\n \n-\tif (!is_directory(git_path(\"repos/%s\", id))) {\n-\t\tstrbuf_addf(reason, _(\"Removing repos/%s: not a valid directory\"), id);\n+\tif (!is_directory(git_path(\"worktrees/%s\", id))) {\n+\t\tstrbuf_addf(reason, _(\"Removing worktrees/%s: not a valid directory\"), id);\n \t\treturn 1;\n \t}\n-\tif (file_exists(git_path(\"repos/%s/locked\", id)))\n+\tif (file_exists(git_path(\"worktrees/%s/locked\", id)))\n \t\treturn 0;\n-\tif (stat(git_path(\"repos/%s/gitdir\", id), &st)) {\n-\t\tstrbuf_addf(reason, _(\"Removing repos/%s: gitdir file does not exist\"), id);\n+\tif (stat(git_path(\"worktrees/%s/gitdir\", id), &st)) {\n+\t\tstrbuf_addf(reason, _(\"Removing worktrees/%s: gitdir file does not exist\"), id);\n \t\treturn 1;\n \t}\n-\tfd = open(git_path(\"repos/%s/gitdir\", id), O_RDONLY);\n+\tfd = open(git_path(\"worktrees/%s/gitdir\", id), O_RDONLY);\n \tif (fd < 0) {\n-\t\tstrbuf_addf(reason, _(\"Removing repos/%s: unable to read gitdir file (%s)\"),\n+\t\tstrbuf_addf(reason, _(\"Removing worktrees/%s: unable to read gitdir file (%s)\"),\n \t\t\t    id, strerror(errno));\n \t\treturn 1;\n \t}\n@@ -141,7 +141,7 @@ static int prune_repo_dir(const char *id, struct strbuf *reason)\n \twhile (len && (path[len - 1] == '\\n' || path[len - 1] == '\\r'))\n \t\tlen--;\n \tif (!len) {\n-\t\tstrbuf_addf(reason, _(\"Removing repos/%s: invalid gitdir file\"), id);\n+\t\tstrbuf_addf(reason, _(\"Removing worktrees/%s: invalid gitdir file\"), id);\n \t\tfree(path);\n \t\treturn 1;\n \t}\n@@ -153,21 +153,21 @@ static int prune_repo_dir(const char *id, struct strbuf *reason)\n \t\t * the repo is moved manually and has not been\n \t\t * accessed since?\n \t\t */\n-\t\tif (!stat(git_path(\"repos/%s/link\", id), &st_link) &&\n+\t\tif (!stat(git_path(\"worktrees/%s/link\", id), &st_link) &&\n \t\t    st_link.st_nlink > 1)\n \t\t\treturn 0;\n-\t\tstrbuf_addf(reason, _(\"Removing repos/%s: gitdir file points to non-existent location\"), id);\n+\t\tstrbuf_addf(reason, _(\"Removing worktrees/%s: gitdir file points to non-existent location\"), id);\n \t\treturn 1;\n \t}\n \tfree(path);\n \treturn st.st_mtime <= expire;\n }\n \n-static void prune_repos_dir(void)\n+static void prune_worktrees(void)\n {\n \tstruct strbuf reason = STRBUF_INIT;\n \tstruct strbuf path = STRBUF_INIT;\n-\tDIR *dir = opendir(git_path(\"repos\"));\n+\tDIR *dir = opendir(git_path(\"worktrees\"));\n \tstruct dirent *d;\n \tint ret;\n \tif (!dir)\n@@ -176,14 +176,14 @@ static void prune_repos_dir(void)\n \t\tif (!strcmp(d->d_name, \".\") || !strcmp(d->d_name, \"..\"))\n \t\t\tcontinue;\n \t\tstrbuf_reset(&reason);\n-\t\tif (!prune_repo_dir(d->d_name, &reason))\n+\t\tif (!prune_worktree(d->d_name, &reason))\n \t\t\tcontinue;\n \t\tif (show_only || verbose)\n \t\t\tprintf(\"%s\\n\", reason.buf);\n \t\tif (show_only)\n \t\t\tcontinue;\n \t\tstrbuf_reset(&path);\n-\t\tstrbuf_addstr(&path, git_path(\"repos/%s\", d->d_name));\n+\t\tstrbuf_addstr(&path, git_path(\"worktrees/%s\", d->d_name));\n \t\tret = remove_dir_recursively(&path, 0);\n \t\tif (ret < 0 && errno == ENOTDIR)\n \t\t\tret = unlink(path.buf);\n@@ -192,7 +192,7 @@ static void prune_repos_dir(void)\n \t}\n \tclosedir(dir);\n \tif (!show_only)\n-\t\trmdir(git_path(\"repos\"));\n+\t\trmdir(git_path(\"worktrees\"));\n \tstrbuf_release(&reason);\n \tstrbuf_release(&path);\n }\n@@ -223,12 +223,12 @@ int cmd_prune(int argc, const char **argv, const char *prefix)\n {\n \tstruct rev_info revs;\n \tstruct progress *progress = NULL;\n-\tint prune_repos = 0;\n+\tint do_prune_worktrees = 0;\n \tconst struct option options[] = {\n \t\tOPT__DRY_RUN(&show_only, N_(\"do not remove, show only\")),\n \t\tOPT__VERBOSE(&verbose, N_(\"report pruned objects\")),\n \t\tOPT_BOOL(0, \"progress\", &show_progress, N_(\"show progress\")),\n-\t\tOPT_BOOL(0, \"repos\", &prune_repos, N_(\"prune .git/repos/\")),\n+\t\tOPT_BOOL(0, \"worktrees\", &do_prune_worktrees, N_(\"prune .git/worktrees\")),\n \t\tOPT_EXPIRY_DATE(0, \"expire\", &expire,\n \t\t\t\tN_(\"expire objects older than <time>\")),\n \t\tOPT_END()\n@@ -242,10 +242,10 @@ int cmd_prune(int argc, const char **argv, const char *prefix)\n \n \targc = parse_options(argc, argv, prefix, options, prune_usage, 0);\n \n-\tif (prune_repos) {\n+\tif (do_prune_worktrees) {\n \t\tif (argc)\n-\t\t\tdie(_(\"--repos does not take extra arguments\"));\n-\t\tprune_repos_dir();\n+\t\t\tdie(_(\"--worktrees does not take extra arguments\"));\n+\t\tprune_worktrees();\n \t\treturn 0;\n \t}\n \ndiff --git a/path.c b/path.c\nindex b1dd2bc..1f1881a 100644\n--- a/path.c\n+++ b/path.c\n@@ -93,7 +93,7 @@ static void replace_dir(struct strbuf *buf, int len, const char *newdir)\n \n static const char *common_list[] = {\n \t\"/branches\", \"/hooks\", \"/info\", \"!/logs\", \"/lost-found\", \"/modules\",\n-\t\"/objects\", \"/refs\", \"/remotes\", \"/repos\", \"/rr-cache\", \"/svn\",\n+\t\"/objects\", \"/refs\", \"/remotes\", \"/worktrees\", \"/rr-cache\", \"/svn\",\n \t\"config\", \"!gc.pid\", \"packed-refs\", \"shallow\",\n \tNULL\n };\ndiff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh\nindex aa24ea8..27384a1 100755\n--- a/t/t2025-checkout-to.sh\n+++ b/t/t2025-checkout-to.sh\n@@ -20,7 +20,7 @@ test_expect_success 'checkout --to an existing worktree' '\n test_expect_success 'checkout --to refuses to checkout locked branch' '\n \ttest_must_fail git checkout --to zere master &&\n \t! test -d zere &&\n-\t! test -d .git/repos/zere\n+\t! test -d .git/worktrees/zere\n '\n \n test_expect_success 'checkout --to a new worktree' '\ndiff --git a/t/t2026-prune-linked-checkouts.sh b/t/t2026-prune-linked-checkouts.sh\nindex 79d84cb..3622800 100755\n--- a/t/t2026-prune-linked-checkouts.sh\n+++ b/t/t2026-prune-linked-checkouts.sh\n@@ -1,84 +1,84 @@\n #!/bin/sh\n \n-test_description='prune $GIT_DIR/repos'\n+test_description='prune $GIT_DIR/worktrees'\n \n . ./test-lib.sh\n \n-test_expect_success 'prune --repos on normal repo' '\n-\tgit prune --repos &&\n-\ttest_must_fail git prune --repos abc\n+test_expect_success 'prune --worktrees on normal repo' '\n+\tgit prune --worktrees &&\n+\ttest_must_fail git prune --worktrees abc\n '\n \n-test_expect_success 'prune files inside $GIT_DIR/repos' '\n-\tmkdir .git/repos &&\n-\t: >.git/repos/abc &&\n-\tgit prune --repos --verbose >actual &&\n+test_expect_success 'prune files inside $GIT_DIR/worktrees' '\n+\tmkdir .git/worktrees &&\n+\t: >.git/worktrees/abc &&\n+\tgit prune --worktrees --verbose >actual &&\n \tcat >expect <<EOF &&\n-Removing repos/abc: not a valid directory\n+Removing worktrees/abc: not a valid directory\n EOF\n \ttest_i18ncmp expect actual &&\n-\t! test -f .git/repos/abc &&\n-\t! test -d .git/repos\n+\t! test -f .git/worktrees/abc &&\n+\t! test -d .git/worktrees\n '\n \n test_expect_success 'prune directories without gitdir' '\n-\tmkdir -p .git/repos/def/abc &&\n-\t: >.git/repos/def/def &&\n+\tmkdir -p .git/worktrees/def/abc &&\n+\t: >.git/worktrees/def/def &&\n \tcat >expect <<EOF &&\n-Removing repos/def: gitdir file does not exist\n+Removing worktrees/def: gitdir file does not exist\n EOF\n-\tgit prune --repos --verbose >actual &&\n+\tgit prune --worktrees --verbose >actual &&\n \ttest_i18ncmp expect actual &&\n-\t! test -d .git/repos/def &&\n-\t! test -d .git/repos\n+\t! test -d .git/worktrees/def &&\n+\t! test -d .git/worktrees\n '\n \n test_expect_success POSIXPERM 'prune directories with unreadable gitdir' '\n-\tmkdir -p .git/repos/def/abc &&\n-\t: >.git/repos/def/def &&\n-\t: >.git/repos/def/gitdir &&\n-\tchmod u-r .git/repos/def/gitdir &&\n-\tgit prune --repos --verbose >actual &&\n-\ttest_i18ngrep \"Removing repos/def: unable to read gitdir file\" actual &&\n-\t! test -d .git/repos/def &&\n-\t! test -d .git/repos\n+\tmkdir -p .git/worktrees/def/abc &&\n+\t: >.git/worktrees/def/def &&\n+\t: >.git/worktrees/def/gitdir &&\n+\tchmod u-r .git/worktrees/def/gitdir &&\n+\tgit prune --worktrees --verbose >actual &&\n+\ttest_i18ngrep \"Removing worktrees/def: unable to read gitdir file\" actual &&\n+\t! test -d .git/worktrees/def &&\n+\t! test -d .git/worktrees\n '\n \n test_expect_success 'prune directories with invalid gitdir' '\n-\tmkdir -p .git/repos/def/abc &&\n-\t: >.git/repos/def/def &&\n-\t: >.git/repos/def/gitdir &&\n-\tgit prune --repos --verbose >actual &&\n-\ttest_i18ngrep \"Removing repos/def: invalid gitdir file\" actual &&\n-\t! test -d .git/repos/def &&\n-\t! test -d .git/repos\n+\tmkdir -p .git/worktrees/def/abc &&\n+\t: >.git/worktrees/def/def &&\n+\t: >.git/worktrees/def/gitdir &&\n+\tgit prune --worktrees --verbose >actual &&\n+\ttest_i18ngrep \"Removing worktrees/def: invalid gitdir file\" actual &&\n+\t! test -d .git/worktrees/def &&\n+\t! test -d .git/worktrees\n '\n \n test_expect_success 'prune directories with gitdir pointing to nowhere' '\n-\tmkdir -p .git/repos/def/abc &&\n-\t: >.git/repos/def/def &&\n-\techo \"$TRASH_DIRECTORY\"/nowhere >.git/repos/def/gitdir &&\n-\tgit prune --repos --verbose >actual &&\n-\ttest_i18ngrep \"Removing repos/def: gitdir file points to non-existent location\" actual &&\n-\t! test -d .git/repos/def &&\n-\t! test -d .git/repos\n+\tmkdir -p .git/worktrees/def/abc &&\n+\t: >.git/worktrees/def/def &&\n+\techo \"$TRASH_DIRECTORY\"/nowhere >.git/worktrees/def/gitdir &&\n+\tgit prune --worktrees --verbose >actual &&\n+\ttest_i18ngrep \"Removing worktrees/def: gitdir file points to non-existent location\" actual &&\n+\t! test -d .git/worktrees/def &&\n+\t! test -d .git/worktrees\n '\n \n test_expect_success 'not prune locked checkout' '\n-\ttest_when_finished rm -r .git/repos\n-\tmkdir -p .git/repos/ghi &&\n-\t: >.git/repos/ghi/locked &&\n-\tgit prune --repos &&\n-\ttest -d .git/repos/ghi\n+\ttest_when_finished rm -r .git/worktrees\n+\tmkdir -p .git/worktrees/ghi &&\n+\t: >.git/worktrees/ghi/locked &&\n+\tgit prune --worktrees &&\n+\ttest -d .git/worktrees/ghi\n '\n \n test_expect_success 'not prune recent checkouts' '\n-\ttest_when_finished rm -r .git/repos\n+\ttest_when_finished rm -r .git/worktrees\n \tmkdir zz &&\n-\tmkdir -p .git/repos/jlm &&\n-\techo \"$TRASH_DIRECTORY\"/zz >.git/repos/jlm/gitdir &&\n-\tgit prune --repos --verbose --expire=2.days.ago &&\n-\ttest -d .git/repos/jlm\n+\tmkdir -p .git/worktrees/jlm &&\n+\techo \"$TRASH_DIRECTORY\"/zz >.git/worktrees/jlm/gitdir &&\n+\tgit prune --worktrees --verbose --expire=2.days.ago &&\n+\ttest -d .git/worktrees/jlm\n '\n \n test_done\n\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249161","messageId":"1410388928-32265-2-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1410388928-32265-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v2 01/32] path.c: make get_pathname() return strbuf instead of static buffer","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-10T22:41:37Z","receivedAt":"2014-09-10T22:41:37Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"We've been avoiding PATH_MAX whenever possible. This patch makes\nget_pathname() return a strbuf and updates the callers to take\nadvantage of this. The code is simplified as we no longer need to\nworry about buffer overflow.\n\nvsnpath() behavior is changed slightly: previously it always clears\nthe buffer before writing, now it just appends. Fortunately this is a\nstatic function and all of its callers prepare the buffer properly:\ngit_path() gets the buffer from get_pathname() which resets the\nbuffer, the remaining call sites start with STRBUF_INIT'd buffer.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n path.c | 120 ++++++++++++++++++++++++++++-------------------------------------\n 1 file changed, 51 insertions(+), 69 deletions(-)\n\ndiff --git a/path.c b/path.c\nindex bc804a3..42ef3af 100644\n--- a/path.c\n+++ b/path.c\n@@ -16,11 +16,15 @@ static int get_st_mode_bits(const char *path, int *mode)\n \n static char bad_path[] = \"/bad-path/\";\n \n-static char *get_pathname(void)\n+static struct strbuf *get_pathname(void)\n {\n-\tstatic char pathname_array[4][PATH_MAX];\n+\tstatic struct strbuf pathname_array[4] = {\n+\t\tSTRBUF_INIT, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT\n+\t};\n \tstatic int index;\n-\treturn pathname_array[3 & ++index];\n+\tstruct strbuf *sb = &pathname_array[3 & ++index];\n+\tstrbuf_reset(sb);\n+\treturn sb;\n }\n \n static char *cleanup_path(char *path)\n@@ -34,6 +38,13 @@ static char *cleanup_path(char *path)\n \treturn path;\n }\n \n+static void strbuf_cleanup_path(struct strbuf *sb)\n+{\n+\tchar *path = cleanup_path(sb->buf);\n+\tif (path > sb->buf)\n+\t\tstrbuf_remove(sb, 0, path - sb->buf);\n+}\n+\n char *mksnpath(char *buf, size_t n, const char *fmt, ...)\n {\n \tva_list args;\n@@ -49,85 +60,70 @@ char *mksnpath(char *buf, size_t n, const char *fmt, ...)\n \treturn cleanup_path(buf);\n }\n \n-static char *vsnpath(char *buf, size_t n, const char *fmt, va_list args)\n+static void vsnpath(struct strbuf *buf, const char *fmt, va_list args)\n {\n \tconst char *git_dir = get_git_dir();\n-\tsize_t len;\n-\n-\tlen = strlen(git_dir);\n-\tif (n < len + 1)\n-\t\tgoto bad;\n-\tmemcpy(buf, git_dir, len);\n-\tif (len && !is_dir_sep(git_dir[len-1]))\n-\t\tbuf[len++] = '/';\n-\tlen += vsnprintf(buf + len, n - len, fmt, args);\n-\tif (len >= n)\n-\t\tgoto bad;\n-\treturn cleanup_path(buf);\n-bad:\n-\tstrlcpy(buf, bad_path, n);\n-\treturn buf;\n+\tstrbuf_addstr(buf, git_dir);\n+\tif (buf->len && !is_dir_sep(buf->buf[buf->len - 1]))\n+\t\tstrbuf_addch(buf, '/');\n+\tstrbuf_vaddf(buf, fmt, args);\n+\tstrbuf_cleanup_path(buf);\n }\n \n char *git_snpath(char *buf, size_t n, const char *fmt, ...)\n {\n-\tchar *ret;\n+\tstruct strbuf sb = STRBUF_INIT;\n \tva_list args;\n \tva_start(args, fmt);\n-\tret = vsnpath(buf, n, fmt, args);\n+\tvsnpath(&sb, fmt, args);\n \tva_end(args);\n-\treturn ret;\n+\tif (sb.len >= n)\n+\t\tstrlcpy(buf, bad_path, n);\n+\telse\n+\t\tmemcpy(buf, sb.buf, sb.len + 1);\n+\tstrbuf_release(&sb);\n+\treturn buf;\n }\n \n char *git_pathdup(const char *fmt, ...)\n {\n-\tchar path[PATH_MAX], *ret;\n+\tstruct strbuf path = STRBUF_INIT;\n \tva_list args;\n \tva_start(args, fmt);\n-\tret = vsnpath(path, sizeof(path), fmt, args);\n+\tvsnpath(&path, fmt, args);\n \tva_end(args);\n-\treturn xstrdup(ret);\n+\treturn strbuf_detach(&path, NULL);\n }\n \n char *mkpathdup(const char *fmt, ...)\n {\n-\tchar *path;\n \tstruct strbuf sb = STRBUF_INIT;\n \tva_list args;\n-\n \tva_start(args, fmt);\n \tstrbuf_vaddf(&sb, fmt, args);\n \tva_end(args);\n-\tpath = xstrdup(cleanup_path(sb.buf));\n-\n-\tstrbuf_release(&sb);\n-\treturn path;\n+\tstrbuf_cleanup_path(&sb);\n+\treturn strbuf_detach(&sb, NULL);\n }\n \n char *mkpath(const char *fmt, ...)\n {\n \tva_list args;\n-\tunsigned len;\n-\tchar *pathname = get_pathname();\n-\n+\tstruct strbuf *pathname = get_pathname();\n \tva_start(args, fmt);\n-\tlen = vsnprintf(pathname, PATH_MAX, fmt, args);\n+\tstrbuf_vaddf(pathname, fmt, args);\n \tva_end(args);\n-\tif (len >= PATH_MAX)\n-\t\treturn bad_path;\n-\treturn cleanup_path(pathname);\n+\treturn cleanup_path(pathname->buf);\n }\n \n char *git_path(const char *fmt, ...)\n {\n-\tchar *pathname = get_pathname();\n+\tstruct strbuf *pathname = get_pathname();\n \tva_list args;\n-\tchar *ret;\n-\n \tva_start(args, fmt);\n-\tret = vsnpath(pathname, PATH_MAX, fmt, args);\n+\tvsnpath(pathname, fmt, args);\n \tva_end(args);\n-\treturn ret;\n+\treturn pathname->buf;\n }\n \n void home_config_paths(char **global, char **xdg, char *file)\n@@ -158,41 +154,27 @@ void home_config_paths(char **global, char **xdg, char *file)\n \n char *git_path_submodule(const char *path, const char *fmt, ...)\n {\n-\tchar *pathname = get_pathname();\n-\tstruct strbuf buf = STRBUF_INIT;\n+\tstruct strbuf *buf = get_pathname();\n \tconst char *git_dir;\n \tva_list args;\n-\tunsigned len;\n-\n-\tlen = strlen(path);\n-\tif (len > PATH_MAX-100)\n-\t\treturn bad_path;\n \n-\tstrbuf_addstr(&buf, path);\n-\tif (len && path[len-1] != '/')\n-\t\tstrbuf_addch(&buf, '/');\n-\tstrbuf_addstr(&buf, \".git\");\n+\tstrbuf_addstr(buf, path);\n+\tif (buf->len && buf->buf[buf->len - 1] != '/')\n+\t\tstrbuf_addch(buf, '/');\n+\tstrbuf_addstr(buf, \".git\");\n \n-\tgit_dir = read_gitfile(buf.buf);\n+\tgit_dir = read_gitfile(buf->buf);\n \tif (git_dir) {\n-\t\tstrbuf_reset(&buf);\n-\t\tstrbuf_addstr(&buf, git_dir);\n+\t\tstrbuf_reset(buf);\n+\t\tstrbuf_addstr(buf, git_dir);\n \t}\n-\tstrbuf_addch(&buf, '/');\n-\n-\tif (buf.len >= PATH_MAX)\n-\t\treturn bad_path;\n-\tmemcpy(pathname, buf.buf, buf.len + 1);\n-\n-\tstrbuf_release(&buf);\n-\tlen = strlen(pathname);\n+\tstrbuf_addch(buf, '/');\n \n \tva_start(args, fmt);\n-\tlen += vsnprintf(pathname + len, PATH_MAX - len, fmt, args);\n+\tstrbuf_vaddf(buf, fmt, args);\n \tva_end(args);\n-\tif (len >= PATH_MAX)\n-\t\treturn bad_path;\n-\treturn cleanup_path(pathname);\n+\tstrbuf_cleanup_path(buf);\n+\treturn buf->buf;\n }\n \n int validate_headref(const char *path)\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249162","messageId":"1410388928-32265-3-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1410388928-32265-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v2 02/32] path.c: make get_pathname() call sites return const char *","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-10T22:41:38Z","receivedAt":"2014-09-10T22:41:38Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Before the previous commit, get_pathname returns an array of PATH_MAX\nlength. Even if git_path() and similar functions does not use the\nwhole array, git_path() caller can, in theory.\n\nAfter the commit, get_pathname() may return a buffer that has just\nenough room for the returned string and git_path() caller should never\nwrite beyond that.\n\nMake git_path(), mkpath() and git_path_submodule() return a const\nbuffer to make sure callers do not write in it at all.\n\nThis could have been part of the previous commit, but the \"const\"\nconversion is too much distraction from the core changes in path.c.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n builtin/checkout.c     | 2 +-\n builtin/clone.c        | 9 +++++----\n builtin/fetch.c        | 5 +++--\n builtin/fsck.c         | 4 ++--\n builtin/receive-pack.c | 2 +-\n builtin/remote.c       | 2 +-\n builtin/repack.c       | 8 +++++---\n cache.h                | 6 +++---\n fast-import.c          | 2 +-\n notes-merge.c          | 6 +++---\n path.c                 | 6 +++---\n refs.c                 | 8 ++++----\n run-command.c          | 4 ++--\n run-command.h          | 2 +-\n sha1_file.c            | 2 +-\n 15 files changed, 36 insertions(+), 32 deletions(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 463cfee..3abcef1 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -585,7 +585,7 @@ static void update_refs_for_switch(const struct checkout_opts *opts,\n \t\t\tif (opts->new_branch_log && !log_all_ref_updates) {\n \t\t\t\tint temp;\n \t\t\t\tchar log_file[PATH_MAX];\n-\t\t\t\tchar *ref_name = mkpath(\"refs/heads/%s\", opts->new_orphan_branch);\n+\t\t\t\tconst char *ref_name = mkpath(\"refs/heads/%s\", opts->new_orphan_branch);\n \n \t\t\t\ttemp = log_all_ref_updates;\n \t\t\t\tlog_all_ref_updates = 1;\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex e15ca33..91fac9d 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -289,16 +289,17 @@ static void copy_alternates(struct strbuf *src, struct strbuf *dst,\n \tstruct strbuf line = STRBUF_INIT;\n \n \twhile (strbuf_getline(&line, in, '\\n') != EOF) {\n-\t\tchar *abs_path, abs_buf[PATH_MAX];\n+\t\tchar *abs_path;\n \t\tif (!line.len || line.buf[0] == '#')\n \t\t\tcontinue;\n \t\tif (is_absolute_path(line.buf)) {\n \t\t\tadd_to_alternates_file(line.buf);\n \t\t\tcontinue;\n \t\t}\n-\t\tabs_path = mkpath(\"%s/objects/%s\", src_repo, line.buf);\n-\t\tnormalize_path_copy(abs_buf, abs_path);\n-\t\tadd_to_alternates_file(abs_buf);\n+\t\tabs_path = mkpathdup(\"%s/objects/%s\", src_repo, line.buf);\n+\t\tnormalize_path_copy(abs_path, abs_path);\n+\t\tadd_to_alternates_file(abs_path);\n+\t\tfree(abs_path);\n \t}\n \tstrbuf_release(&line);\n \tfclose(in);\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex e8d0cca..9522b1b 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -573,7 +573,8 @@ static int store_updated_refs(const char *raw_url, const char *remote_name,\n \tstruct strbuf note = STRBUF_INIT;\n \tconst char *what, *kind;\n \tstruct ref *rm;\n-\tchar *url, *filename = dry_run ? \"/dev/null\" : git_path(\"FETCH_HEAD\");\n+\tchar *url;\n+\tconst char *filename = dry_run ? \"/dev/null\" : git_path(\"FETCH_HEAD\");\n \tint want_status;\n \n \tfp = fopen(filename, \"a\");\n@@ -807,7 +808,7 @@ static void check_not_current_branch(struct ref *ref_map)\n \n static int truncate_fetch_head(void)\n {\n-\tchar *filename = git_path(\"FETCH_HEAD\");\n+\tconst char *filename = git_path(\"FETCH_HEAD\");\n \tFILE *fp = fopen(filename, \"w\");\n \n \tif (!fp)\ndiff --git a/builtin/fsck.c b/builtin/fsck.c\nindex 8aadca1..d414962 100644\n--- a/builtin/fsck.c\n+++ b/builtin/fsck.c\n@@ -225,12 +225,12 @@ static void check_unreachable_object(struct object *obj)\n \t\t\tprintf(\"dangling %s %s\\n\", typename(obj->type),\n \t\t\t       sha1_to_hex(obj->sha1));\n \t\tif (write_lost_and_found) {\n-\t\t\tchar *filename = git_path(\"lost-found/%s/%s\",\n+\t\t\tconst char *filename = git_path(\"lost-found/%s/%s\",\n \t\t\t\tobj->type == OBJ_COMMIT ? \"commit\" : \"other\",\n \t\t\t\tsha1_to_hex(obj->sha1));\n \t\t\tFILE *f;\n \n-\t\t\tif (safe_create_leading_directories(filename)) {\n+\t\t\tif (safe_create_leading_directories_const(filename)) {\n \t\t\t\terror(\"Could not create lost-found\");\n \t\t\t\treturn;\n \t\t\t}\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 18458e8..ed11e7e 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -599,7 +599,7 @@ static void run_update_post_hook(struct command *commands)\n \tint argc;\n \tconst char **argv;\n \tstruct child_process proc;\n-\tchar *hook;\n+\tconst char *hook;\n \n \thook = find_hook(\"post-update\");\n \tfor (argc = 0, cmd = commands; cmd; cmd = cmd->next) {\ndiff --git a/builtin/remote.c b/builtin/remote.c\nindex a8efe3d..3d99c9c 100644\n--- a/builtin/remote.c\n+++ b/builtin/remote.c\n@@ -580,7 +580,7 @@ static int migrate_file(struct remote *remote)\n {\n \tstruct strbuf buf = STRBUF_INIT;\n \tint i;\n-\tchar *path = NULL;\n+\tconst char *path = NULL;\n \n \tstrbuf_addf(&buf, \"remote.%s.url\", remote->name);\n \tfor (i = 0; i < remote->url_nr; i++)\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex ff2216a..a64a4a9 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -285,7 +285,8 @@ int cmd_repack(int argc, const char **argv, const char *prefix)\n \tfailed = 0;\n \tfor_each_string_list_item(item, &names) {\n \t\tfor (ext = 0; ext < ARRAY_SIZE(exts); ext++) {\n-\t\t\tchar *fname, *fname_old;\n+\t\t\tconst char *fname_old;\n+\t\t\tchar *fname;\n \t\t\tfname = mkpathdup(\"%s/pack-%s%s\", packdir,\n \t\t\t\t\t\titem->string, exts[ext].name);\n \t\t\tif (!file_exists(fname)) {\n@@ -313,7 +314,8 @@ int cmd_repack(int argc, const char **argv, const char *prefix)\n \tif (failed) {\n \t\tstruct string_list rollback_failure = STRING_LIST_INIT_DUP;\n \t\tfor_each_string_list_item(item, &rollback) {\n-\t\t\tchar *fname, *fname_old;\n+\t\t\tconst char *fname_old;\n+\t\t\tchar *fname;\n \t\t\tfname = mkpathdup(\"%s/%s\", packdir, item->string);\n \t\t\tfname_old = mkpath(\"%s/old-%s\", packdir, item->string);\n \t\t\tif (rename(fname_old, fname))\n@@ -366,7 +368,7 @@ int cmd_repack(int argc, const char **argv, const char *prefix)\n \t/* Remove the \"old-\" files */\n \tfor_each_string_list_item(item, &names) {\n \t\tfor (ext = 0; ext < ARRAY_SIZE(exts); ext++) {\n-\t\t\tchar *fname;\n+\t\t\tconst char *fname;\n \t\t\tfname = mkpath(\"%s/old-%s%s\",\n \t\t\t\t\tpackdir,\n \t\t\t\t\titem->string,\ndiff --git a/cache.h b/cache.h\nindex 44aa439..707408b 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -682,9 +682,9 @@ extern char *mkpathdup(const char *fmt, ...)\n \t__attribute__((format (printf, 1, 2)));\n \n /* Return a statically allocated filename matching the sha1 signature */\n-extern char *mkpath(const char *fmt, ...) __attribute__((format (printf, 1, 2)));\n-extern char *git_path(const char *fmt, ...) __attribute__((format (printf, 1, 2)));\n-extern char *git_path_submodule(const char *path, const char *fmt, ...)\n+extern const char *mkpath(const char *fmt, ...) __attribute__((format (printf, 1, 2)));\n+extern const char *git_path(const char *fmt, ...) __attribute__((format (printf, 1, 2)));\n+extern const char *git_path_submodule(const char *path, const char *fmt, ...)\n \t__attribute__((format (printf, 2, 3)));\n \n /*\ndiff --git a/fast-import.c b/fast-import.c\nindex fa635f7..d9c068b 100644\n--- a/fast-import.c\n+++ b/fast-import.c\n@@ -404,7 +404,7 @@ static void dump_marks_helper(FILE *, uintmax_t, struct mark_set *);\n \n static void write_crash_report(const char *err)\n {\n-\tchar *loc = git_path(\"fast_import_crash_%\"PRIuMAX, (uintmax_t) getpid());\n+\tconst char *loc = git_path(\"fast_import_crash_%\"PRIuMAX, (uintmax_t) getpid());\n \tFILE *rpt = fopen(loc, \"w\");\n \tstruct branch *b;\n \tunsigned long lu;\ndiff --git a/notes-merge.c b/notes-merge.c\nindex fd5fae2..a9e6b15 100644\n--- a/notes-merge.c\n+++ b/notes-merge.c\n@@ -280,7 +280,7 @@ static void check_notes_merge_worktree(struct notes_merge_options *o)\n \t\t\t\t    \"(%s exists).\", git_path(\"NOTES_MERGE_*\"));\n \t\t}\n \n-\t\tif (safe_create_leading_directories(git_path(\n+\t\tif (safe_create_leading_directories_const(git_path(\n \t\t\t\tNOTES_MERGE_WORKTREE \"/.test\")))\n \t\t\tdie_errno(\"unable to create directory %s\",\n \t\t\t\t  git_path(NOTES_MERGE_WORKTREE));\n@@ -295,8 +295,8 @@ static void write_buf_to_worktree(const unsigned char *obj,\n \t\t\t\t  const char *buf, unsigned long size)\n {\n \tint fd;\n-\tchar *path = git_path(NOTES_MERGE_WORKTREE \"/%s\", sha1_to_hex(obj));\n-\tif (safe_create_leading_directories(path))\n+\tconst char *path = git_path(NOTES_MERGE_WORKTREE \"/%s\", sha1_to_hex(obj));\n+\tif (safe_create_leading_directories_const(path))\n \t\tdie_errno(\"unable to create directory for '%s'\", path);\n \tif (file_exists(path))\n \t\tdie(\"found existing file at '%s'\", path);\ndiff --git a/path.c b/path.c\nindex 42ef3af..a3f8826 100644\n--- a/path.c\n+++ b/path.c\n@@ -106,7 +106,7 @@ char *mkpathdup(const char *fmt, ...)\n \treturn strbuf_detach(&sb, NULL);\n }\n \n-char *mkpath(const char *fmt, ...)\n+const char *mkpath(const char *fmt, ...)\n {\n \tva_list args;\n \tstruct strbuf *pathname = get_pathname();\n@@ -116,7 +116,7 @@ char *mkpath(const char *fmt, ...)\n \treturn cleanup_path(pathname->buf);\n }\n \n-char *git_path(const char *fmt, ...)\n+const char *git_path(const char *fmt, ...)\n {\n \tstruct strbuf *pathname = get_pathname();\n \tva_list args;\n@@ -152,7 +152,7 @@ void home_config_paths(char **global, char **xdg, char *file)\n \tfree(to_free);\n }\n \n-char *git_path_submodule(const char *path, const char *fmt, ...)\n+const char *git_path_submodule(const char *path, const char *fmt, ...)\n {\n \tstruct strbuf *buf = get_pathname();\n \tconst char *git_dir;\ndiff --git a/refs.c b/refs.c\nindex 82e4842..dbd131e 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1442,7 +1442,7 @@ static int resolve_gitlink_ref_recursive(struct ref_cache *refs,\n {\n \tint fd, len;\n \tchar buffer[128], *p;\n-\tchar *path;\n+\tconst char *path;\n \n \tif (recursion > MAXDEPTH || strlen(refname) > MAXREFLEN)\n \t\treturn -1;\n@@ -2255,7 +2255,7 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname,\n \t\t\t\t\t    const unsigned char *old_sha1,\n \t\t\t\t\t    int flags, int *type_p)\n {\n-\tchar *ref_file;\n+\tconst char *ref_file;\n \tconst char *orig_refname = refname;\n \tstruct ref_lock *lock;\n \tint last_errno = 0;\n@@ -2318,7 +2318,7 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname,\n \t\tlock->force_write = 1;\n \n  retry:\n-\tswitch (safe_create_leading_directories(ref_file)) {\n+\tswitch (safe_create_leading_directories_const(ref_file)) {\n \tcase SCLD_OK:\n \t\tbreak; /* success */\n \tcase SCLD_VANISHED:\n@@ -2756,7 +2756,7 @@ static int rename_tmp_log(const char *newrefname)\n \tint attempts_remaining = 4;\n \n  retry:\n-\tswitch (safe_create_leading_directories(git_path(\"logs/%s\", newrefname))) {\n+\tswitch (safe_create_leading_directories_const(git_path(\"logs/%s\", newrefname))) {\n \tcase SCLD_OK:\n \t\tbreak; /* success */\n \tcase SCLD_VANISHED:\ndiff --git a/run-command.c b/run-command.c\nindex be07d4a..614b8ac 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -758,9 +758,9 @@ int finish_async(struct async *async)\n #endif\n }\n \n-char *find_hook(const char *name)\n+const char *find_hook(const char *name)\n {\n-\tchar *path = git_path(\"hooks/%s\", name);\n+\tconst char *path = git_path(\"hooks/%s\", name);\n \tif (access(path, X_OK) < 0)\n \t\tpath = NULL;\n \ndiff --git a/run-command.h b/run-command.h\nindex ea73de3..890cc95 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -48,7 +48,7 @@ int start_command(struct child_process *);\n int finish_command(struct child_process *);\n int run_command(struct child_process *);\n \n-extern char *find_hook(const char *name);\n+extern const char *find_hook(const char *name);\n LAST_ARG_MUST_BE_NULL\n extern int run_hook_le(const char *const *env, const char *name, ...);\n extern int run_hook_ve(const char *const *env, const char *name, va_list args);\ndiff --git a/sha1_file.c b/sha1_file.c\nindex a38854c..9700108 100644\n--- a/sha1_file.c\n+++ b/sha1_file.c\n@@ -402,7 +402,7 @@ void add_to_alternates_file(const char *reference)\n {\n \tstruct lock_file *lock = xcalloc(1, sizeof(struct lock_file));\n \tint fd = hold_lock_file_for_append(lock, git_path(\"objects/info/alternates\"), LOCK_DIE_ON_ERROR);\n-\tchar *alt = mkpath(\"%s\\n\", reference);\n+\tconst char *alt = mkpath(\"%s\\n\", reference);\n \twrite_or_die(fd, alt, strlen(alt));\n \tif (commit_lock_file(lock))\n \t\tdie(\"could not close alternates file\");\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249163","messageId":"1410388928-32265-4-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1410388928-32265-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v2 03/32] git_snpath(): retire and replace with strbuf_git_path()","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-10T22:41:39Z","receivedAt":"2014-09-10T22:41:39Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"In the previous patch, git_snpath() is modified to allocate a new\nstrbuf buffer because vsnpath() needs that. But that makes it\nawkward because git_snpath() receives a pre-allocated buffer from\noutside and has to copy data back. Rename it to strbuf_git_path()\nand make it receive strbuf directly.\n\nUsing git_path() in update_refs_for_switch() which used to call\ngit_snpath() is safe because that function and all of its callers do\nnot keep any pointer to the round-robin buffer pool allocated by\nget_pathname().\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n builtin/checkout.c | 13 +++++----\n cache.h            |  4 +--\n path.c             | 11 ++------\n refs.c             | 78 +++++++++++++++++++++++++++++++++---------------------\n refs.h             |  2 +-\n 5 files changed, 61 insertions(+), 47 deletions(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 3abcef1..8023987 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -584,18 +584,21 @@ static void update_refs_for_switch(const struct checkout_opts *opts,\n \t\tif (opts->new_orphan_branch) {\n \t\t\tif (opts->new_branch_log && !log_all_ref_updates) {\n \t\t\t\tint temp;\n-\t\t\t\tchar log_file[PATH_MAX];\n-\t\t\t\tconst char *ref_name = mkpath(\"refs/heads/%s\", opts->new_orphan_branch);\n+\t\t\t\tstruct strbuf log_file = STRBUF_INIT;\n+\t\t\t\tint ret;\n+\t\t\t\tconst char *ref_name;\n \n+\t\t\t\tref_name = mkpath(\"refs/heads/%s\", opts->new_orphan_branch);\n \t\t\t\ttemp = log_all_ref_updates;\n \t\t\t\tlog_all_ref_updates = 1;\n-\t\t\t\tif (log_ref_setup(ref_name, log_file, sizeof(log_file))) {\n+\t\t\t\tret = log_ref_setup(ref_name, &log_file);\n+\t\t\t\tlog_all_ref_updates = temp;\n+\t\t\t\tstrbuf_release(&log_file);\n+\t\t\t\tif (ret) {\n \t\t\t\t\tfprintf(stderr, _(\"Can not do reflog for '%s'\\n\"),\n \t\t\t\t\t    opts->new_orphan_branch);\n-\t\t\t\t\tlog_all_ref_updates = temp;\n \t\t\t\t\treturn;\n \t\t\t\t}\n-\t\t\t\tlog_all_ref_updates = temp;\n \t\t\t}\n \t\t}\n \t\telse\ndiff --git a/cache.h b/cache.h\nindex 707408b..279b581 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -674,8 +674,8 @@ extern int check_repository_format(void);\n \n extern char *mksnpath(char *buf, size_t n, const char *fmt, ...)\n \t__attribute__((format (printf, 3, 4)));\n-extern char *git_snpath(char *buf, size_t n, const char *fmt, ...)\n-\t__attribute__((format (printf, 3, 4)));\n+extern void strbuf_git_path(struct strbuf *sb, const char *fmt, ...)\n+\t__attribute__((format (printf, 2, 3)));\n extern char *git_pathdup(const char *fmt, ...)\n \t__attribute__((format (printf, 1, 2)));\n extern char *mkpathdup(const char *fmt, ...)\ndiff --git a/path.c b/path.c\nindex a3f8826..e545064 100644\n--- a/path.c\n+++ b/path.c\n@@ -70,19 +70,12 @@ static void vsnpath(struct strbuf *buf, const char *fmt, va_list args)\n \tstrbuf_cleanup_path(buf);\n }\n \n-char *git_snpath(char *buf, size_t n, const char *fmt, ...)\n+void strbuf_git_path(struct strbuf *sb, const char *fmt, ...)\n {\n-\tstruct strbuf sb = STRBUF_INIT;\n \tva_list args;\n \tva_start(args, fmt);\n-\tvsnpath(&sb, fmt, args);\n+\tvsnpath(sb, fmt, args);\n \tva_end(args);\n-\tif (sb.len >= n)\n-\t\tstrlcpy(buf, bad_path, n);\n-\telse\n-\t\tmemcpy(buf, sb.buf, sb.len + 1);\n-\tstrbuf_release(&sb);\n-\treturn buf;\n }\n \n char *git_pathdup(const char *fmt, ...)\ndiff --git a/refs.c b/refs.c\nindex dbd131e..00b2312 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1535,10 +1535,12 @@ static const char *handle_missing_loose_ref(const char *refname,\n \n const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int reading, int *flag)\n {\n+\tstruct strbuf sb_path = STRBUF_INIT;\n \tint depth = MAXDEPTH;\n \tssize_t len;\n \tchar buffer[256];\n \tstatic char refname_buffer[256];\n+\tconst char *ret;\n \n \tif (flag)\n \t\t*flag = 0;\n@@ -1547,15 +1549,17 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea\n \t\treturn NULL;\n \n \tfor (;;) {\n-\t\tchar path[PATH_MAX];\n+\t\tconst char *path;\n \t\tstruct stat st;\n \t\tchar *buf;\n \t\tint fd;\n \n \t\tif (--depth < 0)\n-\t\t\treturn NULL;\n+\t\t\tgoto fail;\n \n-\t\tgit_snpath(path, sizeof(path), \"%s\", refname);\n+\t\tstrbuf_reset(&sb_path);\n+\t\tstrbuf_git_path(&sb_path, \"%s\", refname);\n+\t\tpath = sb_path.buf;\n \n \t\t/*\n \t\t * We might have to loop back here to avoid a race\n@@ -1569,10 +1573,11 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea\n \tstat_ref:\n \t\tif (lstat(path, &st) < 0) {\n \t\t\tif (errno == ENOENT)\n-\t\t\t\treturn handle_missing_loose_ref(refname, sha1,\n-\t\t\t\t\t\t\t\treading, flag);\n+\t\t\t\tret = handle_missing_loose_ref(refname, sha1,\n+\t\t\t\t\t\t\t       reading, flag);\n \t\t\telse\n-\t\t\t\treturn NULL;\n+\t\t\t\tret = NULL;\n+\t\t\tgoto done;\n \t\t}\n \n \t\t/* Follow \"normalized\" - ie \"refs/..\" symlinks by hand */\n@@ -1583,7 +1588,7 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea\n \t\t\t\t\t/* inconsistent with lstat; retry */\n \t\t\t\t\tgoto stat_ref;\n \t\t\t\telse\n-\t\t\t\t\treturn NULL;\n+\t\t\t\t\tgoto fail;\n \t\t\t}\n \t\t\tbuffer[len] = 0;\n \t\t\tif (starts_with(buffer, \"refs/\") &&\n@@ -1599,7 +1604,7 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea\n \t\t/* Is it a directory? */\n \t\tif (S_ISDIR(st.st_mode)) {\n \t\t\terrno = EISDIR;\n-\t\t\treturn NULL;\n+\t\t\tgoto fail;\n \t\t}\n \n \t\t/*\n@@ -1612,12 +1617,13 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea\n \t\t\t\t/* inconsistent with lstat; retry */\n \t\t\t\tgoto stat_ref;\n \t\t\telse\n-\t\t\t\treturn NULL;\n+\t\t\t\tgoto fail;\n \t\t}\n+\n \t\tlen = read_in_full(fd, buffer, sizeof(buffer)-1);\n \t\tclose(fd);\n \t\tif (len < 0)\n-\t\t\treturn NULL;\n+\t\t\tgoto fail;\n \t\twhile (len && isspace(buffer[len-1]))\n \t\t\tlen--;\n \t\tbuffer[len] = '\\0';\n@@ -1634,9 +1640,10 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea\n \t\t\t    (buffer[40] != '\\0' && !isspace(buffer[40]))) {\n \t\t\t\tif (flag)\n \t\t\t\t\t*flag |= REF_ISBROKEN;\n-\t\t\t\treturn NULL;\n+\t\t\t\tgoto fail;\n \t\t\t}\n-\t\t\treturn refname;\n+\t\t\tret = refname;\n+\t\t\tgoto done;\n \t\t}\n \t\tif (flag)\n \t\t\t*flag |= REF_ISSYMREF;\n@@ -1646,10 +1653,15 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea\n \t\tif (check_refname_format(buf, REFNAME_ALLOW_ONELEVEL)) {\n \t\t\tif (flag)\n \t\t\t\t*flag |= REF_ISBROKEN;\n-\t\t\treturn NULL;\n+\t\t\tgoto fail;\n \t\t}\n \t\trefname = strcpy(refname_buffer, buf);\n \t}\n+fail:\n+\tret = NULL;\n+done:\n+\tstrbuf_release(&sb_path);\n+\treturn ret;\n }\n \n char *resolve_refdup(const char *ref, unsigned char *sha1, int reading, int *flag)\n@@ -2940,41 +2952,41 @@ static int copy_msg(char *buf, const char *msg)\n \treturn cp - buf;\n }\n \n-int log_ref_setup(const char *refname, char *logfile, int bufsize)\n+int log_ref_setup(const char *refname, struct strbuf *logfile)\n {\n \tint logfd, oflags = O_APPEND | O_WRONLY;\n \n-\tgit_snpath(logfile, bufsize, \"logs/%s\", refname);\n+\tstrbuf_git_path(logfile, \"logs/%s\", refname);\n \tif (log_all_ref_updates &&\n \t    (starts_with(refname, \"refs/heads/\") ||\n \t     starts_with(refname, \"refs/remotes/\") ||\n \t     starts_with(refname, \"refs/notes/\") ||\n \t     !strcmp(refname, \"HEAD\"))) {\n-\t\tif (safe_create_leading_directories(logfile) < 0)\n+\t\tif (safe_create_leading_directories(logfile->buf) < 0)\n \t\t\treturn error(\"unable to create directory for %s\",\n-\t\t\t\t     logfile);\n+\t\t\t\t     logfile->buf);\n \t\toflags |= O_CREAT;\n \t}\n \n-\tlogfd = open(logfile, oflags, 0666);\n+\tlogfd = open(logfile->buf, oflags, 0666);\n \tif (logfd < 0) {\n \t\tif (!(oflags & O_CREAT) && errno == ENOENT)\n \t\t\treturn 0;\n \n \t\tif ((oflags & O_CREAT) && errno == EISDIR) {\n-\t\t\tif (remove_empty_directories(logfile)) {\n+\t\t\tif (remove_empty_directories(logfile->buf)) {\n \t\t\t\treturn error(\"There are still logs under '%s'\",\n-\t\t\t\t\t     logfile);\n+\t\t\t\t\t     logfile->buf);\n \t\t\t}\n-\t\t\tlogfd = open(logfile, oflags, 0666);\n+\t\t\tlogfd = open(logfile->buf, oflags, 0666);\n \t\t}\n \n \t\tif (logfd < 0)\n \t\t\treturn error(\"Unable to append to %s: %s\",\n-\t\t\t\t     logfile, strerror(errno));\n+\t\t\t\t     logfile->buf, strerror(errno));\n \t}\n \n-\tadjust_shared_perm(logfile);\n+\tadjust_shared_perm(logfile->buf);\n \tclose(logfd);\n \treturn 0;\n }\n@@ -2985,20 +2997,22 @@ static int log_ref_write(const char *refname, const unsigned char *old_sha1,\n \tint logfd, result, written, oflags = O_APPEND | O_WRONLY;\n \tunsigned maxlen, len;\n \tint msglen;\n-\tchar log_file[PATH_MAX];\n+\tstruct strbuf sb_log_file = STRBUF_INIT;\n+\tconst char *log_file;\n \tchar *logrec;\n \tconst char *committer;\n \n \tif (log_all_ref_updates < 0)\n \t\tlog_all_ref_updates = !is_bare_repository();\n \n-\tresult = log_ref_setup(refname, log_file, sizeof(log_file));\n+\tresult = log_ref_setup(refname, &sb_log_file);\n \tif (result)\n-\t\treturn result;\n+\t\tgoto done;\n+\tlog_file = sb_log_file.buf;\n \n \tlogfd = open(log_file, oflags);\n \tif (logfd < 0)\n-\t\treturn 0;\n+\t\tgoto done;\n \tmsglen = msg ? strlen(msg) : 0;\n \tcommitter = git_committer_info(0);\n \tmaxlen = strlen(committer) + msglen + 100;\n@@ -3011,9 +3025,13 @@ static int log_ref_write(const char *refname, const unsigned char *old_sha1,\n \t\tlen += copy_msg(logrec + len - 1, msg) - 1;\n \twritten = len <= maxlen ? write_in_full(logfd, logrec, len) : -1;\n \tfree(logrec);\n-\tif (close(logfd) != 0 || written != len)\n-\t\treturn error(\"Unable to append to %s\", log_file);\n-\treturn 0;\n+\tif (close(logfd) != 0 || written != len) {\n+\t\terror(\"Unable to append to %s\", log_file);\n+\t\tresult = -1;\n+\t}\n+done:\n+\tstrbuf_release(&sb_log_file);\n+\treturn result;\n }\n \n static int is_branch(const char *refname)\ndiff --git a/refs.h b/refs.h\nindex 4e3050d..99d88a7 100644\n--- a/refs.h\n+++ b/refs.h\n@@ -157,7 +157,7 @@ extern void unlock_ref(struct ref_lock *lock);\n extern int write_ref_sha1(struct ref_lock *lock, const unsigned char *sha1, const char *msg);\n \n /** Setup reflog before using. **/\n-int log_ref_setup(const char *refname, char *logfile, int bufsize);\n+int log_ref_setup(const char *refname, struct strbuf *logfile);\n \n /** Reads log for the value of ref during at_time. **/\n extern int read_ref_at(const char *refname, unsigned long at_time, int cnt,\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249164","messageId":"1410388928-32265-5-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1410388928-32265-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v2 04/32] path.c: rename vsnpath() to do_git_path()","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-10T22:41:40Z","receivedAt":"2014-09-10T22:41:40Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"The name vsnpath() gives an impression that this is general path\nhandling function. It's not. This is the underlying implementation of\ngit_path(), git_pathdup() and strbuf_git_path() which will prefix\n$GIT_DIR in the result string.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n path.c | 8 ++++----\n 1 file changed, 4 insertions(+), 4 deletions(-)\n\ndiff --git a/path.c b/path.c\nindex e545064..2cb2e61 100644\n--- a/path.c\n+++ b/path.c\n@@ -60,7 +60,7 @@ char *mksnpath(char *buf, size_t n, const char *fmt, ...)\n \treturn cleanup_path(buf);\n }\n \n-static void vsnpath(struct strbuf *buf, const char *fmt, va_list args)\n+static void do_git_path(struct strbuf *buf, const char *fmt, va_list args)\n {\n \tconst char *git_dir = get_git_dir();\n \tstrbuf_addstr(buf, git_dir);\n@@ -74,7 +74,7 @@ void strbuf_git_path(struct strbuf *sb, const char *fmt, ...)\n {\n \tva_list args;\n \tva_start(args, fmt);\n-\tvsnpath(sb, fmt, args);\n+\tdo_git_path(sb, fmt, args);\n \tva_end(args);\n }\n \n@@ -83,7 +83,7 @@ char *git_pathdup(const char *fmt, ...)\n \tstruct strbuf path = STRBUF_INIT;\n \tva_list args;\n \tva_start(args, fmt);\n-\tvsnpath(&path, fmt, args);\n+\tdo_git_path(&path, fmt, args);\n \tva_end(args);\n \treturn strbuf_detach(&path, NULL);\n }\n@@ -114,7 +114,7 @@ const char *git_path(const char *fmt, ...)\n \tstruct strbuf *pathname = get_pathname();\n \tva_list args;\n \tva_start(args, fmt);\n-\tvsnpath(pathname, fmt, args);\n+\tdo_git_path(pathname, fmt, args);\n \tva_end(args);\n \treturn pathname->buf;\n }\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249165","messageId":"1410388928-32265-6-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1410388928-32265-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v2 05/32] path.c: group git_path(), git_pathdup() and strbuf_git_path() together","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-10T22:41:41Z","receivedAt":"2014-09-10T22:41:41Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n path.c | 20 ++++++++++----------\n 1 file changed, 10 insertions(+), 10 deletions(-)\n\ndiff --git a/path.c b/path.c\nindex 2cb2e61..65881aa 100644\n--- a/path.c\n+++ b/path.c\n@@ -78,6 +78,16 @@ void strbuf_git_path(struct strbuf *sb, const char *fmt, ...)\n \tva_end(args);\n }\n \n+const char *git_path(const char *fmt, ...)\n+{\n+\tstruct strbuf *pathname = get_pathname();\n+\tva_list args;\n+\tva_start(args, fmt);\n+\tdo_git_path(pathname, fmt, args);\n+\tva_end(args);\n+\treturn pathname->buf;\n+}\n+\n char *git_pathdup(const char *fmt, ...)\n {\n \tstruct strbuf path = STRBUF_INIT;\n@@ -109,16 +119,6 @@ const char *mkpath(const char *fmt, ...)\n \treturn cleanup_path(pathname->buf);\n }\n \n-const char *git_path(const char *fmt, ...)\n-{\n-\tstruct strbuf *pathname = get_pathname();\n-\tva_list args;\n-\tva_start(args, fmt);\n-\tdo_git_path(pathname, fmt, args);\n-\tva_end(args);\n-\treturn pathname->buf;\n-}\n-\n void home_config_paths(char **global, char **xdg, char *file)\n {\n \tchar *xdg_home = getenv(\"XDG_CONFIG_HOME\");\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249167","messageId":"1410388928-32265-7-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1410388928-32265-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v2 06/32] git_path(): be aware of file relocation in $GIT_DIR","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-10T22:41:42Z","receivedAt":"2014-09-10T22:41:42Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"We allow the user to relocate certain paths out of $GIT_DIR via\nenvironment variables, e.g. GIT_OBJECT_DIRECTORY, GIT_INDEX_FILE and\nGIT_GRAFT_FILE. Callers are not supposed to use git_path() or\ngit_pathdup() to get those paths. Instead they must use\nget_object_directory(), get_index_file() and get_graft_file()\nrespectively. This is inconvenient and could be missed in review (for\nexample, there's git_path(\"objects/info/alternates\") somewhere in\nsha1_file.c).\n\nThis patch makes git_path() and git_pathdup() understand those\nenvironment variables. So if you set GIT_OBJECT_DIRECTORY to /foo/bar,\ngit_path(\"objects/abc\") should return /foo/bar/abc. The same is done\nfor the two remaining env variables.\n\n\"git rev-parse --git-path\" is the wrapper for script use.\n\nThis patch kinda reverts a0279e1 (setup_git_env: use git_pathdup\ninstead of xmalloc + sprintf - 2014-06-19) because using git_pathdup\nhere would result in infinite recursion:\n\n  setup_git_env() -> git_pathdup(\"objects\") -> .. -> adjust_git_path()\n  -> get_object_directory() -> oops, git_object_directory is NOT set\n  yet -> setup_git_env()\n\nI wanted to make git_pathdup_literal() that skips adjust_git_path().\nBut that won't work because later on when $GIT_COMMON_DIR is\nintroduced, git_pathdup_literal(\"objects\") needs adjust_git_path() to\nreplace $GIT_DIR with $GIT_COMMON_DIR.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n Documentation/git-rev-parse.txt |  7 ++++++\n builtin/rev-parse.c             |  7 ++++++\n cache.h                         |  1 +\n environment.c                   | 19 +++++++++++-----\n path.c                          | 49 +++++++++++++++++++++++++++++++++++++++--\n t/t0060-path-utils.sh           | 19 ++++++++++++++++\n 6 files changed, 95 insertions(+), 7 deletions(-)\n\ndiff --git a/Documentation/git-rev-parse.txt b/Documentation/git-rev-parse.txt\nindex 987395d..9465399 100644\n--- a/Documentation/git-rev-parse.txt\n+++ b/Documentation/git-rev-parse.txt\n@@ -232,6 +232,13 @@ print a message to stderr and exit with nonzero status.\n \trepository.  If <path> is a gitfile then the resolved path\n \tto the real repository is printed.\n \n+--git-path <path>::\n+\tResolve \"$GIT_DIR/<path>\" and takes other path relocation\n+\tvariables such as $GIT_OBJECT_DIRECTORY,\n+\t$GIT_INDEX_FILE... into account. For example, if\n+\t$GIT_OBJECT_DIRECTORY is set to /foo/bar then \"git rev-parse\n+\t--git-path objects/abc\" returns /foo/bar/abc.\n+\n --show-cdup::\n \tWhen the command is invoked from a subdirectory, show the\n \tpath of the top-level directory relative to the current\ndiff --git a/builtin/rev-parse.c b/builtin/rev-parse.c\nindex 1a6122d..7606d43 100644\n--- a/builtin/rev-parse.c\n+++ b/builtin/rev-parse.c\n@@ -529,6 +529,13 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)\n \tfor (i = 1; i < argc; i++) {\n \t\tconst char *arg = argv[i];\n \n+\t\tif (!strcmp(arg, \"--git-path\")) {\n+\t\t\tif (!argv[i + 1])\n+\t\t\t\tdie(\"--git-path requires an argument\");\n+\t\t\tputs(git_path(\"%s\", argv[i + 1]));\n+\t\t\ti++;\n+\t\t\tcontinue;\n+\t\t}\n \t\tif (as_is) {\n \t\t\tif (show_file(arg, output_prefix) && as_is < 2)\n \t\t\t\tverify_filename(prefix, arg, 0);\ndiff --git a/cache.h b/cache.h\nindex 279b581..0128b9a 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -612,6 +612,7 @@ extern int fsync_object_files;\n extern int core_preload_index;\n extern int core_apply_sparse_checkout;\n extern int precomposed_unicode;\n+extern int git_db_env, git_index_env, git_graft_env;\n \n /*\n  * The character that begins a commented line in user-editable file\ndiff --git a/environment.c b/environment.c\nindex 565f652..fee12a6 100644\n--- a/environment.c\n+++ b/environment.c\n@@ -83,6 +83,7 @@ static size_t namespace_len;\n \n static const char *git_dir;\n static char *git_object_dir, *git_index_file, *git_graft_file;\n+int git_db_env, git_index_env, git_graft_env;\n \n /*\n  * Repository-local GIT_* environment variables; see cache.h for details.\n@@ -124,10 +125,18 @@ static char *expand_namespace(const char *raw_namespace)\n \treturn strbuf_detach(&buf, NULL);\n }\n \n-static char *git_path_from_env(const char *envvar, const char *path)\n+static char *git_path_from_env(const char *envvar, const char *path,\n+\t\t\t       int *fromenv)\n {\n \tconst char *value = getenv(envvar);\n-\treturn value ? xstrdup(value) : git_pathdup(\"%s\", path);\n+\tif (!value) {\n+\t\tchar *buf = xmalloc(strlen(git_dir) + strlen(path) + 2);\n+\t\tsprintf(buf, \"%s/%s\", git_dir, path);\n+\t\treturn buf;\n+\t}\n+\tif (fromenv)\n+\t\t*fromenv = 1;\n+\treturn xstrdup(value);\n }\n \n static void setup_git_env(void)\n@@ -140,9 +149,9 @@ static void setup_git_env(void)\n \t\tgit_dir = DEFAULT_GIT_DIR_ENVIRONMENT;\n \tgitfile = read_gitfile(git_dir);\n \tgit_dir = xstrdup(gitfile ? gitfile : git_dir);\n-\tgit_object_dir = git_path_from_env(DB_ENVIRONMENT, \"objects\");\n-\tgit_index_file = git_path_from_env(INDEX_ENVIRONMENT, \"index\");\n-\tgit_graft_file = git_path_from_env(GRAFT_ENVIRONMENT, \"info/grafts\");\n+\tgit_object_dir = git_path_from_env(DB_ENVIRONMENT, \"objects\", &git_db_env);\n+\tgit_index_file = git_path_from_env(INDEX_ENVIRONMENT, \"index\", &git_index_env);\n+\tgit_graft_file = git_path_from_env(GRAFT_ENVIRONMENT, \"info/grafts\", &git_graft_env);\n \tif (getenv(NO_REPLACE_OBJECTS_ENVIRONMENT))\n \t\tcheck_replace_refs = 0;\n \tnamespace = expand_namespace(getenv(GIT_NAMESPACE_ENVIRONMENT));\ndiff --git a/path.c b/path.c\nindex 65881aa..3deb80c 100644\n--- a/path.c\n+++ b/path.c\n@@ -60,13 +60,58 @@ char *mksnpath(char *buf, size_t n, const char *fmt, ...)\n \treturn cleanup_path(buf);\n }\n \n+static int dir_prefix(const char *buf, const char *dir)\n+{\n+\tint len = strlen(dir);\n+\treturn !strncmp(buf, dir, len) &&\n+\t\t(is_dir_sep(buf[len]) || buf[len] == '\\0');\n+}\n+\n+/* $buf =~ m|$dir/+$file| but without regex */\n+static int is_dir_file(const char *buf, const char *dir, const char *file)\n+{\n+\tint len = strlen(dir);\n+\tif (strncmp(buf, dir, len) || !is_dir_sep(buf[len]))\n+\t\treturn 0;\n+\twhile (is_dir_sep(buf[len]))\n+\t\tlen++;\n+\treturn !strcmp(buf + len, file);\n+}\n+\n+static void replace_dir(struct strbuf *buf, int len, const char *newdir)\n+{\n+\tint newlen = strlen(newdir);\n+\tint need_sep = (buf->buf[len] && !is_dir_sep(buf->buf[len])) &&\n+\t\t!is_dir_sep(newdir[newlen - 1]);\n+\tif (need_sep)\n+\t\tlen--;\t /* keep one char, to be replaced with '/'  */\n+\tstrbuf_splice(buf, 0, len, newdir, newlen);\n+\tif (need_sep)\n+\t\tbuf->buf[newlen] = '/';\n+}\n+\n+static void adjust_git_path(struct strbuf *buf, int git_dir_len)\n+{\n+\tconst char *base = buf->buf + git_dir_len;\n+\tif (git_graft_env && is_dir_file(base, \"info\", \"grafts\"))\n+\t\tstrbuf_splice(buf, 0, buf->len,\n+\t\t\t      get_graft_file(), strlen(get_graft_file()));\n+\telse if (git_index_env && !strcmp(base, \"index\"))\n+\t\tstrbuf_splice(buf, 0, buf->len,\n+\t\t\t      get_index_file(), strlen(get_index_file()));\n+\telse if (git_db_env && dir_prefix(base, \"objects\"))\n+\t\treplace_dir(buf, git_dir_len + 7, get_object_directory());\n+}\n+\n static void do_git_path(struct strbuf *buf, const char *fmt, va_list args)\n {\n-\tconst char *git_dir = get_git_dir();\n-\tstrbuf_addstr(buf, git_dir);\n+\tint gitdir_len;\n+\tstrbuf_addstr(buf, get_git_dir());\n \tif (buf->len && !is_dir_sep(buf->buf[buf->len - 1]))\n \t\tstrbuf_addch(buf, '/');\n+\tgitdir_len = buf->len;\n \tstrbuf_vaddf(buf, fmt, args);\n+\tadjust_git_path(buf, gitdir_len);\n \tstrbuf_cleanup_path(buf);\n }\n \ndiff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh\nindex c0143a0..33d2818 100755\n--- a/t/t0060-path-utils.sh\n+++ b/t/t0060-path-utils.sh\n@@ -19,6 +19,14 @@ relative_path() {\n \t\"test \\\"\\$(test-path-utils relative_path '$1' '$2')\\\" = '$expected'\"\n }\n \n+test_git_path() {\n+\ttest_expect_success \"git-path $1 $2 => $3\" \"\n+\t\t$1 git rev-parse --git-path $2 >actual &&\n+\t\techo $3 >expect &&\n+\t\ttest_cmp expect actual\n+\t\"\n+}\n+\n # On Windows, we are using MSYS's bash, which mangles the paths.\n # Absolute paths are anchored at the MSYS installation directory,\n # which means that the path / accounts for this many characters:\n@@ -244,4 +252,15 @@ relative_path \"<null>\"\t\t\"<empty>\"\t./\n relative_path \"<null>\"\t\t\"<null>\"\t./\n relative_path \"<null>\"\t\t/foo/a/b\t./\n \n+test_git_path A=B                info/grafts .git/info/grafts\n+test_git_path GIT_GRAFT_FILE=foo info/grafts foo\n+test_git_path GIT_GRAFT_FILE=foo info/////grafts foo\n+test_git_path GIT_INDEX_FILE=foo index foo\n+test_git_path GIT_INDEX_FILE=foo index/foo .git/index/foo\n+test_git_path GIT_INDEX_FILE=foo index2 .git/index2\n+test_expect_success 'setup fake objects directory foo' 'mkdir foo'\n+test_git_path GIT_OBJECT_DIRECTORY=foo objects foo\n+test_git_path GIT_OBJECT_DIRECTORY=foo objects/foo foo/foo\n+test_git_path GIT_OBJECT_DIRECTORY=foo objects2 .git/objects2\n+\n test_done\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249166","messageId":"1410388928-32265-8-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1410388928-32265-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v2 07/32] *.sh: respect $GIT_INDEX_FILE","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-10T22:41:43Z","receivedAt":"2014-09-10T22:41:43Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n git-pull.sh  | 2 +-\n git-stash.sh | 2 +-\n 2 files changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/git-pull.sh b/git-pull.sh\nindex 18a394f..6ab0c31 100755\n--- a/git-pull.sh\n+++ b/git-pull.sh\n@@ -240,7 +240,7 @@ test true = \"$rebase\" && {\n \tif ! git rev-parse -q --verify HEAD >/dev/null\n \tthen\n \t\t# On an unborn branch\n-\t\tif test -f \"$GIT_DIR/index\"\n+\t\tif test -f \"$(git rev-parse --git-path index)\"\n \t\tthen\n \t\t\tdie \"$(gettext \"updating an unborn branch with changes added to the index\")\"\n \t\tfi\ndiff --git a/git-stash.sh b/git-stash.sh\nindex bcc757b..393e1ec 100755\n--- a/git-stash.sh\n+++ b/git-stash.sh\n@@ -20,7 +20,7 @@ require_work_tree\n cd_to_toplevel\n \n TMP=\"$GIT_DIR/.git-stash.$$\"\n-TMPindex=${GIT_INDEX_FILE-\"$GIT_DIR/index\"}.stash.$$\n+TMPindex=${GIT_INDEX_FILE-\"$(git rev-parse --git-path index)\"}.stash.$$\n trap 'rm -f \"$TMP-\"* \"$TMPindex\"' 0\n \n ref_stash=refs/stash\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249168","messageId":"1410388928-32265-9-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1410388928-32265-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v2 08/32] reflog: avoid constructing .lock path with git_path","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-10T22:41:44Z","receivedAt":"2014-09-10T22:41:44Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Among pathnames in $GIT_DIR, e.g. \"index\" or \"packed-refs\", we want to\nautomatically and silently map some of them to the $GIT_DIR of the\nrepository we are borrowing from via $GIT_COMMON_DIR mechanism.  When\nwe formulate the pathname for its lockfile, we want it to be in the\nsame location as its final destination.  \"index\" is not shared and\nneeds to remain in the borrowing repository, while \"packed-refs\" is\nshared and needs to go to the borrowed repository.\n\ngit_path() could be taught about the \".lock\" suffix and map\n\"index.lock\" and \"packed-refs.lock\" the same way their basenames are\nmapped, but instead the caller can help by asking where the basename\n(e.g. \"index\") is mapped to git_path() and then appending \".lock\"\nafter the mapping is done.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n builtin/reflog.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/builtin/reflog.c b/builtin/reflog.c\nindex e8a8fb1..9bd874d 100644\n--- a/builtin/reflog.c\n+++ b/builtin/reflog.c\n@@ -372,7 +372,7 @@ static int expire_reflog(const char *ref, const unsigned char *sha1, int unused,\n \tif (!reflog_exists(ref))\n \t\tgoto finish;\n \tif (!cmd->dry_run) {\n-\t\tnewlog_path = git_pathdup(\"logs/%s.lock\", ref);\n+\t\tnewlog_path = mkpathdup(\"%s.lock\", log_file);\n \t\tcb.newlog = fopen(newlog_path, \"w\");\n \t}\n \n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249169","messageId":"1410388928-32265-10-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1410388928-32265-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v2 09/32] fast-import: use git_path() for accessing .git dir instead of get_git_dir()","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-10T22:41:45Z","receivedAt":"2014-09-10T22:41:45Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"This allows git_path() to redirect info/fast-import to another place\nif needed\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n fast-import.c | 5 +----\n 1 file changed, 1 insertion(+), 4 deletions(-)\n\ndiff --git a/fast-import.c b/fast-import.c\nindex d9c068b..ea426c4 100644\n--- a/fast-import.c\n+++ b/fast-import.c\n@@ -3103,12 +3103,9 @@ static void parse_progress(void)\n \n static char* make_fast_import_path(const char *path)\n {\n-\tstruct strbuf abs_path = STRBUF_INIT;\n-\n \tif (!relative_marks_paths || is_absolute_path(path))\n \t\treturn xstrdup(path);\n-\tstrbuf_addf(&abs_path, \"%s/info/fast-import/%s\", get_git_dir(), path);\n-\treturn strbuf_detach(&abs_path, NULL);\n+\treturn xstrdup(git_path(\"info/fast-import/%s\", path));\n }\n \n static void option_import_marks(const char *marks,\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249170","messageId":"1410388928-32265-11-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1410388928-32265-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v2 10/32] commit: use SEQ_DIR instead of hardcoding \"sequencer\"","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-10T22:41:46Z","receivedAt":"2014-09-10T22:41:46Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n builtin/commit.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 461c3b1..4b9f012 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -156,7 +156,7 @@ static void determine_whence(struct wt_status *s)\n \t\twhence = FROM_MERGE;\n \telse if (file_exists(git_path(\"CHERRY_PICK_HEAD\"))) {\n \t\twhence = FROM_CHERRY_PICK;\n-\t\tif (file_exists(git_path(\"sequencer\")))\n+\t\tif (file_exists(git_path(SEQ_DIR)))\n \t\t\tsequencer_in_use = 1;\n \t}\n \telse\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249171","messageId":"1410388928-32265-12-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1410388928-32265-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v2 11/32] $GIT_COMMON_DIR: a new environment variable","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-10T22:41:47Z","receivedAt":"2014-09-10T22:41:47Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"This variable is intended to support multiple working directories\nattached to a repository. Such a repository may have a main working\ndirectory, created by either \"git init\" or \"git clone\" and one or more\nlinked working directories. These working directories and the main\nrepository share the same repository directory.\n\nIn linked working directories, $GIT_COMMON_DIR must be defined to point\nto the real repository directory and $GIT_DIR points to an unused\nsubdirectory inside $GIT_COMMON_DIR. File locations inside the\nrepository are reorganized from the linked worktree view point:\n\n - worktree-specific such as HEAD, logs/HEAD, index, other top-level\n   refs and unrecognized files are from $GIT_DIR.\n\n - the rest like objects, refs, info, hooks, packed-refs, shallow...\n   are from $GIT_COMMON_DIR (except info/sparse-checkout, but that's\n   a separate patch)\n\nScripts are supposed to retrieve paths in $GIT_DIR with \"git rev-parse\n--git-path\", which will take care of \"$GIT_DIR vs $GIT_COMMON_DIR\"\nbusiness.\n\nThe redirection is done by git_path(), git_pathdup() and\nstrbuf_git_path(). The selected list of paths goes to $GIT_COMMON_DIR,\nnot the other way around in case a developer adds a new\nworktree-specific file and it's accidentally promoted to be shared\nacross repositories (this includes unknown files added by third party\ncommands)\n\nThe list of known files that belong to $GIT_DIR are:\n\nADD_EDIT.patch BISECT_ANCESTORS_OK BISECT_EXPECTED_REV BISECT_LOG\nBISECT_NAMES CHERRY_PICK_HEAD COMMIT_MSG FETCH_HEAD HEAD MERGE_HEAD\nMERGE_MODE MERGE_RR NOTES_EDITMSG NOTES_MERGE_WORKTREE ORIG_HEAD\nREVERT_HEAD SQUASH_MSG TAG_EDITMSG fast_import_crash_* logs/HEAD\nnext-index-* rebase-apply rebase-merge rsync-refs-* sequencer/*\nshallow_*\n\nPath mapping is NOT done for git_path_submodule(). Multi-checkouts are\nnot supported as submodules.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n Documentation/git.txt                  |  8 ++++++\n Documentation/gitrepository-layout.txt | 45 +++++++++++++++++++++++++---------\n cache.h                                |  4 ++-\n environment.c                          | 28 +++++++++++++++------\n path.c                                 | 34 +++++++++++++++++++++++++\n t/t0060-path-utils.sh                  | 16 ++++++++++++\n 6 files changed, 116 insertions(+), 19 deletions(-)\n\ndiff --git a/Documentation/git.txt b/Documentation/git.txt\nindex 7924209..749052f 100644\n--- a/Documentation/git.txt\n+++ b/Documentation/git.txt\n@@ -788,6 +788,14 @@ Git so take care if using Cogito etc.\n \tan explicit repository directory set via 'GIT_DIR' or on the\n \tcommand line.\n \n+'GIT_COMMON_DIR'::\n+\tIf this variable is set to a path, non-worktree files that are\n+\tnormally in $GIT_DIR will be taken from this path\n+\tinstead. Worktree-specific files such as HEAD or index are\n+\ttaken from $GIT_DIR. See linkgit:gitrepository-layout[5] for\n+\tdetails. This variable has lower precedence than other path\n+\tvariables such as GIT_INDEX_FILE, GIT_OBJECT_DIRECTORY...\n+\n Git Commits\n ~~~~~~~~~~~\n 'GIT_AUTHOR_NAME'::\ndiff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt\nindex 17d2ea6..b7b9dfb 100644\n--- a/Documentation/gitrepository-layout.txt\n+++ b/Documentation/gitrepository-layout.txt\n@@ -46,6 +46,9 @@ of incomplete object store is not suitable to be published for\n use with dumb transports but otherwise is OK as long as\n `objects/info/alternates` points at the object stores it\n borrows from.\n++\n+This directory is ignored if $GIT_COMMON_DIR is set and\n+\"$GIT_COMMON_DIR/objects\" will be used instead.\n \n objects/[0-9a-f][0-9a-f]::\n \tA newly created object is stored in its own file.\n@@ -92,7 +95,8 @@ refs::\n \tReferences are stored in subdirectories of this\n \tdirectory.  The 'git prune' command knows to preserve\n \tobjects reachable from refs found in this directory and\n-\tits subdirectories.\n+\tits subdirectories. This directory is ignored if $GIT_COMMON_DIR\n+\tis set and \"$GIT_COMMON_DIR/refs\" will be used instead.\n \n refs/heads/`name`::\n \trecords tip-of-the-tree commit objects of branch `name`\n@@ -114,7 +118,8 @@ refs/replace/`<obj-sha1>`::\n packed-refs::\n \trecords the same information as refs/heads/, refs/tags/,\n \tand friends record in a more efficient way.  See\n-\tlinkgit:git-pack-refs[1].\n+\tlinkgit:git-pack-refs[1]. This file is ignored if $GIT_COMMON_DIR\n+\tis set and \"$GIT_COMMON_DIR/packed-refs\" will be used instead.\n \n HEAD::\n \tA symref (see glossary) to the `refs/heads/` namespace\n@@ -133,6 +138,11 @@ being a symref to point at the current branch.  Such a state\n is often called 'detached HEAD.'  See linkgit:git-checkout[1]\n for details.\n \n+config::\n+\tRepository specific configuration file. This file is ignored\n+\tif $GIT_COMMON_DIR is set and \"$GIT_COMMON_DIR/config\" will be\n+\tused instead.\n+\n branches::\n \tA slightly deprecated way to store shorthands to be used\n \tto specify a URL to 'git fetch', 'git pull' and 'git push'.\n@@ -140,7 +150,10 @@ branches::\n \t'name' can be given to these commands in place of\n \t'repository' argument.  See the REMOTES section in\n \tlinkgit:git-fetch[1] for details.  This mechanism is legacy\n-\tand not likely to be found in modern repositories.\n+\tand not likely to be found in modern repositories. This\n+\tdirectory is ignored if $GIT_COMMON_DIR is set and\n+\t\"$GIT_COMMON_DIR/branches\" will be used instead.\n+\n \n hooks::\n \tHooks are customization scripts used by various Git\n@@ -149,7 +162,9 @@ hooks::\n \tdefault.  To enable, the `.sample` suffix has to be\n \tremoved from the filename by renaming.\n \tRead linkgit:githooks[5] for more details about\n-\teach hook.\n+\teach hook. This directory is ignored if $GIT_COMMON_DIR is set\n+\tand \"$GIT_COMMON_DIR/hooks\" will be used instead.\n+\n \n index::\n \tThe current index file for the repository.  It is\n@@ -157,7 +172,8 @@ index::\n \n info::\n \tAdditional information about the repository is recorded\n-\tin this directory.\n+\tin this directory. This directory is ignored if $GIT_COMMON_DIR\n+\tis set and \"$GIT_COMMON_DIR/index\" will be used instead.\n \n info/refs::\n \tThis file helps dumb transports discover what refs are\n@@ -197,12 +213,15 @@ remotes::\n \twhen interacting with remote repositories via 'git fetch',\n \t'git pull' and 'git push' commands.  See the REMOTES section\n \tin linkgit:git-fetch[1] for details.  This mechanism is legacy\n-\tand not likely to be found in modern repositories.\n+\tand not likely to be found in modern repositories. This\n+\tdirectory is ignored if $GIT_COMMON_DIR is set and\n+\t\"$GIT_COMMON_DIR/remotes\" will be used instead.\n \n logs::\n-\tRecords of changes made to refs are stored in this\n-\tdirectory.  See linkgit:git-update-ref[1]\n-\tfor more information.\n+\tRecords of changes made to refs are stored in this directory.\n+\tSee linkgit:git-update-ref[1] for more information. This\n+\tdirectory is ignored if $GIT_COMMON_DIR is set and\n+\t\"$GIT_COMMON_DIR/logs\" will be used instead.\n \n logs/refs/heads/`name`::\n \tRecords all changes made to the branch tip named `name`.\n@@ -213,10 +232,14 @@ logs/refs/tags/`name`::\n shallow::\n \tThis is similar to `info/grafts` but is internally used\n \tand maintained by shallow clone mechanism.  See `--depth`\n-\toption to linkgit:git-clone[1] and linkgit:git-fetch[1].\n+\toption to linkgit:git-clone[1] and linkgit:git-fetch[1]. This\n+\tfile is ignored if $GIT_COMMON_DIR is set and\n+\t\"$GIT_COMMON_DIR/shallow\" will be used instead.\n \n modules::\n-\tContains the git-repositories of the submodules.\n+\tContains the git-repositories of the submodules. This\n+\tdirectory is ignored if $GIT_COMMON_DIR is set and\n+\t\"$GIT_COMMON_DIR/modules\" will be used instead.\n \n SEE ALSO\n --------\ndiff --git a/cache.h b/cache.h\nindex 0128b9a..b606ee4 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -362,6 +362,7 @@ static inline enum object_type object_type(unsigned int mode)\n \n /* Double-check local_repo_env below if you add to this list. */\n #define GIT_DIR_ENVIRONMENT \"GIT_DIR\"\n+#define GIT_COMMON_DIR_ENVIRONMENT \"GIT_COMMON_DIR\"\n #define GIT_NAMESPACE_ENVIRONMENT \"GIT_NAMESPACE\"\n #define GIT_WORK_TREE_ENVIRONMENT \"GIT_WORK_TREE\"\n #define GIT_PREFIX_ENVIRONMENT \"GIT_PREFIX\"\n@@ -415,6 +416,7 @@ extern int is_inside_git_dir(void);\n extern char *git_work_tree_cfg;\n extern int is_inside_work_tree(void);\n extern const char *get_git_dir(void);\n+extern const char *get_git_common_dir(void);\n extern int is_git_directory(const char *path);\n extern char *get_object_directory(void);\n extern char *get_index_file(void);\n@@ -612,7 +614,7 @@ extern int fsync_object_files;\n extern int core_preload_index;\n extern int core_apply_sparse_checkout;\n extern int precomposed_unicode;\n-extern int git_db_env, git_index_env, git_graft_env;\n+extern int git_db_env, git_index_env, git_graft_env, git_common_dir_env;\n \n /*\n  * The character that begins a commented line in user-editable file\ndiff --git a/environment.c b/environment.c\nindex fee12a6..78a07e4 100644\n--- a/environment.c\n+++ b/environment.c\n@@ -81,9 +81,9 @@ static char *work_tree;\n static const char *namespace;\n static size_t namespace_len;\n \n-static const char *git_dir;\n+static const char *git_dir, *git_common_dir;\n static char *git_object_dir, *git_index_file, *git_graft_file;\n-int git_db_env, git_index_env, git_graft_env;\n+int git_db_env, git_index_env, git_graft_env, git_common_dir_env;\n \n /*\n  * Repository-local GIT_* environment variables; see cache.h for details.\n@@ -125,8 +125,8 @@ static char *expand_namespace(const char *raw_namespace)\n \treturn strbuf_detach(&buf, NULL);\n }\n \n-static char *git_path_from_env(const char *envvar, const char *path,\n-\t\t\t       int *fromenv)\n+static char *git_path_from_env(const char *envvar, const char *git_dir,\n+\t\t\t       const char *path, int *fromenv)\n {\n \tconst char *value = getenv(envvar);\n \tif (!value) {\n@@ -149,9 +149,18 @@ static void setup_git_env(void)\n \t\tgit_dir = DEFAULT_GIT_DIR_ENVIRONMENT;\n \tgitfile = read_gitfile(git_dir);\n \tgit_dir = xstrdup(gitfile ? gitfile : git_dir);\n-\tgit_object_dir = git_path_from_env(DB_ENVIRONMENT, \"objects\", &git_db_env);\n-\tgit_index_file = git_path_from_env(INDEX_ENVIRONMENT, \"index\", &git_index_env);\n-\tgit_graft_file = git_path_from_env(GRAFT_ENVIRONMENT, \"info/grafts\", &git_graft_env);\n+\tgit_common_dir = getenv(GIT_COMMON_DIR_ENVIRONMENT);\n+\tif (git_common_dir) {\n+\t\tgit_common_dir_env = 1;\n+\t\tgit_common_dir = xstrdup(git_common_dir);\n+\t} else\n+\t\tgit_common_dir = git_dir;\n+\tgit_object_dir = git_path_from_env(DB_ENVIRONMENT, git_common_dir,\n+\t\t\t\t\t   \"objects\", &git_db_env);\n+\tgit_index_file = git_path_from_env(INDEX_ENVIRONMENT, git_dir,\n+\t\t\t\t\t   \"index\", &git_index_env);\n+\tgit_graft_file = git_path_from_env(GRAFT_ENVIRONMENT, git_common_dir,\n+\t\t\t\t\t   \"info/grafts\", &git_graft_env);\n \tif (getenv(NO_REPLACE_OBJECTS_ENVIRONMENT))\n \t\tcheck_replace_refs = 0;\n \tnamespace = expand_namespace(getenv(GIT_NAMESPACE_ENVIRONMENT));\n@@ -174,6 +183,11 @@ const char *get_git_dir(void)\n \treturn git_dir;\n }\n \n+const char *get_git_common_dir(void)\n+{\n+\treturn git_common_dir;\n+}\n+\n const char *get_git_namespace(void)\n {\n \tif (!namespace)\ndiff --git a/path.c b/path.c\nindex 3deb80c..8a6586c 100644\n--- a/path.c\n+++ b/path.c\n@@ -90,6 +90,38 @@ static void replace_dir(struct strbuf *buf, int len, const char *newdir)\n \t\tbuf->buf[newlen] = '/';\n }\n \n+static const char *common_list[] = {\n+\t\"/branches\", \"/hooks\", \"/info\", \"/logs\", \"/lost-found\", \"/modules\",\n+\t\"/objects\", \"/refs\", \"/remotes\", \"/rr-cache\", \"/svn\",\n+\t\"config\", \"gc.pid\", \"packed-refs\", \"shallow\",\n+\tNULL\n+};\n+\n+static void update_common_dir(struct strbuf *buf, int git_dir_len)\n+{\n+\tchar *base = buf->buf + git_dir_len;\n+\tconst char **p;\n+\n+\tif (is_dir_file(base, \"logs\", \"HEAD\"))\n+\t\treturn;\t/* keep this in $GIT_DIR */\n+\tfor (p = common_list; *p; p++) {\n+\t\tconst char *path = *p;\n+\t\tint is_dir = 0;\n+\t\tif (*path == '/') {\n+\t\t\tpath++;\n+\t\t\tis_dir = 1;\n+\t\t}\n+\t\tif (is_dir && dir_prefix(base, path)) {\n+\t\t\treplace_dir(buf, git_dir_len, get_git_common_dir());\n+\t\t\treturn;\n+\t\t}\n+\t\tif (!is_dir && !strcmp(base, path)) {\n+\t\t\treplace_dir(buf, git_dir_len, get_git_common_dir());\n+\t\t\treturn;\n+\t\t}\n+\t}\n+}\n+\n static void adjust_git_path(struct strbuf *buf, int git_dir_len)\n {\n \tconst char *base = buf->buf + git_dir_len;\n@@ -101,6 +133,8 @@ static void adjust_git_path(struct strbuf *buf, int git_dir_len)\n \t\t\t      get_index_file(), strlen(get_index_file()));\n \telse if (git_db_env && dir_prefix(base, \"objects\"))\n \t\treplace_dir(buf, git_dir_len + 7, get_object_directory());\n+\telse if (git_common_dir_env)\n+\t\tupdate_common_dir(buf, git_dir_len);\n }\n \n static void do_git_path(struct strbuf *buf, const char *fmt, va_list args)\ndiff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh\nindex 33d2818..f5d6f80 100755\n--- a/t/t0060-path-utils.sh\n+++ b/t/t0060-path-utils.sh\n@@ -262,5 +262,21 @@ test_expect_success 'setup fake objects directory foo' 'mkdir foo'\n test_git_path GIT_OBJECT_DIRECTORY=foo objects foo\n test_git_path GIT_OBJECT_DIRECTORY=foo objects/foo foo/foo\n test_git_path GIT_OBJECT_DIRECTORY=foo objects2 .git/objects2\n+test_expect_success 'setup common repository' 'git --git-dir=bar init'\n+test_git_path GIT_COMMON_DIR=bar index                    .git/index\n+test_git_path GIT_COMMON_DIR=bar HEAD                     .git/HEAD\n+test_git_path GIT_COMMON_DIR=bar logs/HEAD                .git/logs/HEAD\n+test_git_path GIT_COMMON_DIR=bar objects                  bar/objects\n+test_git_path GIT_COMMON_DIR=bar objects/bar              bar/objects/bar\n+test_git_path GIT_COMMON_DIR=bar info/exclude             bar/info/exclude\n+test_git_path GIT_COMMON_DIR=bar info/grafts              bar/info/grafts\n+test_git_path GIT_COMMON_DIR=bar remotes/bar              bar/remotes/bar\n+test_git_path GIT_COMMON_DIR=bar branches/bar             bar/branches/bar\n+test_git_path GIT_COMMON_DIR=bar logs/refs/heads/master   bar/logs/refs/heads/master\n+test_git_path GIT_COMMON_DIR=bar refs/heads/master        bar/refs/heads/master\n+test_git_path GIT_COMMON_DIR=bar hooks/me                 bar/hooks/me\n+test_git_path GIT_COMMON_DIR=bar config                   bar/config\n+test_git_path GIT_COMMON_DIR=bar packed-refs              bar/packed-refs\n+test_git_path GIT_COMMON_DIR=bar shallow                  bar/shallow\n \n test_done\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249172","messageId":"1410388928-32265-13-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1410388928-32265-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v2 12/32] git-sh-setup.sh: use rev-parse --git-path to get $GIT_DIR/objects","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-10T22:41:48Z","receivedAt":"2014-09-10T22:41:48Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"If $GIT_COMMON_DIR is set, $GIT_OBJECT_DIRECTORY should be\n$GIT_COMMON_DIR/objects, not $GIT_DIR/objects. Just let rev-parse\n--git-path handle it.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n git-sh-setup.sh | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/git-sh-setup.sh b/git-sh-setup.sh\nindex 9447980..d3dbb2f 100644\n--- a/git-sh-setup.sh\n+++ b/git-sh-setup.sh\n@@ -345,7 +345,7 @@ then\n \t\techo >&2 \"Unable to determine absolute path of git directory\"\n \t\texit 1\n \t}\n-\t: ${GIT_OBJECT_DIRECTORY=\"$GIT_DIR/objects\"}\n+\t: ${GIT_OBJECT_DIRECTORY=\"$(git rev-parse --git-path objects)\"}\n fi\n \n peel_committish () {\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249173","messageId":"1410388928-32265-14-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1410388928-32265-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v2 13/32] *.sh: avoid hardcoding $GIT_DIR/hooks/...","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-10T22:41:49Z","receivedAt":"2014-09-10T22:41:49Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"If $GIT_COMMON_DIR is set, it should be $GIT_COMMON_DIR/hooks/, not\n$GIT_DIR/hooks/. Just let rev-parse --git-path handle it.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n git-am.sh                              | 22 +++++++++++-----------\n git-rebase--interactive.sh             |  6 +++---\n git-rebase--merge.sh                   |  6 ++----\n git-rebase.sh                          |  4 ++--\n templates/hooks--applypatch-msg.sample |  4 ++--\n templates/hooks--pre-applypatch.sample |  4 ++--\n 6 files changed, 22 insertions(+), 24 deletions(-)\n\ndiff --git a/git-am.sh b/git-am.sh\nindex ee61a77..66803d1 100755\n--- a/git-am.sh\n+++ b/git-am.sh\n@@ -810,10 +810,10 @@ To restore the original branch and stop patching run \\\"\\$cmdline --abort\\\".\"\n \t\tcontinue\n \tfi\n \n-\tif test -x \"$GIT_DIR\"/hooks/applypatch-msg\n+\thook=\"$(git rev-parse --git-path hooks/applypatch-msg)\"\n+\tif test -x \"$hook\"\n \tthen\n-\t\t\"$GIT_DIR\"/hooks/applypatch-msg \"$dotest/final-commit\" ||\n-\t\tstop_here $this\n+\t\t\"$hook\" \"$dotest/final-commit\" || stop_here $this\n \tfi\n \n \tif test -f \"$dotest/final-commit\"\n@@ -887,9 +887,10 @@ did you forget to use 'git add'?\"\n \t\tstop_here_user_resolve $this\n \tfi\n \n-\tif test -x \"$GIT_DIR\"/hooks/pre-applypatch\n+\thook=\"$(git rev-parse --git-path hooks/pre-applypatch)\"\n+\tif test -x \"$hook\"\n \tthen\n-\t\t\"$GIT_DIR\"/hooks/pre-applypatch || stop_here $this\n+\t\t\"$hook\" || stop_here $this\n \tfi\n \n \ttree=$(git write-tree) &&\n@@ -916,18 +917,17 @@ did you forget to use 'git add'?\"\n \t\techo \"$(cat \"$dotest/original-commit\") $commit\" >> \"$dotest/rewritten\"\n \tfi\n \n-\tif test -x \"$GIT_DIR\"/hooks/post-applypatch\n-\tthen\n-\t\t\"$GIT_DIR\"/hooks/post-applypatch\n-\tfi\n+\thook=\"$(git rev-parse --git-path hooks/post-applypatch)\"\n+\ttest -x \"$hook\" && \"$hook\"\n \n \tgo_next\n done\n \n if test -s \"$dotest\"/rewritten; then\n     git notes copy --for-rewrite=rebase < \"$dotest\"/rewritten\n-    if test -x \"$GIT_DIR\"/hooks/post-rewrite; then\n-\t\"$GIT_DIR\"/hooks/post-rewrite rebase < \"$dotest\"/rewritten\n+    hook=\"$(git rev-parse --git-path hooks/post-rewrite)\"\n+    if test -x \"$hook\"; then\n+\t\"$hook\" rebase < \"$dotest\"/rewritten\n     fi\n fi\n \ndiff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh\nindex 7e1eda0..e8995f9 100644\n--- a/git-rebase--interactive.sh\n+++ b/git-rebase--interactive.sh\n@@ -642,9 +642,9 @@ do_next () {\n \t\tgit notes copy --for-rewrite=rebase < \"$rewritten_list\" ||\n \t\ttrue # we don't care if this copying failed\n \t} &&\n-\tif test -x \"$GIT_DIR\"/hooks/post-rewrite &&\n-\t\ttest -s \"$rewritten_list\"; then\n-\t\t\"$GIT_DIR\"/hooks/post-rewrite rebase < \"$rewritten_list\"\n+\thook=\"$(git rev-parse --git-path hooks/post-rewrite)\"\n+\tif test -x \"$hook\" && test -s \"$rewritten_list\"; then\n+\t\t\"$hook\" rebase < \"$rewritten_list\"\n \t\ttrue # we don't care if this hook failed\n \tfi &&\n \twarn \"Successfully rebased and updated $head_name.\"\ndiff --git a/git-rebase--merge.sh b/git-rebase--merge.sh\nindex d3fb67d..2cc2a6d 100644\n--- a/git-rebase--merge.sh\n+++ b/git-rebase--merge.sh\n@@ -94,10 +94,8 @@ finish_rb_merge () {\n \tif test -s \"$state_dir\"/rewritten\n \tthen\n \t\tgit notes copy --for-rewrite=rebase <\"$state_dir\"/rewritten\n-\t\tif test -x \"$GIT_DIR\"/hooks/post-rewrite\n-\t\tthen\n-\t\t\t\"$GIT_DIR\"/hooks/post-rewrite rebase <\"$state_dir\"/rewritten\n-\t\tfi\n+\t\thook=\"$(git rev-parse --git-path hooks/post-rewrite)\"\n+\t\ttest -x \"$hook\" && \"$hook\" rebase <\"$state_dir\"/rewritten\n \tfi\n \tsay All done.\n }\ndiff --git a/git-rebase.sh b/git-rebase.sh\nindex 06c810b..d60e710 100755\n--- a/git-rebase.sh\n+++ b/git-rebase.sh\n@@ -201,9 +201,9 @@ run_specific_rebase () {\n \n run_pre_rebase_hook () {\n \tif test -z \"$ok_to_skip_pre_rebase\" &&\n-\t   test -x \"$GIT_DIR/hooks/pre-rebase\"\n+\t   test -x \"$(git rev-parse --git-path hooks/pre-rebase)\"\n \tthen\n-\t\t\"$GIT_DIR/hooks/pre-rebase\" ${1+\"$@\"} ||\n+\t\t\"$(git rev-parse --git-path hooks/pre-rebase)\" ${1+\"$@\"} ||\n \t\tdie \"$(gettext \"The pre-rebase hook refused to rebase.\")\"\n \tfi\n }\ndiff --git a/templates/hooks--applypatch-msg.sample b/templates/hooks--applypatch-msg.sample\nindex 8b2a2fe..a5d7b84 100755\n--- a/templates/hooks--applypatch-msg.sample\n+++ b/templates/hooks--applypatch-msg.sample\n@@ -10,6 +10,6 @@\n # To enable this hook, rename this file to \"applypatch-msg\".\n \n . git-sh-setup\n-test -x \"$GIT_DIR/hooks/commit-msg\" &&\n-\texec \"$GIT_DIR/hooks/commit-msg\" ${1+\"$@\"}\n+commitmsg=\"$(git rev-parse --git-path hooks/commit-msg)\"\n+test -x \"$commitmsg\" && exec \"$commitmsg\" ${1+\"$@\"}\n :\ndiff --git a/templates/hooks--pre-applypatch.sample b/templates/hooks--pre-applypatch.sample\nindex b1f187c..4142082 100755\n--- a/templates/hooks--pre-applypatch.sample\n+++ b/templates/hooks--pre-applypatch.sample\n@@ -9,6 +9,6 @@\n # To enable this hook, rename this file to \"pre-applypatch\".\n \n . git-sh-setup\n-test -x \"$GIT_DIR/hooks/pre-commit\" &&\n-\texec \"$GIT_DIR/hooks/pre-commit\" ${1+\"$@\"}\n+precommit=\"$(git rev-parse --git-path hooks/pre-commit)\"\n+test -x \"$precommit\" && exec \"$precommit\" ${1+\"$@\"}\n :\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249174","messageId":"1410388928-32265-15-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1410388928-32265-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v2 14/32] git-stash: avoid hardcoding $GIT_DIR/logs/....","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-10T22:41:50Z","receivedAt":"2014-09-10T22:41:50Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n git-stash.sh | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/git-stash.sh b/git-stash.sh\nindex 393e1ec..41f8f6b 100755\n--- a/git-stash.sh\n+++ b/git-stash.sh\n@@ -184,7 +184,7 @@ store_stash () {\n \tfi\n \n \t# Make sure the reflog for stash is kept.\n-\t: >>\"$GIT_DIR/logs/$ref_stash\"\n+\t: >>\"$(git rev-parse --git-path logs/$ref_stash)\"\n \tgit update-ref -m \"$stash_msg\" $ref_stash $w_commit\n \tret=$?\n \ttest $ret != 0 && test -z $quiet &&\n@@ -259,7 +259,7 @@ save_stash () {\n \t\tsay \"$(gettext \"No local changes to save\")\"\n \t\texit 0\n \tfi\n-\ttest -f \"$GIT_DIR/logs/$ref_stash\" ||\n+\ttest -f \"$(git rev-parse --git-path logs/$ref_stash)\" ||\n \t\tclear_stash || die \"$(gettext \"Cannot initialize stash\")\"\n \n \tcreate_stash \"$stash_msg\" $untracked\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249175","messageId":"1410388928-32265-16-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1410388928-32265-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v2 15/32] setup.c: convert is_git_directory() to use strbuf","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-10T22:41:51Z","receivedAt":"2014-09-10T22:41:51Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n setup.c | 37 +++++++++++++++++++++----------------\n 1 file changed, 21 insertions(+), 16 deletions(-)\n\ndiff --git a/setup.c b/setup.c\nindex 0a22f8b..425fd79 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -238,31 +238,36 @@ void verify_non_filename(const char *prefix, const char *arg)\n  */\n int is_git_directory(const char *suspect)\n {\n-\tchar path[PATH_MAX];\n-\tsize_t len = strlen(suspect);\n+\tstruct strbuf path = STRBUF_INIT;\n+\tint ret = 0;\n+\tsize_t len;\n \n-\tif (PATH_MAX <= len + strlen(\"/objects\"))\n-\t\tdie(\"Too long path: %.*s\", 60, suspect);\n-\tstrcpy(path, suspect);\n+\tstrbuf_addstr(&path, suspect);\n+\tlen = path.len;\n \tif (getenv(DB_ENVIRONMENT)) {\n \t\tif (access(getenv(DB_ENVIRONMENT), X_OK))\n-\t\t\treturn 0;\n+\t\t\tgoto done;\n \t}\n \telse {\n-\t\tstrcpy(path + len, \"/objects\");\n-\t\tif (access(path, X_OK))\n-\t\t\treturn 0;\n+\t\tstrbuf_addstr(&path, \"/objects\");\n+\t\tif (access(path.buf, X_OK))\n+\t\t\tgoto done;\n \t}\n \n-\tstrcpy(path + len, \"/refs\");\n-\tif (access(path, X_OK))\n-\t\treturn 0;\n+\tstrbuf_setlen(&path, len);\n+\tstrbuf_addstr(&path, \"/refs\");\n+\tif (access(path.buf, X_OK))\n+\t\tgoto done;\n \n-\tstrcpy(path + len, \"/HEAD\");\n-\tif (validate_headref(path))\n-\t\treturn 0;\n+\tstrbuf_setlen(&path, len);\n+\tstrbuf_addstr(&path, \"/HEAD\");\n+\tif (validate_headref(path.buf))\n+\t\tgoto done;\n \n-\treturn 1;\n+\tret = 1;\n+done:\n+\tstrbuf_release(&path);\n+\treturn ret;\n }\n \n int is_inside_git_dir(void)\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249176","messageId":"1410388928-32265-17-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1410388928-32265-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v2 16/32] setup.c: detect $GIT_COMMON_DIR in is_git_directory()","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-10T22:41:52Z","receivedAt":"2014-09-10T22:41:52Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"If the file \"$GIT_DIR/commondir\" exists, it contains the value of\n$GIT_COMMON_DIR.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n Documentation/gitrepository-layout.txt |  7 ++++++\n setup.c                                | 43 +++++++++++++++++++++++++++++-----\n 2 files changed, 44 insertions(+), 6 deletions(-)\n\ndiff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt\nindex b7b9dfb..1cc33f3 100644\n--- a/Documentation/gitrepository-layout.txt\n+++ b/Documentation/gitrepository-layout.txt\n@@ -236,6 +236,13 @@ shallow::\n \tfile is ignored if $GIT_COMMON_DIR is set and\n \t\"$GIT_COMMON_DIR/shallow\" will be used instead.\n \n+commondir::\n+\tIf this file exists, $GIT_COMMON_DIR (see linkgit:git[1]) will\n+\tbe set to the path specified in this file if it is not\n+\texplicitly set. If the specified path is relative, it is\n+\trelative to $GIT_DIR. The repository with commondir is\n+\tincomplete without the repository pointed by \"commondir\".\n+\n modules::\n \tContains the git-repositories of the submodules. This\n \tdirectory is ignored if $GIT_COMMON_DIR is set and\ndiff --git a/setup.c b/setup.c\nindex 425fd79..176d505 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -224,6 +224,33 @@ void verify_non_filename(const char *prefix, const char *arg)\n \t    \"'git <command> [<revision>...] -- [<file>...]'\", arg);\n }\n \n+static void get_common_dir(struct strbuf *sb, const char *gitdir)\n+{\n+\tstruct strbuf data = STRBUF_INIT;\n+\tstruct strbuf path = STRBUF_INIT;\n+\tconst char *git_common_dir = getenv(GIT_COMMON_DIR_ENVIRONMENT);\n+\tif (git_common_dir) {\n+\t\tstrbuf_addstr(sb, git_common_dir);\n+\t\treturn;\n+\t}\n+\tstrbuf_addf(&path, \"%s/commondir\", gitdir);\n+\tif (file_exists(path.buf)) {\n+\t\tif (strbuf_read_file(&data, path.buf, 0) <= 0)\n+\t\t\tdie_errno(_(\"failed to read %s\"), path.buf);\n+\t\twhile (data.len && (data.buf[data.len - 1] == '\\n' ||\n+\t\t\t\t    data.buf[data.len - 1] == '\\r'))\n+\t\t\tdata.len--;\n+\t\tdata.buf[data.len] = '\\0';\n+\t\tstrbuf_reset(&path);\n+\t\tif (!is_absolute_path(data.buf))\n+\t\t\tstrbuf_addf(&path, \"%s/\", gitdir);\n+\t\tstrbuf_addbuf(&path, &data);\n+\t\tstrbuf_addstr(sb, real_path(path.buf));\n+\t} else\n+\t\tstrbuf_addstr(sb, gitdir);\n+\tstrbuf_release(&data);\n+\tstrbuf_release(&path);\n+}\n \n /*\n  * Test if it looks like we're at a git directory.\n@@ -242,13 +269,22 @@ int is_git_directory(const char *suspect)\n \tint ret = 0;\n \tsize_t len;\n \n-\tstrbuf_addstr(&path, suspect);\n+\t/* Check worktree-related signatures */\n+\tstrbuf_addf(&path, \"%s/HEAD\", suspect);\n+\tif (validate_headref(path.buf))\n+\t\tgoto done;\n+\n+\tstrbuf_reset(&path);\n+\tget_common_dir(&path, suspect);\n \tlen = path.len;\n+\n+\t/* Check non-worktree-related signatures */\n \tif (getenv(DB_ENVIRONMENT)) {\n \t\tif (access(getenv(DB_ENVIRONMENT), X_OK))\n \t\t\tgoto done;\n \t}\n \telse {\n+\t\tstrbuf_setlen(&path, len);\n \t\tstrbuf_addstr(&path, \"/objects\");\n \t\tif (access(path.buf, X_OK))\n \t\t\tgoto done;\n@@ -259,11 +295,6 @@ int is_git_directory(const char *suspect)\n \tif (access(path.buf, X_OK))\n \t\tgoto done;\n \n-\tstrbuf_setlen(&path, len);\n-\tstrbuf_addstr(&path, \"/HEAD\");\n-\tif (validate_headref(path.buf))\n-\t\tgoto done;\n-\n \tret = 1;\n done:\n \tstrbuf_release(&path);\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249177","messageId":"1410388928-32265-18-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1410388928-32265-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v2 17/32] setup.c: convert check_repository_format_gently to use strbuf","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-10T22:41:53Z","receivedAt":"2014-09-10T22:41:53Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n setup.c | 12 ++++++++----\n 1 file changed, 8 insertions(+), 4 deletions(-)\n\ndiff --git a/setup.c b/setup.c\nindex 176d505..a17389f 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -342,7 +342,9 @@ void setup_work_tree(void)\n \n static int check_repository_format_gently(const char *gitdir, int *nongit_ok)\n {\n-\tchar repo_config[PATH_MAX+1];\n+\tstruct strbuf sb = STRBUF_INIT;\n+\tconst char *repo_config;\n+\tint ret = 0;\n \n \t/*\n \t * git_config() can't be used here because it calls git_pathdup()\n@@ -353,7 +355,8 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)\n \t * Use a gentler version of git_config() to check if this repo\n \t * is a good one.\n \t */\n-\tsnprintf(repo_config, PATH_MAX, \"%s/config\", gitdir);\n+\tstrbuf_addf(&sb, \"%s/config\", gitdir);\n+\trepo_config = sb.buf;\n \tgit_config_early(check_repository_format_version, NULL, repo_config);\n \tif (GIT_REPO_VERSION < repository_format_version) {\n \t\tif (!nongit_ok)\n@@ -363,9 +366,10 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)\n \t\t\tGIT_REPO_VERSION, repository_format_version);\n \t\twarning(\"Please upgrade Git\");\n \t\t*nongit_ok = -1;\n-\t\treturn -1;\n+\t\tret = -1;\n \t}\n-\treturn 0;\n+\tstrbuf_release(&sb);\n+\treturn ret;\n }\n \n /*\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249178","messageId":"1410388928-32265-19-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1410388928-32265-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v2 18/32] setup.c: detect $GIT_COMMON_DIR check_repository_format_gently()","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-10T22:41:54Z","receivedAt":"2014-09-10T22:41:54Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n setup.c | 6 ++++--\n 1 file changed, 4 insertions(+), 2 deletions(-)\n\ndiff --git a/setup.c b/setup.c\nindex a17389f..79f79f2 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -346,6 +346,10 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)\n \tconst char *repo_config;\n \tint ret = 0;\n \n+\tget_common_dir(&sb, gitdir);\n+\tstrbuf_addstr(&sb, \"/config\");\n+\trepo_config = sb.buf;\n+\n \t/*\n \t * git_config() can't be used here because it calls git_pathdup()\n \t * to get $GIT_CONFIG/config. That call will make setup_git_env()\n@@ -355,8 +359,6 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)\n \t * Use a gentler version of git_config() to check if this repo\n \t * is a good one.\n \t */\n-\tstrbuf_addf(&sb, \"%s/config\", gitdir);\n-\trepo_config = sb.buf;\n \tgit_config_early(check_repository_format_version, NULL, repo_config);\n \tif (GIT_REPO_VERSION < repository_format_version) {\n \t\tif (!nongit_ok)\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249179","messageId":"1410388928-32265-20-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1410388928-32265-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v2 19/32] setup.c: support multi-checkout repo setup","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-10T22:41:55Z","receivedAt":"2014-09-10T22:41:55Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"The repo setup procedure is updated to detect $GIT_DIR/commondir and\nset $GIT_COMMON_DIR properly.\n\nThe core.worktree is ignored when $GIT_COMMON_DIR is set. This is\nbecause the config file is shared in multi-checkout setup, but\ncheckout directories _are_ different. Making core.worktree effective\nin all checkouts mean it's back to a single checkout.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n Documentation/config.txt        |  2 ++\n Documentation/git-rev-parse.txt |  3 ++\n builtin/rev-parse.c             |  4 +++\n cache.h                         |  1 +\n environment.c                   |  8 ++---\n setup.c                         | 33 +++++++++++++-----\n t/t1501-worktree.sh             | 76 +++++++++++++++++++++++++++++++++++++++++\n t/t1510-repo-setup.sh           |  1 +\n trace.c                         |  1 +\n 9 files changed, 115 insertions(+), 14 deletions(-)\n\ndiff --git a/Documentation/config.txt b/Documentation/config.txt\nindex 1d718bd..286e539 100644\n--- a/Documentation/config.txt\n+++ b/Documentation/config.txt\n@@ -380,6 +380,8 @@ false), while all other repositories are assumed to be bare (bare\n \n core.worktree::\n \tSet the path to the root of the working tree.\n+\tIf GIT_COMMON_DIR environment variable is set, core.worktree\n+\tis ignored and not used for determining the root of working tree.\n \tThis can be overridden by the GIT_WORK_TREE environment\n \tvariable and the '--work-tree' command-line option.\n \tThe value can be an absolute path or relative to the path to\ndiff --git a/Documentation/git-rev-parse.txt b/Documentation/git-rev-parse.txt\nindex 9465399..f1867d3 100644\n--- a/Documentation/git-rev-parse.txt\n+++ b/Documentation/git-rev-parse.txt\n@@ -215,6 +215,9 @@ If `$GIT_DIR` is not defined and the current directory\n is not detected to lie in a Git repository or work tree\n print a message to stderr and exit with nonzero status.\n \n+--git-common-dir::\n+\tShow `$GIT_COMMON_DIR` if defined, else `$GIT_DIR`.\n+\n --is-inside-git-dir::\n \tWhen the current working directory is below the repository\n \tdirectory print \"true\", otherwise \"false\".\ndiff --git a/builtin/rev-parse.c b/builtin/rev-parse.c\nindex 7606d43..29475c5 100644\n--- a/builtin/rev-parse.c\n+++ b/builtin/rev-parse.c\n@@ -757,6 +757,10 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)\n \t\t\t\tprintf(\"%s%s.git\\n\", cwd, len && cwd[len-1] != '/' ? \"/\" : \"\");\n \t\t\t\tcontinue;\n \t\t\t}\n+\t\t\tif (!strcmp(arg, \"--git-common-dir\")) {\n+\t\t\t\tputs(get_git_common_dir());\n+\t\t\t\tcontinue;\n+\t\t\t}\n \t\t\tif (!strcmp(arg, \"--resolve-git-dir\")) {\n \t\t\t\tconst char *gitdir = argv[++i];\n \t\t\t\tif (!gitdir)\ndiff --git a/cache.h b/cache.h\nindex b606ee4..e3ff7dc 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -422,6 +422,7 @@ extern char *get_object_directory(void);\n extern char *get_index_file(void);\n extern char *get_graft_file(void);\n extern int set_git_dir(const char *path);\n+extern int get_common_dir(struct strbuf *sb, const char *gitdir);\n extern const char *get_git_namespace(void);\n extern const char *strip_namespace(const char *namespaced_ref);\n extern const char *get_git_work_tree(void);\ndiff --git a/environment.c b/environment.c\nindex 78a07e4..d5b0788 100644\n--- a/environment.c\n+++ b/environment.c\n@@ -141,6 +141,7 @@ static char *git_path_from_env(const char *envvar, const char *git_dir,\n \n static void setup_git_env(void)\n {\n+\tstruct strbuf sb = STRBUF_INIT;\n \tconst char *gitfile;\n \tconst char *shallow_file;\n \n@@ -149,12 +150,9 @@ static void setup_git_env(void)\n \t\tgit_dir = DEFAULT_GIT_DIR_ENVIRONMENT;\n \tgitfile = read_gitfile(git_dir);\n \tgit_dir = xstrdup(gitfile ? gitfile : git_dir);\n-\tgit_common_dir = getenv(GIT_COMMON_DIR_ENVIRONMENT);\n-\tif (git_common_dir) {\n+\tif (get_common_dir(&sb, git_dir))\n \t\tgit_common_dir_env = 1;\n-\t\tgit_common_dir = xstrdup(git_common_dir);\n-\t} else\n-\t\tgit_common_dir = git_dir;\n+\tgit_common_dir = strbuf_detach(&sb, NULL);\n \tgit_object_dir = git_path_from_env(DB_ENVIRONMENT, git_common_dir,\n \t\t\t\t\t   \"objects\", &git_db_env);\n \tgit_index_file = git_path_from_env(INDEX_ENVIRONMENT, git_dir,\ndiff --git a/setup.c b/setup.c\nindex 79f79f2..8f90bc3 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -224,14 +224,15 @@ void verify_non_filename(const char *prefix, const char *arg)\n \t    \"'git <command> [<revision>...] -- [<file>...]'\", arg);\n }\n \n-static void get_common_dir(struct strbuf *sb, const char *gitdir)\n+int get_common_dir(struct strbuf *sb, const char *gitdir)\n {\n \tstruct strbuf data = STRBUF_INIT;\n \tstruct strbuf path = STRBUF_INIT;\n \tconst char *git_common_dir = getenv(GIT_COMMON_DIR_ENVIRONMENT);\n+\tint ret = 0;\n \tif (git_common_dir) {\n \t\tstrbuf_addstr(sb, git_common_dir);\n-\t\treturn;\n+\t\treturn 1;\n \t}\n \tstrbuf_addf(&path, \"%s/commondir\", gitdir);\n \tif (file_exists(path.buf)) {\n@@ -246,10 +247,12 @@ static void get_common_dir(struct strbuf *sb, const char *gitdir)\n \t\t\tstrbuf_addf(&path, \"%s/\", gitdir);\n \t\tstrbuf_addbuf(&path, &data);\n \t\tstrbuf_addstr(sb, real_path(path.buf));\n+\t\tret = 1;\n \t} else\n \t\tstrbuf_addstr(sb, gitdir);\n \tstrbuf_release(&data);\n \tstrbuf_release(&path);\n+\treturn ret;\n }\n \n /*\n@@ -340,13 +343,26 @@ void setup_work_tree(void)\n \tinitialized = 1;\n }\n \n+static int check_repo_format(const char *var, const char *value, void *cb)\n+{\n+\tif (strcmp(var, \"core.repositoryformatversion\") == 0)\n+\t\trepository_format_version = git_config_int(var, value);\n+\telse if (strcmp(var, \"core.sharedrepository\") == 0)\n+\t\tshared_repository = git_config_perm(var, value);\n+\treturn 0;\n+}\n+\n static int check_repository_format_gently(const char *gitdir, int *nongit_ok)\n {\n \tstruct strbuf sb = STRBUF_INIT;\n \tconst char *repo_config;\n+\tconfig_fn_t fn;\n \tint ret = 0;\n \n-\tget_common_dir(&sb, gitdir);\n+\tif (get_common_dir(&sb, gitdir))\n+\t\tfn = check_repo_format;\n+\telse\n+\t\tfn = check_repository_format_version;\n \tstrbuf_addstr(&sb, \"/config\");\n \trepo_config = sb.buf;\n \n@@ -359,7 +375,7 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)\n \t * Use a gentler version of git_config() to check if this repo\n \t * is a good one.\n \t */\n-\tgit_config_early(check_repository_format_version, NULL, repo_config);\n+\tgit_config_early(fn, NULL, repo_config);\n \tif (GIT_REPO_VERSION < repository_format_version) {\n \t\tif (!nongit_ok)\n \t\t\tdie (\"Expected git repo version <= %d, found %d\",\n@@ -831,11 +847,10 @@ int git_config_perm(const char *var, const char *value)\n \n int check_repository_format_version(const char *var, const char *value, void *cb)\n {\n-\tif (strcmp(var, \"core.repositoryformatversion\") == 0)\n-\t\trepository_format_version = git_config_int(var, value);\n-\telse if (strcmp(var, \"core.sharedrepository\") == 0)\n-\t\tshared_repository = git_config_perm(var, value);\n-\telse if (strcmp(var, \"core.bare\") == 0) {\n+\tint ret = check_repo_format(var, value, cb);\n+\tif (ret)\n+\t\treturn ret;\n+\tif (strcmp(var, \"core.bare\") == 0) {\n \t\tis_bare_repository_cfg = git_config_bool(var, value);\n \t\tif (is_bare_repository_cfg == 1)\n \t\t\tinside_work_tree = -1;\ndiff --git a/t/t1501-worktree.sh b/t/t1501-worktree.sh\nindex 8f36aa9..e6ac7a4 100755\n--- a/t/t1501-worktree.sh\n+++ b/t/t1501-worktree.sh\n@@ -346,4 +346,80 @@ test_expect_success 'relative $GIT_WORK_TREE and git subprocesses' '\n \ttest_cmp expected actual\n '\n \n+test_expect_success 'Multi-worktree setup' '\n+\tmkdir work &&\n+\tmkdir -p repo.git/repos/foo &&\n+\tcp repo.git/HEAD repo.git/index repo.git/repos/foo &&\n+\tsane_unset GIT_DIR GIT_CONFIG GIT_WORK_TREE\n+'\n+\n+test_expect_success 'GIT_DIR set (1)' '\n+\techo \"gitdir: repo.git/repos/foo\" >gitfile &&\n+\techo ../.. >repo.git/repos/foo/commondir &&\n+\t(\n+\t\tcd work &&\n+\t\tGIT_DIR=../gitfile git rev-parse --git-common-dir >actual &&\n+\t\ttest-path-utils real_path \"$TRASH_DIRECTORY/repo.git\" >expect &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'GIT_DIR set (2)' '\n+\techo \"gitdir: repo.git/repos/foo\" >gitfile &&\n+\techo \"$TRASH_DIRECTORY/repo.git\" >repo.git/repos/foo/commondir &&\n+\t(\n+\t\tcd work &&\n+\t\tGIT_DIR=../gitfile git rev-parse --git-common-dir >actual &&\n+\t\ttest-path-utils real_path \"$TRASH_DIRECTORY/repo.git\" >expect &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'Auto discovery' '\n+\techo \"gitdir: repo.git/repos/foo\" >.git &&\n+\techo ../.. >repo.git/repos/foo/commondir &&\n+\t(\n+\t\tcd work &&\n+\t\tgit rev-parse --git-common-dir >actual &&\n+\t\ttest-path-utils real_path \"$TRASH_DIRECTORY/repo.git\" >expect &&\n+\t\ttest_cmp expect actual &&\n+\t\techo haha >data1 &&\n+\t\tgit add data1 &&\n+\t\tgit ls-files --full-name :/ | grep data1 >actual &&\n+\t\techo work/data1 >expect &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success '$GIT_DIR/common overrides core.worktree' '\n+\tmkdir elsewhere &&\n+\tgit --git-dir=repo.git config core.worktree \"$TRASH_DIRECTORY/elsewhere\" &&\n+\techo \"gitdir: repo.git/repos/foo\" >.git &&\n+\techo ../.. >repo.git/repos/foo/commondir &&\n+\t(\n+\t\tcd work &&\n+\t\tgit rev-parse --git-common-dir >actual &&\n+\t\ttest-path-utils real_path \"$TRASH_DIRECTORY/repo.git\" >expect &&\n+\t\ttest_cmp expect actual &&\n+\t\techo haha >data2 &&\n+\t\tgit add data2 &&\n+\t\tgit ls-files --full-name :/ | grep data2 >actual &&\n+\t\techo work/data2 >expect &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success '$GIT_WORK_TREE overrides $GIT_DIR/common' '\n+\techo \"gitdir: repo.git/repos/foo\" >.git &&\n+\techo ../.. >repo.git/repos/foo/commondir &&\n+\t(\n+\t\tcd work &&\n+\t\techo haha >data3 &&\n+\t\tgit --git-dir=../.git --work-tree=. add data3 &&\n+\t\tgit ls-files --full-name -- :/ | grep data3 >actual &&\n+\t\techo data3 >expect &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n test_done\ndiff --git a/t/t1510-repo-setup.sh b/t/t1510-repo-setup.sh\nindex e1b2a99..33c1a58 100755\n--- a/t/t1510-repo-setup.sh\n+++ b/t/t1510-repo-setup.sh\n@@ -106,6 +106,7 @@ setup_env () {\n expect () {\n \tcat >\"$1/expected\" <<-EOF\n \tsetup: git_dir: $2\n+\tsetup: git_common_dir: $2\n \tsetup: worktree: $3\n \tsetup: cwd: $4\n \tsetup: prefix: $5\ndiff --git a/trace.c b/trace.c\nindex 08180a9..a594761 100644\n--- a/trace.c\n+++ b/trace.c\n@@ -173,6 +173,7 @@ void trace_repo_setup(const char *prefix)\n \t\tprefix = \"(null)\";\n \n \ttrace_printf_key(key, \"setup: git_dir: %s\\n\", quote_crnl(get_git_dir()));\n+\ttrace_printf_key(key, \"setup: git_common_dir: %s\\n\", quote_crnl(get_git_common_dir()));\n \ttrace_printf_key(key, \"setup: worktree: %s\\n\", quote_crnl(git_work_tree));\n \ttrace_printf_key(key, \"setup: cwd: %s\\n\", quote_crnl(cwd));\n \ttrace_printf_key(key, \"setup: prefix: %s\\n\", quote_crnl(prefix));\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249180","messageId":"1410388928-32265-21-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1410388928-32265-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v2 20/32] wrapper.c: wrapper to open a file, fprintf then close","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-10T22:41:56Z","receivedAt":"2014-09-10T22:41:56Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n cache.h   |  2 ++\n wrapper.c | 31 +++++++++++++++++++++++++++++++\n 2 files changed, 33 insertions(+)\n\ndiff --git a/cache.h b/cache.h\nindex e3ff7dc..bf4d15e 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -1359,6 +1359,8 @@ static inline ssize_t write_str_in_full(int fd, const char *str)\n {\n \treturn write_in_full(fd, str, strlen(str));\n }\n+__attribute__((format (printf,3,4)))\n+extern int write_file(const char *path, int fatal, const char *fmt, ...);\n \n /* pager.c */\n extern void setup_pager(void);\ndiff --git a/wrapper.c b/wrapper.c\nindex bc1bfb8..9d7b9ac 100644\n--- a/wrapper.c\n+++ b/wrapper.c\n@@ -493,3 +493,34 @@ struct passwd *xgetpwuid_self(void)\n \t\t    errno ? strerror(errno) : _(\"no such user\"));\n \treturn pw;\n }\n+\n+int write_file(const char *path, int fatal, const char *fmt, ...)\n+{\n+\tstruct strbuf sb = STRBUF_INIT;\n+\tva_list params;\n+\tint fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0666);\n+\tif (fd < 0) {\n+\t\tif (fatal)\n+\t\t\tdie_errno(_(\"could not open %s for writing\"), path);\n+\t\treturn -1;\n+\t}\n+\tva_start(params, fmt);\n+\tstrbuf_vaddf(&sb, fmt, params);\n+\tva_end(params);\n+\tif (write_in_full(fd, sb.buf, sb.len) != sb.len) {\n+\t\tint err = errno;\n+\t\tclose(fd);\n+\t\tstrbuf_release(&sb);\n+\t\terrno = err;\n+\t\tif (fatal)\n+\t\t\tdie_errno(_(\"could not write to %s\"), path);\n+\t\treturn -1;\n+\t}\n+\tstrbuf_release(&sb);\n+\tif (close(fd)) {\n+\t\tif (fatal)\n+\t\t\tdie_errno(_(\"could not close %s\"), path);\n+\t\treturn -1;\n+\t}\n+\treturn 0;\n+}\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249181","messageId":"1410388928-32265-22-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1410388928-32265-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v2 21/32] use new wrapper write_file() for simple file writing","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-10T22:41:57Z","receivedAt":"2014-09-10T22:41:57Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"This fixes common problems in these code about error handling,\nforgetting to close the file handle after fprintf() fails, or not\nprinting out the error string..\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n builtin/branch.c  |  4 +---\n builtin/init-db.c |  7 +------\n daemon.c          | 11 +----------\n submodule.c       |  9 ++-------\n transport.c       |  8 +++-----\n 5 files changed, 8 insertions(+), 31 deletions(-)\n\ndiff --git a/builtin/branch.c b/builtin/branch.c\nindex 0591b22..e4265a1 100644\n--- a/builtin/branch.c\n+++ b/builtin/branch.c\n@@ -754,7 +754,6 @@ static const char edit_description[] = \"BRANCH_DESCRIPTION\";\n \n static int edit_branch_description(const char *branch_name)\n {\n-\tFILE *fp;\n \tint status;\n \tstruct strbuf buf = STRBUF_INIT;\n \tstruct strbuf name = STRBUF_INIT;\n@@ -767,8 +766,7 @@ static int edit_branch_description(const char *branch_name)\n \t\t    \"  %s\\n\"\n \t\t    \"Lines starting with '%c' will be stripped.\\n\",\n \t\t    branch_name, comment_line_char);\n-\tfp = fopen(git_path(edit_description), \"w\");\n-\tif ((fwrite(buf.buf, 1, buf.len, fp) < buf.len) || fclose(fp)) {\n+\tif (write_file(git_path(edit_description), 0, \"%s\", buf.buf)) {\n \t\tstrbuf_release(&buf);\n \t\treturn error(_(\"could not write branch description template: %s\"),\n \t\t\t     strerror(errno));\ndiff --git a/builtin/init-db.c b/builtin/init-db.c\nindex 56f85e2..ce8416a 100644\n--- a/builtin/init-db.c\n+++ b/builtin/init-db.c\n@@ -342,7 +342,6 @@ int set_git_dir_init(const char *git_dir, const char *real_git_dir,\n static void separate_git_dir(const char *git_dir)\n {\n \tstruct stat st;\n-\tFILE *fp;\n \n \tif (!stat(git_link, &st)) {\n \t\tconst char *src;\n@@ -358,11 +357,7 @@ static void separate_git_dir(const char *git_dir)\n \t\t\tdie_errno(_(\"unable to move %s to %s\"), src, git_dir);\n \t}\n \n-\tfp = fopen(git_link, \"w\");\n-\tif (!fp)\n-\t\tdie(_(\"Could not create git link %s\"), git_link);\n-\tfprintf(fp, \"gitdir: %s\\n\", git_dir);\n-\tfclose(fp);\n+\twrite_file(git_link, 1, \"gitdir: %s\\n\", git_dir);\n }\n \n int init_db(const char *template_dir, unsigned int flags)\ndiff --git a/daemon.c b/daemon.c\nindex 1eb6631..dd638a9 100644\n--- a/daemon.c\n+++ b/daemon.c\n@@ -1094,15 +1094,6 @@ static struct credentials *prepare_credentials(const char *user_name,\n }\n #endif\n \n-static void store_pid(const char *path)\n-{\n-\tFILE *f = fopen(path, \"w\");\n-\tif (!f)\n-\t\tdie_errno(\"cannot open pid file '%s'\", path);\n-\tif (fprintf(f, \"%\"PRIuMAX\"\\n\", (uintmax_t) getpid()) < 0 || fclose(f) != 0)\n-\t\tdie_errno(\"failed to write pid file '%s'\", path);\n-}\n-\n static int serve(struct string_list *listen_addr, int listen_port,\n     struct credentials *cred)\n {\n@@ -1313,7 +1304,7 @@ int main(int argc, char **argv)\n \t\tsanitize_stdfds();\n \n \tif (pid_file)\n-\t\tstore_pid(pid_file);\n+\t\twrite_file(pid_file, 1, \"%\"PRIuMAX\"\\n\", (uintmax_t) getpid());\n \n \t/* prepare argv for serving-processes */\n \tcld_argv = xmalloc(sizeof (char *) * (argc + 2));\ndiff --git a/submodule.c b/submodule.c\nindex b80ecac..b7b6059 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -1112,16 +1112,11 @@ void connect_work_tree_and_git_dir(const char *work_tree, const char *git_dir)\n \tstruct strbuf file_name = STRBUF_INIT;\n \tstruct strbuf rel_path = STRBUF_INIT;\n \tconst char *real_work_tree = xstrdup(real_path(work_tree));\n-\tFILE *fp;\n \n \t/* Update gitfile */\n \tstrbuf_addf(&file_name, \"%s/.git\", work_tree);\n-\tfp = fopen(file_name.buf, \"w\");\n-\tif (!fp)\n-\t\tdie(_(\"Could not create git link %s\"), file_name.buf);\n-\tfprintf(fp, \"gitdir: %s\\n\", relative_path(git_dir, real_work_tree,\n-\t\t\t\t\t\t  &rel_path));\n-\tfclose(fp);\n+\twrite_file(file_name.buf, 1, \"gitdir: %s\\n\",\n+\t\t   relative_path(git_dir, real_work_tree, &rel_path));\n \n \t/* Update core.worktree setting */\n \tstrbuf_reset(&file_name);\ndiff --git a/transport.c b/transport.c\nindex 59c9727..1b779bb 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -296,7 +296,6 @@ static int write_one_ref(const char *name, const unsigned char *sha1,\n {\n \tstruct strbuf *buf = data;\n \tint len = buf->len;\n-\tFILE *f;\n \n \t/* when called via for_each_ref(), flags is non-zero */\n \tif (flags && !starts_with(name, \"refs/heads/\") &&\n@@ -305,10 +304,9 @@ static int write_one_ref(const char *name, const unsigned char *sha1,\n \n \tstrbuf_addstr(buf, name);\n \tif (safe_create_leading_directories(buf->buf) ||\n-\t\t\t!(f = fopen(buf->buf, \"w\")) ||\n-\t\t\tfprintf(f, \"%s\\n\", sha1_to_hex(sha1)) < 0 ||\n-\t\t\tfclose(f))\n-\t\treturn error(\"problems writing temporary file %s\", buf->buf);\n+\t    write_file(buf->buf, 0, \"%s\\n\", sha1_to_hex(sha1)))\n+\t\treturn error(\"problems writing temporary file %s: %s\",\n+\t\t\t     buf->buf, strerror(errno));\n \tstrbuf_setlen(buf, len);\n \treturn 0;\n }\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249182","messageId":"1410388928-32265-23-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1410388928-32265-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v2 22/32] checkout: support checking out into a new working directory","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-10T22:41:58Z","receivedAt":"2014-09-10T22:41:58Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"\"git checkout --to\" sets up a new working directory with a .git file\npointing to $GIT_DIR/worktrees/<id>. It then executes \"git checkout\"\nagain on the new worktree with the same arguments except \"--to\" is\ntaken out. The second checkout execution, which is not contaminated\nwith any info from the current repository, will actually check out and\neverything that normal \"git checkout\" does.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n Documentation/git-checkout.txt         | 43 +++++++++++++++\n Documentation/git.txt                  |  3 +-\n Documentation/gitrepository-layout.txt |  7 +++\n builtin/checkout.c                     | 95 +++++++++++++++++++++++++++++++++-\n path.c                                 |  2 +-\n t/t2025-checkout-to.sh (new +x)        | 63 ++++++++++++++++++++++\n 6 files changed, 209 insertions(+), 4 deletions(-)\n create mode 100755 t/t2025-checkout-to.sh\n\ndiff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt\nindex 33ad2ad..bd0fc1d 100644\n--- a/Documentation/git-checkout.txt\n+++ b/Documentation/git-checkout.txt\n@@ -225,6 +225,13 @@ This means that you can use `git checkout -p` to selectively discard\n edits from your current working tree. See the ``Interactive Mode''\n section of linkgit:git-add[1] to learn how to operate the `--patch` mode.\n \n+--to=<path>::\n+\tCheck out a branch in a separate working directory at\n+\t`<path>`. A new working directory is linked to the current\n+\trepository, sharing everything except working directory\n+\tspecific files such as HEAD, index... See \"MULTIPLE CHECKOUT\n+\tMODE\" section for more information.\n+\n <branch>::\n \tBranch to checkout; if it refers to a branch (i.e., a name that,\n \twhen prepended with \"refs/heads/\", is a valid ref), then that\n@@ -388,6 +395,42 @@ $ git reflog -2 HEAD # or\n $ git log -g -2 HEAD\n ------------\n \n+MULTIPLE CHECKOUT MODE\n+----------------------\n+Normally a working directory is attached to repository. When \"git\n+checkout --to\" is used, a new working directory is attached to the\n+current repository. This new working directory is called \"linked\n+checkout\" as compared to the \"main checkout\" prepared by \"git init\" or\n+\"git clone\". A repository has one main checkout and zero or more\n+linked checkouts.\n+\n+Each linked checkout has a private directory in $GIT_DIR/worktrees in\n+the main checkout, usually named after the base name of the new\n+working directory, optionally with a number added to make it\n+unique. For example, the command `git checkout --to ../test-next next`\n+running with $GIT_DIR=/path/main may create the directory\n+`$GIT_DIR/worktrees/test-next` (or `$GIT_DIR/worktrees/test-next1` if\n+`test-next` is already taken).\n+\n+Within a linked checkout, $GIT_DIR is set to point to this private\n+directory (e.g. `/path/main/worktrees/test-next` in the example) and\n+$GIT_COMMON_DIR is set to point back to the main checkout's $GIT_DIR\n+(e.g. `/path/main`). Setting is done via a .git file located at the\n+top directory of the linked checkout.\n+\n+Path resolution via `git rev-parse --git-path` would use either\n+$GIT_DIR or $GIT_COMMON_DIR depending on the path. For example, the\n+linked checkout's `$GIT_DIR/HEAD` resolve to\n+`/path/main/worktrees/test-next/HEAD` (not `/path/main/HEAD` which is\n+the main checkout's HEAD) while `$GIT_DIR/refs/heads/master` would use\n+$GIT_COMMON_DIR and resolve to `/path/main/refs/heads/master`, which\n+is shared across checkouts.\n+\n+See linkgit:gitrepository-layout[5] for more information. The rule of\n+thumb is do not make any assumption about whether a path belongs to\n+$GIT_DIR or $GIT_COMMON_DIR when you need to directly access something\n+inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.\n+\n EXAMPLES\n --------\n \ndiff --git a/Documentation/git.txt b/Documentation/git.txt\nindex 749052f..c0a4940 100644\n--- a/Documentation/git.txt\n+++ b/Documentation/git.txt\n@@ -792,7 +792,8 @@ Git so take care if using Cogito etc.\n \tIf this variable is set to a path, non-worktree files that are\n \tnormally in $GIT_DIR will be taken from this path\n \tinstead. Worktree-specific files such as HEAD or index are\n-\ttaken from $GIT_DIR. See linkgit:gitrepository-layout[5] for\n+\ttaken from $GIT_DIR. See linkgit:gitrepository-layout[5] and\n+\tthe section 'MULTIPLE CHECKOUT MODE' in linkgit:checkout[1]\n \tdetails. This variable has lower precedence than other path\n \tvariables such as GIT_INDEX_FILE, GIT_OBJECT_DIRECTORY...\n \ndiff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt\nindex 1cc33f3..a82780c 100644\n--- a/Documentation/gitrepository-layout.txt\n+++ b/Documentation/gitrepository-layout.txt\n@@ -248,6 +248,13 @@ modules::\n \tdirectory is ignored if $GIT_COMMON_DIR is set and\n \t\"$GIT_COMMON_DIR/modules\" will be used instead.\n \n+worktrees::\n+\tContains worktree specific information of linked\n+\tcheckouts. Each subdirectory contains the worktree-related\n+\tpart of a linked checkout. This directory is ignored if\n+\t$GIT_COMMON_DIR is set and \"$GIT_COMMON_DIR/worktrees\" will be\n+\tused instead.\n+\n SEE ALSO\n --------\n linkgit:git-init[1],\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 8023987..1868f69 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -48,6 +48,10 @@ struct checkout_opts {\n \tconst char *prefix;\n \tstruct pathspec pathspec;\n \tstruct tree *source_tree;\n+\n+\tconst char *new_worktree;\n+\tconst char **saved_argv;\n+\tint new_worktree_mode;\n };\n \n static int post_checkout_hook(struct commit *old, struct commit *new,\n@@ -250,6 +254,9 @@ static int checkout_paths(const struct checkout_opts *opts,\n \t\tdie(_(\"Cannot update paths and switch to branch '%s' at the same time.\"),\n \t\t    opts->new_branch);\n \n+\tif (opts->new_worktree)\n+\t\tdie(_(\"'%s' cannot be used with updating paths\"), \"--to\");\n+\n \tif (opts->patch_mode)\n \t\treturn run_add_interactive(revision, \"--patch=checkout\",\n \t\t\t\t\t   &opts->pathspec);\n@@ -485,7 +492,7 @@ static int merge_working_tree(const struct checkout_opts *opts,\n \t\t\ttopts.dir->flags |= DIR_SHOW_IGNORED;\n \t\t\tsetup_standard_excludes(topts.dir);\n \t\t}\n-\t\ttree = parse_tree_indirect(old->commit ?\n+\t\ttree = parse_tree_indirect(old->commit && !opts->new_worktree_mode ?\n \t\t\t\t\t   old->commit->object.sha1 :\n \t\t\t\t\t   EMPTY_TREE_SHA1_BIN);\n \t\tinit_tree_desc(&trees[0], tree->buffer, tree->size);\n@@ -796,7 +803,8 @@ static int switch_branches(const struct checkout_opts *opts,\n \t\treturn ret;\n \t}\n \n-\tif (!opts->quiet && !old.path && old.commit && new->commit != old.commit)\n+\tif (!opts->quiet && !old.path && old.commit &&\n+\t    new->commit != old.commit && !opts->new_worktree_mode)\n \t\torphaned_commit_warning(old.commit, new->commit);\n \n \tupdate_refs_for_switch(opts, &old, new);\n@@ -806,6 +814,76 @@ static int switch_branches(const struct checkout_opts *opts,\n \treturn ret || writeout_error;\n }\n \n+static int prepare_linked_checkout(const struct checkout_opts *opts,\n+\t\t\t\t   struct branch_info *new)\n+{\n+\tstruct strbuf sb_git = STRBUF_INIT, sb_repo = STRBUF_INIT;\n+\tstruct strbuf sb = STRBUF_INIT;\n+\tconst char *path = opts->new_worktree, *name;\n+\tstruct stat st;\n+\tstruct child_process cp;\n+\tint counter = 0, len;\n+\n+\tif (!new->commit)\n+\t\tdie(_(\"no branch specified\"));\n+\tif (file_exists(path))\n+\t\tdie(_(\"'%s' already exists\"), path);\n+\n+\tlen = strlen(path);\n+\twhile (len && is_dir_sep(path[len - 1]))\n+\t\tlen--;\n+\n+\tfor (name = path + len - 1; name > path; name--)\n+\t\tif (is_dir_sep(*name)) {\n+\t\t\tname++;\n+\t\t\tbreak;\n+\t\t}\n+\tstrbuf_addstr(&sb_repo,\n+\t\t      git_path(\"worktrees/%.*s\", (int)(path + len - name), name));\n+\tlen = sb_repo.len;\n+\tif (safe_create_leading_directories_const(sb_repo.buf))\n+\t\tdie_errno(_(\"could not create leading directories of '%s'\"),\n+\t\t\t  sb_repo.buf);\n+\twhile (!stat(sb_repo.buf, &st)) {\n+\t\tcounter++;\n+\t\tstrbuf_setlen(&sb_repo, len);\n+\t\tstrbuf_addf(&sb_repo, \"%d\", counter);\n+\t}\n+\tname = strrchr(sb_repo.buf, '/') + 1;\n+\tif (mkdir(sb_repo.buf, 0777))\n+\t\tdie_errno(_(\"could not create directory of '%s'\"), sb_repo.buf);\n+\n+\tstrbuf_addf(&sb_git, \"%s/.git\", path);\n+\tif (safe_create_leading_directories_const(sb_git.buf))\n+\t\tdie_errno(_(\"could not create leading directories of '%s'\"),\n+\t\t\t  sb_git.buf);\n+\n+\twrite_file(sb_git.buf, 1, \"gitdir: %s/worktrees/%s\\n\",\n+\t\t   real_path(get_git_common_dir()), name);\n+\t/*\n+\t * This is to keep resolve_ref() happy. We need a valid HEAD\n+\t * or is_git_directory() will reject the directory. Any valid\n+\t * value would do because this value will be ignored and\n+\t * replaced at the next (real) checkout.\n+\t */\n+\tstrbuf_addf(&sb, \"%s/HEAD\", sb_repo.buf);\n+\twrite_file(sb.buf, 1, \"%s\\n\", sha1_to_hex(new->commit->object.sha1));\n+\tstrbuf_reset(&sb);\n+\tstrbuf_addf(&sb, \"%s/commondir\", sb_repo.buf);\n+\twrite_file(sb.buf, 1, \"../..\\n\");\n+\n+\tif (!opts->quiet)\n+\t\tfprintf_ln(stderr, _(\"Enter %s (identifier %s)\"), path, name);\n+\n+\tsetenv(\"GIT_CHECKOUT_NEW_WORKTREE\", \"1\", 1);\n+\tsetenv(GIT_DIR_ENVIRONMENT, sb_git.buf, 1);\n+\tsetenv(GIT_WORK_TREE_ENVIRONMENT, path, 1);\n+\tmemset(&cp, 0, sizeof(cp));\n+\tcp.git_cmd = 1;\n+\tcp.argv = opts->saved_argv;\n+\treturn run_command(&cp);\n+}\n+\n static int git_checkout_config(const char *var, const char *value, void *cb)\n {\n \tif (!strcmp(var, \"diff.ignoresubmodules\")) {\n@@ -1067,6 +1145,9 @@ static int checkout_branch(struct checkout_opts *opts,\n \t\tdie(_(\"Cannot switch branch to a non-commit '%s'\"),\n \t\t    new->name);\n \n+\tif (opts->new_worktree)\n+\t\treturn prepare_linked_checkout(opts, new);\n+\n \tif (!new->commit && opts->new_branch) {\n \t\tunsigned char rev[20];\n \t\tint flag;\n@@ -1109,6 +1190,8 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)\n \t\t\t N_(\"do not limit pathspecs to sparse entries only\")),\n \t\tOPT_HIDDEN_BOOL(0, \"guess\", &dwim_new_local_branch,\n \t\t\t\tN_(\"second guess 'git checkout no-such-branch'\")),\n+\t\tOPT_FILENAME(0, \"to\", &opts.new_worktree,\n+\t\t\t   N_(\"check a branch out in a separate working directory\")),\n \t\tOPT_END(),\n \t};\n \n@@ -1117,6 +1200,9 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)\n \topts.overwrite_ignore = 1;\n \topts.prefix = prefix;\n \n+\topts.saved_argv = xmalloc(sizeof(const char *) * (argc + 2));\n+\tmemcpy(opts.saved_argv, argv, sizeof(const char *) * (argc + 1));\n+\n \tgitmodules_config();\n \tgit_config(git_checkout_config, &opts);\n \n@@ -1125,6 +1211,11 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)\n \targc = parse_options(argc, argv, prefix, options, checkout_usage,\n \t\t\t     PARSE_OPT_KEEP_DASHDASH);\n \n+\t/* recursive execution from checkout_new_worktree() */\n+\topts.new_worktree_mode = getenv(\"GIT_CHECKOUT_NEW_WORKTREE\") != NULL;\n+\tif (opts.new_worktree_mode)\n+\t\topts.new_worktree = NULL;\n+\n \tif (conflict_style) {\n \t\topts.merge = 1; /* implied */\n \t\tgit_xmerge_config(\"merge.conflictstyle\", conflict_style, NULL);\ndiff --git a/path.c b/path.c\nindex 8a6586c..1fd99f8 100644\n--- a/path.c\n+++ b/path.c\n@@ -92,7 +92,7 @@ static void replace_dir(struct strbuf *buf, int len, const char *newdir)\n \n static const char *common_list[] = {\n \t\"/branches\", \"/hooks\", \"/info\", \"/logs\", \"/lost-found\", \"/modules\",\n-\t\"/objects\", \"/refs\", \"/remotes\", \"/rr-cache\", \"/svn\",\n+\t\"/objects\", \"/refs\", \"/remotes\", \"/worktrees\", \"/rr-cache\", \"/svn\",\n \t\"config\", \"gc.pid\", \"packed-refs\", \"shallow\",\n \tNULL\n };\ndiff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh\nnew file mode 100755\nindex 0000000..8c73b18\n--- /dev/null\n+++ b/t/t2025-checkout-to.sh\n@@ -0,0 +1,63 @@\n+#!/bin/sh\n+\n+test_description='test git checkout --to'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'setup' '\n+\ttest_commit init\n+'\n+\n+test_expect_success 'checkout --to not updating paths' '\n+\ttest_must_fail git checkout --to -- init.t\n+'\n+\n+test_expect_success 'checkout --to an existing worktree' '\n+\tmkdir existing &&\n+\ttest_must_fail git checkout --detach --to existing master\n+'\n+\n+test_expect_success 'checkout --to a new worktree' '\n+\tgit checkout --to here master &&\n+\t(\n+\t\tcd here &&\n+\t\ttest_cmp ../init.t init.t &&\n+\t\tgit symbolic-ref HEAD >actual &&\n+\t\techo refs/heads/master >expect &&\n+\t\ttest_cmp expect actual &&\n+\t\tgit fsck\n+\t)\n+'\n+\n+test_expect_success 'checkout --to a new worktree from a subdir' '\n+\t(\n+\t\tmkdir sub &&\n+\t\tcd sub &&\n+\t\tgit checkout --detach --to here master &&\n+\t\tcd here &&\n+\t\ttest_cmp ../../init.t init.t\n+\t)\n+'\n+\n+test_expect_success 'checkout --to from a linked checkout' '\n+\t(\n+\t\tcd here &&\n+\t\tgit checkout --to nested-here master\n+\t\tcd nested-here &&\n+\t\tgit fsck\n+\t)\n+'\n+\n+test_expect_success 'checkout --to a new worktree creating new branch' '\n+\tgit checkout --to there -b newmaster master &&\n+\t(\n+\t\tcd there &&\n+\t\ttest_cmp ../init.t init.t &&\n+\t\tgit symbolic-ref HEAD >actual &&\n+\t\techo refs/heads/newmaster >expect &&\n+\t\ttest_cmp expect actual &&\n+\t\tgit fsck\n+\t)\n+'\n+\n+test_done\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249183","messageId":"1410388928-32265-24-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1410388928-32265-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v2 23/32] prune: strategies for linked checkouts","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-10T22:41:59Z","receivedAt":"2014-09-10T22:41:59Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"(alias R=$GIT_COMMON_DIR/worktrees/<id>)\n\n - linked checkouts are supposed to keep its location in $R/gitdir up\n   to date. The use case is auto fixup after a manual checkout move.\n\n - linked checkouts are supposed to update mtime of $R/gitdir. If\n   $R/gitdir's mtime is older than a limit, and it points to nowhere,\n   worktrees/<id> is to be pruned.\n\n - If $R/locked exists, worktrees/<id> is not supposed to be pruned. If\n   $R/locked exists and $R/gitdir's mtime is older than a really long\n   limit, warn about old unused repo.\n\n - \"git checkout --to\" is supposed to make a hard link named $R/link\n   pointing to the .git file on supported file systems to help detect\n   the user manually deleting the checkout. If $R/link exists and its\n   link count is greated than 1, the repo is kept.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n Documentation/git-checkout.txt             | 18 ++++++\n Documentation/git-prune.txt                |  3 +\n Documentation/gitrepository-layout.txt     | 19 ++++++\n builtin/checkout.c                         | 19 +++++-\n builtin/prune.c                            | 95 ++++++++++++++++++++++++++++++\n setup.c                                    | 13 ++++\n t/t2026-prune-linked-checkouts.sh (new +x) | 84 ++++++++++++++++++++++++++\n 7 files changed, 249 insertions(+), 2 deletions(-)\n create mode 100755 t/t2026-prune-linked-checkouts.sh\n\ndiff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt\nindex bd0fc1d..a29748e 100644\n--- a/Documentation/git-checkout.txt\n+++ b/Documentation/git-checkout.txt\n@@ -431,6 +431,24 @@ thumb is do not make any assumption about whether a path belongs to\n $GIT_DIR or $GIT_COMMON_DIR when you need to directly access something\n inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.\n \n+When you are done, simply deleting the linked working directory would\n+suffice. $GIT_DIR/worktrees can be cleaned up using `git prune\n+--worktrees`.\n+\n+After you move a linked working directory to another file system, or\n+on a file system that does not support hard link, execute any git\n+command (e.g. `git status`) in the new working directory so that it\n+could update its location in $GIT_DIR/worktrees and not be\n+accidentally pruned.\n+\n+To stop `git prune --worktrees` from deleting a specific working\n+directory (e.g. because it's on a portable device), you could add the\n+file 'locked' to $GIT_DIR/worktrees. For example, if `.git` file of\n+the new working directory points to `/path/main/worktrees/test-next`,\n+the full path of the 'locked' file would be\n+`/path/main/worktrees/test-next/locked`. See\n+linkgit:gitrepository-layout[5] for details.\n+\n EXAMPLES\n --------\n \ndiff --git a/Documentation/git-prune.txt b/Documentation/git-prune.txt\nindex 7a493c8..a0ea381 100644\n--- a/Documentation/git-prune.txt\n+++ b/Documentation/git-prune.txt\n@@ -48,6 +48,9 @@ OPTIONS\n --expire <time>::\n \tOnly expire loose objects older than <time>.\n \n+--worktrees::\n+\tPrune dead worktree information in $GIT_DIR/worktrees.\n+\n <head>...::\n \tIn addition to objects\n \treachable from any of our references, keep objects\ndiff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt\nindex a82780c..5ceff51 100644\n--- a/Documentation/gitrepository-layout.txt\n+++ b/Documentation/gitrepository-layout.txt\n@@ -255,6 +255,25 @@ worktrees::\n \t$GIT_COMMON_DIR is set and \"$GIT_COMMON_DIR/worktrees\" will be\n \tused instead.\n \n+worktrees/<id>/gitdir::\n+\tA text file containing the absolute path back to the .git file\n+\tthat points to here. This is used to check if the linked\n+\trepository has been manually removed and there is no need to\n+\tkeep this directory any more. mtime of this file should be\n+\tupdated every time the linked repository is accessed.\n+\n+worktrees/<id>/locked::\n+\tIf this file exists, the linked repository may be on a\n+\tportable device and not available. It does not mean that the\n+\tlinked repository is gone and `worktrees/<id>` could be\n+\tremoved. The file's content contains a reason string on why\n+\tthe repository is locked.\n+\n+worktrees/<id>/link::\n+\tIf this file exists, it is a hard link to the linked .git\n+\tfile. It is used to detect if the linked repository is\n+\tmanually removed.\n+\n SEE ALSO\n --------\n linkgit:git-init[1],\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 1868f69..b5ddf88 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -822,7 +822,7 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,\n \tconst char *path = opts->new_worktree, *name;\n \tstruct stat st;\n \tstruct child_process cp;\n-\tint counter = 0, len;\n+\tint counter = 0, len, ret;\n \n \tif (!new->commit)\n \t\tdie(_(\"no branch specified\"));\n@@ -853,11 +853,21 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,\n \tif (mkdir(sb_repo.buf, 0777))\n \t\tdie_errno(_(\"could not create directory of '%s'\"), sb_repo.buf);\n \n+\t/*\n+\t * lock the incomplete repo so prune won't delete it, unlock\n+\t * after the preparation is over.\n+\t */\n+\tstrbuf_addf(&sb, \"%s/locked\", sb_repo.buf);\n+\twrite_file(sb.buf, 1, \"initializing\\n\");\n+\n \tstrbuf_addf(&sb_git, \"%s/.git\", path);\n \tif (safe_create_leading_directories_const(sb_git.buf))\n \t\tdie_errno(_(\"could not create leading directories of '%s'\"),\n \t\t\t  sb_git.buf);\n \n+\tstrbuf_reset(&sb);\n+\tstrbuf_addf(&sb, \"%s/gitdir\", sb_repo.buf);\n+\twrite_file(sb.buf, 1, \"%s\\n\", real_path(sb_git.buf));\n \twrite_file(sb_git.buf, 1, \"gitdir: %s/worktrees/%s\\n\",\n \t\t   real_path(get_git_common_dir()), name);\n \t/*\n@@ -866,6 +876,7 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,\n \t * value would do because this value will be ignored and\n \t * replaced at the next (real) checkout.\n \t */\n+\tstrbuf_reset(&sb);\n \tstrbuf_addf(&sb, \"%s/HEAD\", sb_repo.buf);\n \twrite_file(sb.buf, 1, \"%s\\n\", sha1_to_hex(new->commit->object.sha1));\n \tstrbuf_reset(&sb);\n@@ -881,7 +892,11 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,\n \tmemset(&cp, 0, sizeof(cp));\n \tcp.git_cmd = 1;\n \tcp.argv = opts->saved_argv;\n-\treturn run_command(&cp);\n+\tret = run_command(&cp);\n+\tstrbuf_reset(&sb);\n+\tstrbuf_addf(&sb, \"%s/locked\", sb_repo.buf);\n+\tunlink_or_warn(sb.buf);\n+\treturn ret;\n }\n \n static int git_checkout_config(const char *var, const char *value, void *cb)\ndiff --git a/builtin/prune.c b/builtin/prune.c\nindex 144a3bd..cf56110 100644\n--- a/builtin/prune.c\n+++ b/builtin/prune.c\n@@ -112,6 +112,91 @@ static void prune_object_dir(const char *path)\n \t}\n }\n \n+static int prune_worktree(const char *id, struct strbuf *reason)\n+{\n+\tstruct stat st;\n+\tchar *path;\n+\tint fd, len;\n+\n+\tif (!is_directory(git_path(\"worktrees/%s\", id))) {\n+\t\tstrbuf_addf(reason, _(\"Removing worktrees/%s: not a valid directory\"), id);\n+\t\treturn 1;\n+\t}\n+\tif (file_exists(git_path(\"worktrees/%s/locked\", id)))\n+\t\treturn 0;\n+\tif (stat(git_path(\"worktrees/%s/gitdir\", id), &st)) {\n+\t\tstrbuf_addf(reason, _(\"Removing worktrees/%s: gitdir file does not exist\"), id);\n+\t\treturn 1;\n+\t}\n+\tfd = open(git_path(\"worktrees/%s/gitdir\", id), O_RDONLY);\n+\tif (fd < 0) {\n+\t\tstrbuf_addf(reason, _(\"Removing worktrees/%s: unable to read gitdir file (%s)\"),\n+\t\t\t    id, strerror(errno));\n+\t\treturn 1;\n+\t}\n+\tlen = st.st_size;\n+\tpath = xmalloc(len + 1);\n+\tread_in_full(fd, path, len);\n+\tclose(fd);\n+\twhile (len && (path[len - 1] == '\\n' || path[len - 1] == '\\r'))\n+\t\tlen--;\n+\tif (!len) {\n+\t\tstrbuf_addf(reason, _(\"Removing worktrees/%s: invalid gitdir file\"), id);\n+\t\tfree(path);\n+\t\treturn 1;\n+\t}\n+\tpath[len] = '\\0';\n+\tif (!file_exists(path)) {\n+\t\tstruct stat st_link;\n+\t\tfree(path);\n+\t\t/*\n+\t\t * the repo is moved manually and has not been\n+\t\t * accessed since?\n+\t\t */\n+\t\tif (!stat(git_path(\"worktrees/%s/link\", id), &st_link) &&\n+\t\t    st_link.st_nlink > 1)\n+\t\t\treturn 0;\n+\t\tstrbuf_addf(reason, _(\"Removing worktrees/%s: gitdir file points to non-existent location\"), id);\n+\t\treturn 1;\n+\t}\n+\tfree(path);\n+\treturn st.st_mtime <= expire;\n+}\n+\n+static void prune_worktrees(void)\n+{\n+\tstruct strbuf reason = STRBUF_INIT;\n+\tstruct strbuf path = STRBUF_INIT;\n+\tDIR *dir = opendir(git_path(\"worktrees\"));\n+\tstruct dirent *d;\n+\tint ret;\n+\tif (!dir)\n+\t\treturn;\n+\twhile ((d = readdir(dir)) != NULL) {\n+\t\tif (!strcmp(d->d_name, \".\") || !strcmp(d->d_name, \"..\"))\n+\t\t\tcontinue;\n+\t\tstrbuf_reset(&reason);\n+\t\tif (!prune_worktree(d->d_name, &reason))\n+\t\t\tcontinue;\n+\t\tif (show_only || verbose)\n+\t\t\tprintf(\"%s\\n\", reason.buf);\n+\t\tif (show_only)\n+\t\t\tcontinue;\n+\t\tstrbuf_reset(&path);\n+\t\tstrbuf_addstr(&path, git_path(\"worktrees/%s\", d->d_name));\n+\t\tret = remove_dir_recursively(&path, 0);\n+\t\tif (ret < 0 && errno == ENOTDIR)\n+\t\t\tret = unlink(path.buf);\n+\t\tif (ret)\n+\t\t\terror(_(\"failed to remove: %s\"), strerror(errno));\n+\t}\n+\tclosedir(dir);\n+\tif (!show_only)\n+\t\trmdir(git_path(\"worktrees\"));\n+\tstrbuf_release(&reason);\n+\tstrbuf_release(&path);\n+}\n+\n /*\n  * Write errors (particularly out of space) can result in\n  * failed temporary packs (and more rarely indexes and other\n@@ -138,10 +223,12 @@ int cmd_prune(int argc, const char **argv, const char *prefix)\n {\n \tstruct rev_info revs;\n \tstruct progress *progress = NULL;\n+\tint do_prune_worktrees = 0;\n \tconst struct option options[] = {\n \t\tOPT__DRY_RUN(&show_only, N_(\"do not remove, show only\")),\n \t\tOPT__VERBOSE(&verbose, N_(\"report pruned objects\")),\n \t\tOPT_BOOL(0, \"progress\", &show_progress, N_(\"show progress\")),\n+\t\tOPT_BOOL(0, \"worktrees\", &do_prune_worktrees, N_(\"prune .git/worktrees\")),\n \t\tOPT_EXPIRY_DATE(0, \"expire\", &expire,\n \t\t\t\tN_(\"expire objects older than <time>\")),\n \t\tOPT_END()\n@@ -154,6 +241,14 @@ int cmd_prune(int argc, const char **argv, const char *prefix)\n \tinit_revisions(&revs, prefix);\n \n \targc = parse_options(argc, argv, prefix, options, prune_usage, 0);\n+\n+\tif (do_prune_worktrees) {\n+\t\tif (argc)\n+\t\t\tdie(_(\"--worktrees does not take extra arguments\"));\n+\t\tprune_worktrees();\n+\t\treturn 0;\n+\t}\n+\n \twhile (argc--) {\n \t\tunsigned char sha1[20];\n \t\tconst char *name = *argv++;\ndiff --git a/setup.c b/setup.c\nindex 8f90bc3..da2d669 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -390,6 +390,17 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)\n \treturn ret;\n }\n \n+static void update_linked_gitdir(const char *gitfile, const char *gitdir)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\tstruct stat st;\n+\n+\tstrbuf_addf(&path, \"%s/gitfile\", gitdir);\n+\tif (stat(path.buf, &st) || st.st_mtime + 24 * 3600 < time(NULL))\n+\t\twrite_file(path.buf, 0, \"%s\\n\", gitfile);\n+\tstrbuf_release(&path);\n+}\n+\n /*\n  * Try to read the location of the git directory from the .git file,\n  * return path to git directory if found.\n@@ -438,6 +449,8 @@ const char *read_gitfile(const char *path)\n \n \tif (!is_git_directory(dir))\n \t\tdie(\"Not a git repository: %s\", dir);\n+\n+\tupdate_linked_gitdir(path, dir);\n \tpath = real_path(dir);\n \n \tfree(buf);\ndiff --git a/t/t2026-prune-linked-checkouts.sh b/t/t2026-prune-linked-checkouts.sh\nnew file mode 100755\nindex 0000000..3622800\n--- /dev/null\n+++ b/t/t2026-prune-linked-checkouts.sh\n@@ -0,0 +1,84 @@\n+#!/bin/sh\n+\n+test_description='prune $GIT_DIR/worktrees'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'prune --worktrees on normal repo' '\n+\tgit prune --worktrees &&\n+\ttest_must_fail git prune --worktrees abc\n+'\n+\n+test_expect_success 'prune files inside $GIT_DIR/worktrees' '\n+\tmkdir .git/worktrees &&\n+\t: >.git/worktrees/abc &&\n+\tgit prune --worktrees --verbose >actual &&\n+\tcat >expect <<EOF &&\n+Removing worktrees/abc: not a valid directory\n+EOF\n+\ttest_i18ncmp expect actual &&\n+\t! test -f .git/worktrees/abc &&\n+\t! test -d .git/worktrees\n+'\n+\n+test_expect_success 'prune directories without gitdir' '\n+\tmkdir -p .git/worktrees/def/abc &&\n+\t: >.git/worktrees/def/def &&\n+\tcat >expect <<EOF &&\n+Removing worktrees/def: gitdir file does not exist\n+EOF\n+\tgit prune --worktrees --verbose >actual &&\n+\ttest_i18ncmp expect actual &&\n+\t! test -d .git/worktrees/def &&\n+\t! test -d .git/worktrees\n+'\n+\n+test_expect_success POSIXPERM 'prune directories with unreadable gitdir' '\n+\tmkdir -p .git/worktrees/def/abc &&\n+\t: >.git/worktrees/def/def &&\n+\t: >.git/worktrees/def/gitdir &&\n+\tchmod u-r .git/worktrees/def/gitdir &&\n+\tgit prune --worktrees --verbose >actual &&\n+\ttest_i18ngrep \"Removing worktrees/def: unable to read gitdir file\" actual &&\n+\t! test -d .git/worktrees/def &&\n+\t! test -d .git/worktrees\n+'\n+\n+test_expect_success 'prune directories with invalid gitdir' '\n+\tmkdir -p .git/worktrees/def/abc &&\n+\t: >.git/worktrees/def/def &&\n+\t: >.git/worktrees/def/gitdir &&\n+\tgit prune --worktrees --verbose >actual &&\n+\ttest_i18ngrep \"Removing worktrees/def: invalid gitdir file\" actual &&\n+\t! test -d .git/worktrees/def &&\n+\t! test -d .git/worktrees\n+'\n+\n+test_expect_success 'prune directories with gitdir pointing to nowhere' '\n+\tmkdir -p .git/worktrees/def/abc &&\n+\t: >.git/worktrees/def/def &&\n+\techo \"$TRASH_DIRECTORY\"/nowhere >.git/worktrees/def/gitdir &&\n+\tgit prune --worktrees --verbose >actual &&\n+\ttest_i18ngrep \"Removing worktrees/def: gitdir file points to non-existent location\" actual &&\n+\t! test -d .git/worktrees/def &&\n+\t! test -d .git/worktrees\n+'\n+\n+test_expect_success 'not prune locked checkout' '\n+\ttest_when_finished rm -r .git/worktrees\n+\tmkdir -p .git/worktrees/ghi &&\n+\t: >.git/worktrees/ghi/locked &&\n+\tgit prune --worktrees &&\n+\ttest -d .git/worktrees/ghi\n+'\n+\n+test_expect_success 'not prune recent checkouts' '\n+\ttest_when_finished rm -r .git/worktrees\n+\tmkdir zz &&\n+\tmkdir -p .git/worktrees/jlm &&\n+\techo \"$TRASH_DIRECTORY\"/zz >.git/worktrees/jlm/gitdir &&\n+\tgit prune --worktrees --verbose --expire=2.days.ago &&\n+\ttest -d .git/worktrees/jlm\n+'\n+\n+test_done\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249184","messageId":"1410388928-32265-25-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1410388928-32265-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v2 24/32] checkout: reject if the branch is already checked out elsewhere","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-10T22:42:00Z","receivedAt":"2014-09-10T22:42:00Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"One branch obviously can't be checked out at two places (but detached\nheads are ok). Give the user a choice in this case: --detach, -b\nnew-branch, switch branch in the other checkout first or simply 'cd'\nand continue to work there.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n builtin/checkout.c     | 86 ++++++++++++++++++++++++++++++++++++++++++++++++--\n t/t2025-checkout-to.sh | 25 ++++++++++++---\n 2 files changed, 104 insertions(+), 7 deletions(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex b5ddf88..6d623fa 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -431,6 +431,11 @@ struct branch_info {\n \tconst char *name; /* The short name used */\n \tconst char *path; /* The full name of a real branch */\n \tstruct commit *commit; /* The named commit */\n+\t/*\n+\t * if not null the branch is detached because it's already\n+\t * checked out in this checkout\n+\t */\n+\tchar *checkout;\n };\n \n static void setup_branch_path(struct branch_info *branch)\n@@ -954,12 +959,78 @@ static const char *unique_tracking_name(const char *name, unsigned char *sha1)\n \treturn NULL;\n }\n \n+static void check_linked_checkout(struct branch_info *new, const char *id)\n+{\n+\tstruct strbuf sb = STRBUF_INIT;\n+\tstruct strbuf path = STRBUF_INIT;\n+\tstruct strbuf gitdir = STRBUF_INIT;\n+\tconst char *start, *end;\n+\n+\tif (id)\n+\t\tstrbuf_addf(&path, \"%s/worktrees/%s/HEAD\", get_git_common_dir(), id);\n+\telse\n+\t\tstrbuf_addf(&path, \"%s/HEAD\", get_git_common_dir());\n+\n+\tif (strbuf_read_file(&sb, path.buf, 0) < 0 ||\n+\t    !skip_prefix(sb.buf, \"ref:\", &start))\n+\t\tgoto done;\n+\twhile (isspace(*start))\n+\t\tstart++;\n+\tend = start;\n+\twhile (*end && !isspace(*end))\n+\t\tend++;\n+\tif (strncmp(start, new->path, end - start) || new->path[end - start] != '\\0')\n+\t\tgoto done;\n+\tif (id) {\n+\t\tstrbuf_reset(&path);\n+\t\tstrbuf_addf(&path, \"%s/worktrees/%s/gitdir\", get_git_common_dir(), id);\n+\t\tif (strbuf_read_file(&gitdir, path.buf, 0) <= 0)\n+\t\t\tgoto done;\n+\t\tstrbuf_rtrim(&gitdir);\n+\t} else\n+\t\tstrbuf_addstr(&gitdir, get_git_common_dir());\n+\tdie(_(\"'%s' is already checked out at '%s'\"), new->name, gitdir.buf);\n+done:\n+\tstrbuf_release(&path);\n+\tstrbuf_release(&sb);\n+\tstrbuf_release(&gitdir);\n+}\n+\n+static void check_linked_checkouts(struct branch_info *new)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\tDIR *dir;\n+\tstruct dirent *d;\n+\n+\tstrbuf_addf(&path, \"%s/worktrees\", get_git_common_dir());\n+\tif ((dir = opendir(path.buf)) == NULL) {\n+\t\tstrbuf_release(&path);\n+\t\treturn;\n+\t}\n+\n+\t/*\n+\t * $GIT_COMMON_DIR/HEAD is practically outside\n+\t * $GIT_DIR so resolve_ref_unsafe() won't work (it\n+\t * uses git_path). Parse the ref ourselves.\n+\t */\n+\tcheck_linked_checkout(new, NULL);\n+\n+\twhile ((d = readdir(dir)) != NULL) {\n+\t\tif (!strcmp(d->d_name, \".\") || !strcmp(d->d_name, \"..\"))\n+\t\t\tcontinue;\n+\t\tcheck_linked_checkout(new, d->d_name);\n+\t}\n+\tstrbuf_release(&path);\n+\tclosedir(dir);\n+}\n+\n static int parse_branchname_arg(int argc, const char **argv,\n \t\t\t\tint dwim_new_local_branch_ok,\n \t\t\t\tstruct branch_info *new,\n \t\t\t\tstruct tree **source_tree,\n \t\t\t\tunsigned char rev[20],\n-\t\t\t\tconst char **new_branch)\n+\t\t\t\tconst char **new_branch,\n+\t\t\t\tint force_detach)\n {\n \tint argcount = 0;\n \tunsigned char branch_rev[20];\n@@ -1081,6 +1152,16 @@ static int parse_branchname_arg(int argc, const char **argv,\n \telse\n \t\tnew->path = NULL; /* not an existing branch */\n \n+\tif (new->path && !force_detach && !*new_branch) {\n+\t\tunsigned char sha1[20];\n+\t\tint flag;\n+\t\tchar *head_ref = resolve_refdup(\"HEAD\", sha1, 0, &flag);\n+\t\tif (head_ref &&\n+\t\t    (!(flag & REF_ISSYMREF) || strcmp(head_ref, new->path)))\n+\t\t\tcheck_linked_checkouts(new);\n+\t\tfree(head_ref);\n+\t}\n+\n \tnew->commit = lookup_commit_reference_gently(rev, 1);\n \tif (!new->commit) {\n \t\t/* not a commit */\n@@ -1287,7 +1368,8 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)\n \t\t\t!opts.new_branch;\n \t\tint n = parse_branchname_arg(argc, argv, dwim_ok,\n \t\t\t\t\t     &new, &opts.source_tree,\n-\t\t\t\t\t     rev, &opts.new_branch);\n+\t\t\t\t\t     rev, &opts.new_branch,\n+\t\t\t\t\t     opts.force_detach);\n \t\targv += n;\n \t\targc -= n;\n \t}\ndiff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh\nindex 8c73b18..afa8a62 100755\n--- a/t/t2025-checkout-to.sh\n+++ b/t/t2025-checkout-to.sh\n@@ -18,13 +18,14 @@ test_expect_success 'checkout --to an existing worktree' '\n '\n \n test_expect_success 'checkout --to a new worktree' '\n-\tgit checkout --to here master &&\n+\tgit rev-parse HEAD >expect &&\n+\tgit checkout --detach --to here master &&\n \t(\n \t\tcd here &&\n \t\ttest_cmp ../init.t init.t &&\n-\t\tgit symbolic-ref HEAD >actual &&\n-\t\techo refs/heads/master >expect &&\n-\t\ttest_cmp expect actual &&\n+\t\ttest_must_fail git symbolic-ref HEAD &&\n+\t\tgit rev-parse HEAD >actual &&\n+\t\ttest_cmp ../expect actual &&\n \t\tgit fsck\n \t)\n '\n@@ -42,7 +43,7 @@ test_expect_success 'checkout --to a new worktree from a subdir' '\n test_expect_success 'checkout --to from a linked checkout' '\n \t(\n \t\tcd here &&\n-\t\tgit checkout --to nested-here master\n+\t\tgit checkout --detach --to nested-here master\n \t\tcd nested-here &&\n \t\tgit fsck\n \t)\n@@ -60,4 +61,18 @@ test_expect_success 'checkout --to a new worktree creating new branch' '\n \t)\n '\n \n+test_expect_success 'die the same branch is already checked out' '\n+\t(\n+\t\tcd here &&\n+\t\ttest_must_fail git checkout newmaster\n+\t)\n+'\n+\n+test_expect_success 'not die on re-checking out current branch' '\n+\t(\n+\t\tcd there &&\n+\t\tgit checkout newmaster\n+\t)\n+'\n+\n test_done\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249185","messageId":"1410388928-32265-26-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1410388928-32265-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v2 25/32] checkout: clean up half-prepared directories in --to mode","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-10T22:42:01Z","receivedAt":"2014-09-10T22:42:01Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n builtin/checkout.c     | 48 ++++++++++++++++++++++++++++++++++++++++++++++++\n t/t2025-checkout-to.sh |  6 ++++++\n 2 files changed, 54 insertions(+)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 6d623fa..f419ddf 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -20,6 +20,7 @@\n #include \"resolve-undo.h\"\n #include \"submodule.h\"\n #include \"argv-array.h\"\n+#include \"sigchain.h\"\n \n static const char * const checkout_usage[] = {\n \tN_(\"git checkout [options] <branch>\"),\n@@ -819,6 +820,35 @@ static int switch_branches(const struct checkout_opts *opts,\n \treturn ret || writeout_error;\n }\n \n+static char *junk_work_tree;\n+static char *junk_git_dir;\n+static int is_junk;\n+static pid_t junk_pid;\n+\n+static void remove_junk(void)\n+{\n+\tstruct strbuf sb = STRBUF_INIT;\n+\tif (!is_junk || getpid() != junk_pid)\n+\t\treturn;\n+\tif (junk_git_dir) {\n+\t\tstrbuf_addstr(&sb, junk_git_dir);\n+\t\tremove_dir_recursively(&sb, 0);\n+\t\tstrbuf_reset(&sb);\n+\t}\n+\tif (junk_work_tree) {\n+\t\tstrbuf_addstr(&sb, junk_work_tree);\n+\t\tremove_dir_recursively(&sb, 0);\n+\t}\n+\tstrbuf_release(&sb);\n+}\n+\n+static void remove_junk_on_signal(int signo)\n+{\n+\tremove_junk();\n+\tsigchain_pop(signo);\n+\traise(signo);\n+}\n+\n static int prepare_linked_checkout(const struct checkout_opts *opts,\n \t\t\t\t   struct branch_info *new)\n {\n@@ -855,8 +885,15 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,\n \t\tstrbuf_addf(&sb_repo, \"%d\", counter);\n \t}\n \tname = strrchr(sb_repo.buf, '/') + 1;\n+\n+\tjunk_pid = getpid();\n+\tatexit(remove_junk);\n+\tsigchain_push_common(remove_junk_on_signal);\n+\n \tif (mkdir(sb_repo.buf, 0777))\n \t\tdie_errno(_(\"could not create directory of '%s'\"), sb_repo.buf);\n+\tjunk_git_dir = xstrdup(sb_repo.buf);\n+\tis_junk = 1;\n \n \t/*\n \t * lock the incomplete repo so prune won't delete it, unlock\n@@ -869,6 +906,7 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,\n \tif (safe_create_leading_directories_const(sb_git.buf))\n \t\tdie_errno(_(\"could not create leading directories of '%s'\"),\n \t\t\t  sb_git.buf);\n+\tjunk_work_tree = xstrdup(path);\n \n \tstrbuf_reset(&sb);\n \tstrbuf_addf(&sb, \"%s/gitdir\", sb_repo.buf);\n@@ -898,9 +936,19 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,\n \tcp.git_cmd = 1;\n \tcp.argv = opts->saved_argv;\n \tret = run_command(&cp);\n+\tif (!ret) {\n+\t\tis_junk = 0;\n+\t\tfree(junk_work_tree);\n+\t\tfree(junk_git_dir);\n+\t\tjunk_work_tree = NULL;\n+\t\tjunk_git_dir = NULL;\n+\t}\n \tstrbuf_reset(&sb);\n \tstrbuf_addf(&sb, \"%s/locked\", sb_repo.buf);\n \tunlink_or_warn(sb.buf);\n+\tstrbuf_release(&sb);\n+\tstrbuf_release(&sb_repo);\n+\tstrbuf_release(&sb_git);\n \treturn ret;\n }\n \ndiff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh\nindex afa8a62..5d28f9e 100755\n--- a/t/t2025-checkout-to.sh\n+++ b/t/t2025-checkout-to.sh\n@@ -17,6 +17,12 @@ test_expect_success 'checkout --to an existing worktree' '\n \ttest_must_fail git checkout --detach --to existing master\n '\n \n+test_expect_success 'checkout --to refuses to checkout locked branch' '\n+\ttest_must_fail git checkout --to zere master &&\n+\t! test -d zere &&\n+\t! test -d .git/worktrees/zere\n+'\n+\n test_expect_success 'checkout --to a new worktree' '\n \tgit rev-parse HEAD >expect &&\n \tgit checkout --detach --to here master &&\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249186","messageId":"1410388928-32265-27-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1410388928-32265-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v2 26/32] gc: style change -- no SP before closing parenthesis","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-10T22:42:02Z","receivedAt":"2014-09-10T22:42:02Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n builtin/gc.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 8d219d8..3bfb990 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -298,7 +298,7 @@ int cmd_gc(int argc, const char **argv, const char *prefix)\n \targv_array_pushl(&pack_refs_cmd, \"pack-refs\", \"--all\", \"--prune\", NULL);\n \targv_array_pushl(&reflog, \"reflog\", \"expire\", \"--all\", NULL);\n \targv_array_pushl(&repack, \"repack\", \"-d\", \"-l\", NULL);\n-\targv_array_pushl(&prune, \"prune\", \"--expire\", NULL );\n+\targv_array_pushl(&prune, \"prune\", \"--expire\", NULL);\n \targv_array_pushl(&rerere, \"rerere\", \"gc\", NULL);\n \n \tgit_config(gc_config, NULL);\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249187","messageId":"1410388928-32265-28-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1410388928-32265-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v2 27/32] gc: factor out gc.pruneexpire parsing code","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-10T22:42:03Z","receivedAt":"2014-09-10T22:42:03Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n builtin/gc.c | 21 +++++++++++++--------\n 1 file changed, 13 insertions(+), 8 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 3bfb990..e38c902 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -55,6 +55,17 @@ static void remove_pidfile_on_signal(int signo)\n \traise(signo);\n }\n \n+static int git_config_date_string(const char **output,\n+\t\t\t\t  const char *var, const char *value)\n+{\n+\tif (value && strcmp(value, \"now\")) {\n+\t\tunsigned long now = approxidate(\"now\");\n+\t\tif (approxidate(value) >= now)\n+\t\t\treturn error(_(\"Invalid %s: '%s'\"), var, value);\n+\t}\n+\treturn git_config_string(output, var, value);\n+}\n+\n static int gc_config(const char *var, const char *value, void *cb)\n {\n \tif (!strcmp(var, \"gc.packrefs\")) {\n@@ -84,14 +95,8 @@ static int gc_config(const char *var, const char *value, void *cb)\n \t\tdetach_auto = git_config_bool(var, value);\n \t\treturn 0;\n \t}\n-\tif (!strcmp(var, \"gc.pruneexpire\")) {\n-\t\tif (value && strcmp(value, \"now\")) {\n-\t\t\tunsigned long now = approxidate(\"now\");\n-\t\t\tif (approxidate(value) >= now)\n-\t\t\t\treturn error(_(\"Invalid %s: '%s'\"), var, value);\n-\t\t}\n-\t\treturn git_config_string(&prune_expire, var, value);\n-\t}\n+\tif (!strcmp(var, \"gc.pruneexpire\"))\n+\t\treturn git_config_date_string(&prune_expire, var, value);\n \treturn git_default_config(var, value, cb);\n }\n \n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249188","messageId":"1410388928-32265-29-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1410388928-32265-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v2 28/32] gc: support prune --worktrees","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-10T22:42:04Z","receivedAt":"2014-09-10T22:42:04Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n Documentation/config.txt       |  7 +++++++\n Documentation/git-checkout.txt |  2 +-\n builtin/gc.c                   | 11 +++++++++++\n 3 files changed, 19 insertions(+), 1 deletion(-)\n\ndiff --git a/Documentation/config.txt b/Documentation/config.txt\nindex 286e539..a85f684 100644\n--- a/Documentation/config.txt\n+++ b/Documentation/config.txt\n@@ -1211,6 +1211,13 @@ gc.pruneexpire::\n \t\"now\" may be used to disable this  grace period and always prune\n \tunreachable objects immediately.\n \n+gc.pruneworktreesexpire::\n+\tWhen 'git gc' is run, it will call\n+\t'prune --worktrees --expire 3.months.ago'.\n+\tOverride the grace period with this config variable. The value\n+\t\"now\" may be used to disable the grace period and prune\n+\t$GIT_DIR/worktrees immediately.\n+\n gc.reflogexpire::\n gc.<pattern>.reflogexpire::\n \t'git reflog expire' removes reflog entries older than\ndiff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt\nindex a29748e..23f0c80 100644\n--- a/Documentation/git-checkout.txt\n+++ b/Documentation/git-checkout.txt\n@@ -433,7 +433,7 @@ inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.\n \n When you are done, simply deleting the linked working directory would\n suffice. $GIT_DIR/worktrees can be cleaned up using `git prune\n---worktrees`.\n+--worktrees`, which is part of automated garbage collection.\n \n After you move a linked working directory to another file system, or\n on a file system that does not support hard link, execute any git\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex e38c902..00239ca 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -33,11 +33,13 @@ static int gc_auto_threshold = 6700;\n static int gc_auto_pack_limit = 50;\n static int detach_auto = 1;\n static const char *prune_expire = \"2.weeks.ago\";\n+static const char *prune_worktrees_expire = \"3.months.ago\";\n \n static struct argv_array pack_refs_cmd = ARGV_ARRAY_INIT;\n static struct argv_array reflog = ARGV_ARRAY_INIT;\n static struct argv_array repack = ARGV_ARRAY_INIT;\n static struct argv_array prune = ARGV_ARRAY_INIT;\n+static struct argv_array prune_worktrees = ARGV_ARRAY_INIT;\n static struct argv_array rerere = ARGV_ARRAY_INIT;\n \n static char *pidfile;\n@@ -97,6 +99,8 @@ static int gc_config(const char *var, const char *value, void *cb)\n \t}\n \tif (!strcmp(var, \"gc.pruneexpire\"))\n \t\treturn git_config_date_string(&prune_expire, var, value);\n+\tif (!strcmp(var, \"gc.pruneworktreesexpire\"))\n+\t\treturn git_config_date_string(&prune_worktrees_expire, var, value);\n \treturn git_default_config(var, value, cb);\n }\n \n@@ -304,6 +308,7 @@ int cmd_gc(int argc, const char **argv, const char *prefix)\n \targv_array_pushl(&reflog, \"reflog\", \"expire\", \"--all\", NULL);\n \targv_array_pushl(&repack, \"repack\", \"-d\", \"-l\", NULL);\n \targv_array_pushl(&prune, \"prune\", \"--expire\", NULL);\n+\targv_array_pushl(&prune_worktrees, \"prune\", \"--worktrees\", \"--expire\", NULL);\n \targv_array_pushl(&rerere, \"rerere\", \"gc\", NULL);\n \n \tgit_config(gc_config, NULL);\n@@ -373,6 +378,12 @@ int cmd_gc(int argc, const char **argv, const char *prefix)\n \t\t\treturn error(FAILED_RUN, prune.argv[0]);\n \t}\n \n+\tif (prune_worktrees_expire) {\n+\t\targv_array_push(&prune_worktrees, prune_worktrees_expire);\n+\t\tif (run_command_v_opt(prune_worktrees.argv, RUN_GIT_CMD))\n+\t\t\treturn error(FAILED_RUN, prune_worktrees.argv[0]);\n+\t}\n+\n \tif (run_command_v_opt(rerere.argv, RUN_GIT_CMD))\n \t\treturn error(FAILED_RUN, rerere.argv[0]);\n \n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249189","messageId":"1410388928-32265-30-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1410388928-32265-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v2 29/32] count-objects: report unused files in $GIT_DIR/worktrees/...","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-10T22:42:05Z","receivedAt":"2014-09-10T22:42:05Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"In linked checkouts, borrowed parts like config is taken from\n$GIT_COMMON_DIR. $GIT_DIR/config is never used. Report them as\ngarbage.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n builtin/count-objects.c |  4 +++-\n cache.h                 |  1 +\n path.c                  | 29 +++++++++++++++++++++++++++--\n 3 files changed, 31 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/count-objects.c b/builtin/count-objects.c\nindex a7f70cb..d3a1620 100644\n--- a/builtin/count-objects.c\n+++ b/builtin/count-objects.c\n@@ -102,8 +102,10 @@ int cmd_count_objects(int argc, const char **argv, const char *prefix)\n \t/* we do not take arguments other than flags for now */\n \tif (argc)\n \t\tusage_with_options(count_objects_usage, opts);\n-\tif (verbose)\n+\tif (verbose) {\n \t\treport_garbage = real_report_garbage;\n+\t\treport_linked_checkout_garbage();\n+\t}\n \tmemcpy(path, objdir, len);\n \tif (len && objdir[len-1] != '/')\n \t\tpath[len++] = '/';\ndiff --git a/cache.h b/cache.h\nindex bf4d15e..12f0fc0 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -690,6 +690,7 @@ extern const char *mkpath(const char *fmt, ...) __attribute__((format (printf, 1\n extern const char *git_path(const char *fmt, ...) __attribute__((format (printf, 1, 2)));\n extern const char *git_path_submodule(const char *path, const char *fmt, ...)\n \t__attribute__((format (printf, 2, 3)));\n+extern void report_linked_checkout_garbage(void);\n \n /*\n  * Return the name of the file in the local object database that would\ndiff --git a/path.c b/path.c\nindex 1fd99f8..d07ae69 100644\n--- a/path.c\n+++ b/path.c\n@@ -4,6 +4,7 @@\n #include \"cache.h\"\n #include \"strbuf.h\"\n #include \"string-list.h\"\n+#include \"dir.h\"\n \n static int get_st_mode_bits(const char *path, int *mode)\n {\n@@ -91,9 +92,9 @@ static void replace_dir(struct strbuf *buf, int len, const char *newdir)\n }\n \n static const char *common_list[] = {\n-\t\"/branches\", \"/hooks\", \"/info\", \"/logs\", \"/lost-found\", \"/modules\",\n+\t\"/branches\", \"/hooks\", \"/info\", \"!/logs\", \"/lost-found\", \"/modules\",\n \t\"/objects\", \"/refs\", \"/remotes\", \"/worktrees\", \"/rr-cache\", \"/svn\",\n-\t\"config\", \"gc.pid\", \"packed-refs\", \"shallow\",\n+\t\"config\", \"!gc.pid\", \"packed-refs\", \"shallow\",\n \tNULL\n };\n \n@@ -107,6 +108,8 @@ static void update_common_dir(struct strbuf *buf, int git_dir_len)\n \tfor (p = common_list; *p; p++) {\n \t\tconst char *path = *p;\n \t\tint is_dir = 0;\n+\t\tif (*path == '!')\n+\t\t\tpath++;\n \t\tif (*path == '/') {\n \t\t\tpath++;\n \t\t\tis_dir = 1;\n@@ -122,6 +125,28 @@ static void update_common_dir(struct strbuf *buf, int git_dir_len)\n \t}\n }\n \n+void report_linked_checkout_garbage(void)\n+{\n+\tstruct strbuf sb = STRBUF_INIT;\n+\tconst char **p;\n+\tint len;\n+\n+\tif (!git_common_dir_env)\n+\t\treturn;\n+\tstrbuf_addf(&sb, \"%s/\", get_git_dir());\n+\tlen = sb.len;\n+\tfor (p = common_list; *p; p++) {\n+\t\tconst char *path = *p;\n+\t\tif (*path == '!')\n+\t\t\tcontinue;\n+\t\tstrbuf_setlen(&sb, len);\n+\t\tstrbuf_addstr(&sb, path);\n+\t\tif (file_exists(sb.buf))\n+\t\t\treport_garbage(\"unused in linked checkout\", sb.buf);\n+\t}\n+\tstrbuf_release(&sb);\n+}\n+\n static void adjust_git_path(struct strbuf *buf, int git_dir_len)\n {\n \tconst char *base = buf->buf + git_dir_len;\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249190","messageId":"1410388928-32265-31-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1410388928-32265-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v2 30/32] git_path(): keep \"info/sparse-checkout\" per work-tree","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-10T22:42:06Z","receivedAt":"2014-09-10T22:42:06Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Currently git_path(\"info/sparse-checkout\") resolves to\n$GIT_COMMON_DIR/info/sparse-checkout in multiple worktree mode. It\nmakes more sense for the sparse checkout patterns to be per worktree,\nso you can have multiple checkouts with different parts of the tree.\n\nWith this, \"git checkout --to <new>\" on a sparse checkout will create\n<new> as a full checkout. Which is expected, it's how a new checkout\nis made. The user can reshape the worktree afterwards.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n path.c                | 3 ++-\n t/t0060-path-utils.sh | 1 +\n 2 files changed, 3 insertions(+), 1 deletion(-)\n\ndiff --git a/path.c b/path.c\nindex d07ae69..1f1881a 100644\n--- a/path.c\n+++ b/path.c\n@@ -103,7 +103,8 @@ static void update_common_dir(struct strbuf *buf, int git_dir_len)\n \tchar *base = buf->buf + git_dir_len;\n \tconst char **p;\n \n-\tif (is_dir_file(base, \"logs\", \"HEAD\"))\n+\tif (is_dir_file(base, \"logs\", \"HEAD\") ||\n+\t    is_dir_file(base, \"info\", \"sparse-checkout\"))\n \t\treturn;\t/* keep this in $GIT_DIR */\n \tfor (p = common_list; *p; p++) {\n \t\tconst char *path = *p;\ndiff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh\nindex f5d6f80..93605f4 100755\n--- a/t/t0060-path-utils.sh\n+++ b/t/t0060-path-utils.sh\n@@ -270,6 +270,7 @@ test_git_path GIT_COMMON_DIR=bar objects                  bar/objects\n test_git_path GIT_COMMON_DIR=bar objects/bar              bar/objects/bar\n test_git_path GIT_COMMON_DIR=bar info/exclude             bar/info/exclude\n test_git_path GIT_COMMON_DIR=bar info/grafts              bar/info/grafts\n+test_git_path GIT_COMMON_DIR=bar info/sparse-checkout     .git/info/sparse-checkout\n test_git_path GIT_COMMON_DIR=bar remotes/bar              bar/remotes/bar\n test_git_path GIT_COMMON_DIR=bar branches/bar             bar/branches/bar\n test_git_path GIT_COMMON_DIR=bar logs/refs/heads/master   bar/logs/refs/heads/master\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249191","messageId":"1410388928-32265-32-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1410388928-32265-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v2 31/32] checkout: don't require a work tree when checking out into a new one","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-10T22:42:07Z","receivedAt":"2014-09-10T22:42:07Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"From: Dennis Kaarsemaker <dennis@kaarsemaker.net>\n\nFor normal use cases, it does not make sense for 'checkout' to work on\na bare repository, without a worktree. But \"checkout --to\" is an\nexception because it _creates_ a new worktree. Allow this option to\nrun on bare repositories.\n\nPeople who check out from a bare repository should remember that\ncore.logallrefupdates is off by default and it should be turned back\non. `--to` cannot do this automatically behind the user's back because\nsome user may deliberately want no reflog.\n\nFor people interested in repository setup/discovery code,\nis_bare_repository_cfg (aka \"core.bare\") is unchanged by this patch,\nwhich means 'true' by default for bare repos. Fortunately when we get\nthe repo through a linked checkout, is_bare_repository_cfg is never\nused. So all is still good.\n\n[nd: commit message]\n\nSigned-off-by: Dennis Kaarsemaker <dennis@kaarsemaker.net>\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n builtin/checkout.c     |  3 +++\n git.c                  |  2 +-\n t/t2025-checkout-to.sh | 15 +++++++++++++++\n 3 files changed, 19 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex f419ddf..6106b81 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -1360,6 +1360,9 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)\n \tif (opts.new_worktree_mode)\n \t\topts.new_worktree = NULL;\n \n+\tif (!opts.new_worktree)\n+\t\tsetup_work_tree();\n+\n \tif (conflict_style) {\n \t\topts.merge = 1; /* implied */\n \t\tgit_xmerge_config(\"merge.conflictstyle\", conflict_style, NULL);\ndiff --git a/git.c b/git.c\nindex 5b6c761..7426651 100644\n--- a/git.c\n+++ b/git.c\n@@ -383,7 +383,7 @@ static struct cmd_struct commands[] = {\n \t{ \"check-ignore\", cmd_check_ignore, RUN_SETUP | NEED_WORK_TREE },\n \t{ \"check-mailmap\", cmd_check_mailmap, RUN_SETUP },\n \t{ \"check-ref-format\", cmd_check_ref_format },\n-\t{ \"checkout\", cmd_checkout, RUN_SETUP | NEED_WORK_TREE },\n+\t{ \"checkout\", cmd_checkout, RUN_SETUP },\n \t{ \"checkout-index\", cmd_checkout_index,\n \t\tRUN_SETUP | NEED_WORK_TREE},\n \t{ \"cherry\", cmd_cherry, RUN_SETUP },\ndiff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh\nindex 5d28f9e..2cddbf1 100755\n--- a/t/t2025-checkout-to.sh\n+++ b/t/t2025-checkout-to.sh\n@@ -81,4 +81,19 @@ test_expect_success 'not die on re-checking out current branch' '\n \t)\n '\n \n+test_expect_success 'checkout --to from a bare repo' '\n+\t(\n+\t\tgit clone --bare . bare &&\n+\t\tcd bare &&\n+\t\tgit checkout --to ../there2 -b bare-master master\n+\t)\n+'\n+\n+test_expect_success 'checkout from a bare repo without --to' '\n+\t(\n+\t\tcd bare &&\n+\t\ttest_must_fail git checkout master\n+\t)\n+'\n+\n test_done\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249192","messageId":"1410388928-32265-33-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1410388928-32265-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v2 32/32] t2025: add a test to make sure grafts is working from a linked checkout","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-10T22:42:08Z","receivedAt":"2014-09-10T22:42:08Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n t/t2025-checkout-to.sh | 18 ++++++++++++++++++\n 1 file changed, 18 insertions(+)\n\ndiff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh\nindex 2cddbf1..27384a1 100755\n--- a/t/t2025-checkout-to.sh\n+++ b/t/t2025-checkout-to.sh\n@@ -96,4 +96,22 @@ test_expect_success 'checkout from a bare repo without --to' '\n \t)\n '\n \n+test_expect_success 'checkout with grafts' '\n+\ttest_when_finished rm .git/info/grafts &&\n+\ttest_commit abc &&\n+\tSHA1=`git rev-parse HEAD` &&\n+\ttest_commit def &&\n+\ttest_commit xyz &&\n+\techo \"`git rev-parse HEAD` $SHA1\" >.git/info/grafts &&\n+\tcat >expected <<-\\EOF &&\n+\txyz\n+\tabc\n+\tEOF\n+\tgit log --format=%s -2 >actual &&\n+\ttest_cmp expected actual &&\n+\tgit checkout --detach --to grafted master &&\n+\tgit --git-dir=grafted/.git log --format=%s -2 >actual &&\n+\ttest_cmp expected actual\n+'\n+\n test_done\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249230","messageId":"5411B98C.1090905@xiplink.com","threadId":"37450","inReplyTo":"1410388928-32265-23-git-send-email-pclouds@gmail.com","subject":"Re: [PATCH v2 22/32] checkout: support checking out into a new working directory","fromName":"Marc Branchaud","fromEmail":"marcnarc@xiplink.com","sentAt":"2014-09-11T15:02:36Z","receivedAt":"2014-09-11T15:02:36Z","isPatch":true,"sender":{"key":"marcnarc@xiplink.com","avatar":"https://avatars.githubusercontent.com/u/14980203?v=4"},"body":"On 14-09-10 06:41 PM, Nguyễn Thái Ngọc Duy wrote:\n> \"git checkout --to\" sets up a new working directory with a .git file\n> pointing to $GIT_DIR/worktrees/<id>. It then executes \"git checkout\"\n> again on the new worktree with the same arguments except \"--to\" is\n> taken out. The second checkout execution, which is not contaminated\n> with any info from the current repository, will actually check out and\n> everything that normal \"git checkout\" does.\n> \n> Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n> ---\n>  Documentation/git-checkout.txt         | 43 +++++++++++++++\n>  Documentation/git.txt                  |  3 +-\n>  Documentation/gitrepository-layout.txt |  7 +++\n>  builtin/checkout.c                     | 95 +++++++++++++++++++++++++++++++++-\n>  path.c                                 |  2 +-\n>  t/t2025-checkout-to.sh (new +x)        | 63 ++++++++++++++++++++++\n>  6 files changed, 209 insertions(+), 4 deletions(-)\n>  create mode 100755 t/t2025-checkout-to.sh\n> \n> diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt\n> index 33ad2ad..bd0fc1d 100644\n> --- a/Documentation/git-checkout.txt\n> +++ b/Documentation/git-checkout.txt\n> @@ -225,6 +225,13 @@ This means that you can use `git checkout -p` to selectively discard\n>  edits from your current working tree. See the ``Interactive Mode''\n>  section of linkgit:git-add[1] to learn how to operate the `--patch` mode.\n>  \n> +--to=<path>::\n> +\tCheck out a branch in a separate working directory at\n> +\t`<path>`. A new working directory is linked to the current\n> +\trepository, sharing everything except working directory\n> +\tspecific files such as HEAD, index... See \"MULTIPLE CHECKOUT\n> +\tMODE\" section for more information.\n> +\n>  <branch>::\n>  \tBranch to checkout; if it refers to a branch (i.e., a name that,\n>  \twhen prepended with \"refs/heads/\", is a valid ref), then that\n> @@ -388,6 +395,42 @@ $ git reflog -2 HEAD # or\n>  $ git log -g -2 HEAD\n>  ------------\n>  \n> +MULTIPLE CHECKOUT MODE\n> +----------------------\n> +Normally a working directory is attached to repository. When \"git\n> +checkout --to\" is used, a new working directory is attached to the\n> +current repository. This new working directory is called \"linked\n> +checkout\" as compared to the \"main checkout\" prepared by \"git init\" or\n> +\"git clone\". A repository has one main checkout and zero or more\n> +linked checkouts.\n> +\n> +Each linked checkout has a private directory in $GIT_DIR/worktrees in\n> +the main checkout, usually named after the base name of the new\n> +working directory, optionally with a number added to make it\n> +unique. For example, the command `git checkout --to ../test-next next`\n> +running with $GIT_DIR=/path/main may create the directory\n> +`$GIT_DIR/worktrees/test-next` (or `$GIT_DIR/worktrees/test-next1` if\n> +`test-next` is already taken).\n> +\n> +Within a linked checkout, $GIT_DIR is set to point to this private\n> +directory (e.g. `/path/main/worktrees/test-next` in the example) and\n> +$GIT_COMMON_DIR is set to point back to the main checkout's $GIT_DIR\n> +(e.g. `/path/main`). Setting is done via a .git file located at the\n> +top directory of the linked checkout.\n> +\n> +Path resolution via `git rev-parse --git-path` would use either\n> +$GIT_DIR or $GIT_COMMON_DIR depending on the path. For example, the\n> +linked checkout's `$GIT_DIR/HEAD` resolve to\n> +`/path/main/worktrees/test-next/HEAD` (not `/path/main/HEAD` which is\n> +the main checkout's HEAD) while `$GIT_DIR/refs/heads/master` would use\n> +$GIT_COMMON_DIR and resolve to `/path/main/refs/heads/master`, which\n> +is shared across checkouts.\n> +\n> +See linkgit:gitrepository-layout[5] for more information. The rule of\n> +thumb is do not make any assumption about whether a path belongs to\n> +$GIT_DIR or $GIT_COMMON_DIR when you need to directly access something\n> +inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.\n> +\n\nUm, didn't you say in [1] that you'd use the text I posted in [2]?\n\n[1] http://article.gmane.org/gmane.comp.version-control.git/256446\n[2] http://article.gmane.org/gmane.comp.version-control.git/256323\n\n\t\tM.\n"},{"id":"249232","messageId":"5411C16C.9010406@xiplink.com","threadId":"37450","inReplyTo":"1410388928-32265-24-git-send-email-pclouds@gmail.com","subject":"Re: [PATCH v2 23/32] prune: strategies for linked checkouts","fromName":"Marc Branchaud","fromEmail":"marcnarc@xiplink.com","sentAt":"2014-09-11T15:36:12Z","receivedAt":"2014-09-11T15:36:12Z","isPatch":true,"sender":{"key":"marcnarc@xiplink.com","avatar":"https://avatars.githubusercontent.com/u/14980203?v=4"},"body":"On 14-09-10 06:41 PM, Nguyễn Thái Ngọc Duy wrote:\n> (alias R=$GIT_COMMON_DIR/worktrees/<id>)\n> \n>  - linked checkouts are supposed to keep its location in $R/gitdir up\n>    to date. The use case is auto fixup after a manual checkout move.\n> \n>  - linked checkouts are supposed to update mtime of $R/gitdir. If\n>    $R/gitdir's mtime is older than a limit, and it points to nowhere,\n>    worktrees/<id> is to be pruned.\n> \n>  - If $R/locked exists, worktrees/<id> is not supposed to be pruned. If\n>    $R/locked exists and $R/gitdir's mtime is older than a really long\n>    limit, warn about old unused repo.\n> \n>  - \"git checkout --to\" is supposed to make a hard link named $R/link\n>    pointing to the .git file on supported file systems to help detect\n>    the user manually deleting the checkout. If $R/link exists and its\n>    link count is greated than 1, the repo is kept.\n> \n> Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n> ---\n>  Documentation/git-checkout.txt             | 18 ++++++\n>  Documentation/git-prune.txt                |  3 +\n>  Documentation/gitrepository-layout.txt     | 19 ++++++\n>  builtin/checkout.c                         | 19 +++++-\n>  builtin/prune.c                            | 95 ++++++++++++++++++++++++++++++\n>  setup.c                                    | 13 ++++\n>  t/t2026-prune-linked-checkouts.sh (new +x) | 84 ++++++++++++++++++++++++++\n>  7 files changed, 249 insertions(+), 2 deletions(-)\n>  create mode 100755 t/t2026-prune-linked-checkouts.sh\n> \n> diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt\n> index bd0fc1d..a29748e 100644\n> --- a/Documentation/git-checkout.txt\n> +++ b/Documentation/git-checkout.txt\n> @@ -431,6 +431,24 @@ thumb is do not make any assumption about whether a path belongs to\n>  $GIT_DIR or $GIT_COMMON_DIR when you need to directly access something\n>  inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.\n>  \n> +When you are done, simply deleting the linked working directory would\n> +suffice. $GIT_DIR/worktrees can be cleaned up using `git prune\n> +--worktrees`.\n\nThis needs a tweak or two so that it follows more naturally from the previous\nparagraph.  How about:\n\n\tWhen you are done with a linked working tree you can simply delete\n\tit.  You can clean up any stale $GIT_DIR/worktrees entries with\n\t`git prune --worktrees`.\n\nThen in commit 28, when you add worktrees pruning to gc, you should change\nthis paragraph again:\n\n\tWhen you are done with a linked working tree you can simply delete\n\tit.  The working tree's entry in the repository's $GIT_DIR/worktrees\n\tdirectory will eventually be removed automatically (see\n\t`gc.pruneworktreesexpire` in linkgit::git-config[1]), or you can run\n\t`git prune --worktrees` to clean up any stale entries in\n\t$GIT_DIR/worktrees.\n\n> +After you move a linked working directory to another file system, or\n> +on a file system that does not support hard link, execute any git\n> +command (e.g. `git status`) in the new working directory so that it\n> +could update its location in $GIT_DIR/worktrees and not be\n> +accidentally pruned.\n\nIt took me a couple of seconds to parse that.  How about:\n\n\tIf you move a linked working directory to another file system, or\n\twithin a file system that does not support hard links, you need to\n\trun at least one git command inside the moved linked working\n\tdirectory (e.g. `git status`) in order to update its entry in\n\t$GIT_DIR/worktrees so that it does not get automatically removed.\n\n> +To stop `git prune --worktrees` from deleting a specific working\n> +directory (e.g. because it's on a portable device), you could add the\n> +file 'locked' to $GIT_DIR/worktrees. For example, if `.git` file of\n> +the new working directory points to `/path/main/worktrees/test-next`,\n> +the full path of the 'locked' file would be\n> +`/path/main/worktrees/test-next/locked`. See\n> +linkgit:gitrepository-layout[5] for details.\n\nSorry, I can't help rewriting this one too:\n\n\tTo prevent `git prune --worktrees` from deleting a\n\t$GIT_DIR/worktrees entry (which can be useful in some situations,\n\tsuch as when the entry's working tree is stored on a portable\n\tdevice), add a file named 'locked' to the entry's directory.  For\n\texample, if a linked working tree's `.git` file points to\n\t`/path/main/.git/worktrees/test-next` then a file named\n\t`/path/main/.git/worktrees/test-next/locked` will prevent the\n\t`test-next` entry from being pruned.  See\n\tlinkgit:gitrepository-layout[5] for details.\n\nI also suggest this paragraph get updated in commit 28, but just change the\nfirst clause, before \"(which can be ...\":\n\n\tTo prevent a $GIT_DIR/worktrees entry from being pruned (which ...\n\nThanks for all this work!\n\n\t\tM.\n"},{"id":"249276","messageId":"CAPig+cQGeEzrx9N1Rrxhf--gphWSam88BV06AJTA=6-YZvQ4Tg@mail.gmail.com","threadId":"37450","inReplyTo":"5411C16C.9010406@xiplink.com","subject":"Re: [PATCH v2 23/32] prune: strategies for linked checkouts","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2014-09-12T03:06:22Z","receivedAt":"2014-09-12T03:06:22Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Thu, Sep 11, 2014 at 11:36 AM, Marc Branchaud <marcnarc@xiplink.com> wrote:\n> On 14-09-10 06:41 PM, Nguyễn Thái Ngọc Duy wrote:\n>> (alias R=$GIT_COMMON_DIR/worktrees/<id>)\n>>\n>>  - linked checkouts are supposed to keep its location in $R/gitdir up\n>>    to date. The use case is auto fixup after a manual checkout move.\n>>\n>>  - linked checkouts are supposed to update mtime of $R/gitdir. If\n>>    $R/gitdir's mtime is older than a limit, and it points to nowhere,\n>>    worktrees/<id> is to be pruned.\n>>\n>>  - If $R/locked exists, worktrees/<id> is not supposed to be pruned. If\n>>    $R/locked exists and $R/gitdir's mtime is older than a really long\n>>    limit, warn about old unused repo.\n>>\n>>  - \"git checkout --to\" is supposed to make a hard link named $R/link\n>>    pointing to the .git file on supported file systems to help detect\n>>    the user manually deleting the checkout. If $R/link exists and its\n>>    link count is greated than 1, the repo is kept.\n>>\n>> Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n>> ---\n>>  Documentation/git-checkout.txt             | 18 ++++++\n>>  Documentation/git-prune.txt                |  3 +\n>>  Documentation/gitrepository-layout.txt     | 19 ++++++\n>>  builtin/checkout.c                         | 19 +++++-\n>>  builtin/prune.c                            | 95 ++++++++++++++++++++++++++++++\n>>  setup.c                                    | 13 ++++\n>>  t/t2026-prune-linked-checkouts.sh (new +x) | 84 ++++++++++++++++++++++++++\n>>  7 files changed, 249 insertions(+), 2 deletions(-)\n>>  create mode 100755 t/t2026-prune-linked-checkouts.sh\n>>\n>> diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt\n>> index bd0fc1d..a29748e 100644\n>> --- a/Documentation/git-checkout.txt\n>> +++ b/Documentation/git-checkout.txt\n>> @@ -431,6 +431,24 @@ thumb is do not make any assumption about whether a path belongs to\n>>  $GIT_DIR or $GIT_COMMON_DIR when you need to directly access something\n>>  inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.\n>>\n>> +When you are done, simply deleting the linked working directory would\n>> +suffice. $GIT_DIR/worktrees can be cleaned up using `git prune\n>> +--worktrees`.\n>\n> This needs a tweak or two so that it follows more naturally from the previous\n> paragraph.  How about:\n>\n>         When you are done with a linked working tree you can simply delete\n>         it.  You can clean up any stale $GIT_DIR/worktrees entries with\n>         `git prune --worktrees`.\n\nThanks for these rewrites; I was going to provide similar suggestions.\n\nOne minor addition for clarification would be to mention that the 'git\nprune --worktrees' invocation applies to the main worktree:\n\n    When you are done with a linked working tree, you can simply delete\n    it. You can clean up any stale $GIT_DIR/worktrees entries via\n    `git prune --worktrees` in the main worktree.\n\n> Then in commit 28, when you add worktrees pruning to gc, you should change\n> this paragraph again:\n>\n>         When you are done with a linked working tree you can simply delete\n>         it.  The working tree's entry in the repository's $GIT_DIR/worktrees\n>         directory will eventually be removed automatically (see\n>         `gc.pruneworktreesexpire` in linkgit::git-config[1]), or you can run\n>         `git prune --worktrees` to clean up any stale entries in\n>         $GIT_DIR/worktrees.\n\nDitto about qualifying 'git prune --worktrees' with \"in the main work tree\".\n\n>> +After you move a linked working directory to another file system, or\n>> +on a file system that does not support hard link, execute any git\n>> +command (e.g. `git status`) in the new working directory so that it\n>> +could update its location in $GIT_DIR/worktrees and not be\n>> +accidentally pruned.\n>\n> It took me a couple of seconds to parse that.  How about:\n>\n>         If you move a linked working directory to another file system, or\n>         within a file system that does not support hard links, you need to\n>         run at least one git command inside the moved linked working\n\nI trip over \"moved linked\" every time I read it. I think there's\nsufficient context in the 'if' clause leading to this that \"moved\" can\nbe dropped.\n\n>         directory (e.g. `git status`) in order to update its entry in\n>         $GIT_DIR/worktrees so that it does not get automatically removed.\n>\n>> +To stop `git prune --worktrees` from deleting a specific working\n>> +directory (e.g. because it's on a portable device), you could add the\n>> +file 'locked' to $GIT_DIR/worktrees. For example, if `.git` file of\n>> +the new working directory points to `/path/main/worktrees/test-next`,\n>> +the full path of the 'locked' file would be\n>> +`/path/main/worktrees/test-next/locked`. See\n>> +linkgit:gitrepository-layout[5] for details.\n>\n> Sorry, I can't help rewriting this one too:\n>\n>         To prevent `git prune --worktrees` from deleting a\n>         $GIT_DIR/worktrees entry (which can be useful in some situations,\n>         such as when the entry's working tree is stored on a portable\n>         device), add a file named 'locked' to the entry's directory.  For\n>         example, if a linked working tree's `.git` file points to\n>         `/path/main/.git/worktrees/test-next` then a file named\n>         `/path/main/.git/worktrees/test-next/locked` will prevent the\n>         `test-next` entry from being pruned.  See\n>         linkgit:gitrepository-layout[5] for details.\n\nEach time I read this (or Duy's original), the first question that\npops into my head is \"should 'locked' be empty or have content, and if\ncontent, what content?\" gitrepository-layout.txt does explain the\ncontent (if the reader bothers to chase the link), but perhaps it is\nworth an explanatory sentence here?\n\n> I also suggest this paragraph get updated in commit 28, but just change the\n> first clause, before \"(which can be ...\":\n>\n>         To prevent a $GIT_DIR/worktrees entry from being pruned (which ...\n>\n> Thanks for all this work!\n>\n>                 M.\n"},{"id":"249325","messageId":"54134492.6020004@xiplink.com","threadId":"37450","inReplyTo":"CAPig+cQGeEzrx9N1Rrxhf--gphWSam88BV06AJTA=6-YZvQ4Tg@mail.gmail.com","subject":"Re: [PATCH v2 23/32] prune: strategies for linked checkouts","fromName":"Marc Branchaud","fromEmail":"marcnarc@xiplink.com","sentAt":"2014-09-12T19:08:02Z","receivedAt":"2014-09-12T19:08:02Z","isPatch":true,"sender":{"key":"marcnarc@xiplink.com","avatar":"https://avatars.githubusercontent.com/u/14980203?v=4"},"body":"On 14-09-11 11:06 PM, Eric Sunshine wrote:\n> On Thu, Sep 11, 2014 at 11:36 AM, Marc Branchaud <marcnarc@xiplink.com> wrote:\n>> On 14-09-10 06:41 PM, Nguyễn Thái Ngọc Duy wrote:\n>>> (alias R=$GIT_COMMON_DIR/worktrees/<id>)\n>>>\n>>>  - linked checkouts are supposed to keep its location in $R/gitdir up\n>>>    to date. The use case is auto fixup after a manual checkout move.\n>>>\n>>>  - linked checkouts are supposed to update mtime of $R/gitdir. If\n>>>    $R/gitdir's mtime is older than a limit, and it points to nowhere,\n>>>    worktrees/<id> is to be pruned.\n>>>\n>>>  - If $R/locked exists, worktrees/<id> is not supposed to be pruned. If\n>>>    $R/locked exists and $R/gitdir's mtime is older than a really long\n>>>    limit, warn about old unused repo.\n>>>\n>>>  - \"git checkout --to\" is supposed to make a hard link named $R/link\n>>>    pointing to the .git file on supported file systems to help detect\n>>>    the user manually deleting the checkout. If $R/link exists and its\n>>>    link count is greated than 1, the repo is kept.\n>>>\n>>> Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n>>> ---\n>>>  Documentation/git-checkout.txt             | 18 ++++++\n>>>  Documentation/git-prune.txt                |  3 +\n>>>  Documentation/gitrepository-layout.txt     | 19 ++++++\n>>>  builtin/checkout.c                         | 19 +++++-\n>>>  builtin/prune.c                            | 95 ++++++++++++++++++++++++++++++\n>>>  setup.c                                    | 13 ++++\n>>>  t/t2026-prune-linked-checkouts.sh (new +x) | 84 ++++++++++++++++++++++++++\n>>>  7 files changed, 249 insertions(+), 2 deletions(-)\n>>>  create mode 100755 t/t2026-prune-linked-checkouts.sh\n>>>\n>>> diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt\n>>> index bd0fc1d..a29748e 100644\n>>> --- a/Documentation/git-checkout.txt\n>>> +++ b/Documentation/git-checkout.txt\n>>> @@ -431,6 +431,24 @@ thumb is do not make any assumption about whether a path belongs to\n>>>  $GIT_DIR or $GIT_COMMON_DIR when you need to directly access something\n>>>  inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.\n>>>\n>>> +When you are done, simply deleting the linked working directory would\n>>> +suffice. $GIT_DIR/worktrees can be cleaned up using `git prune\n>>> +--worktrees`.\n>>\n>> This needs a tweak or two so that it follows more naturally from the previous\n>> paragraph.  How about:\n>>\n>>         When you are done with a linked working tree you can simply delete\n>>         it.  You can clean up any stale $GIT_DIR/worktrees entries with\n>>         `git prune --worktrees`.\n> \n> Thanks for these rewrites; I was going to provide similar suggestions.\n> \n> One minor addition for clarification would be to mention that the 'git\n> prune --worktrees' invocation applies to the main worktree:\n> \n>     When you are done with a linked working tree, you can simply delete\n>     it. You can clean up any stale $GIT_DIR/worktrees entries via\n>     `git prune --worktrees` in the main worktree.\n> \n>> Then in commit 28, when you add worktrees pruning to gc, you should change\n>> this paragraph again:\n>>\n>>         When you are done with a linked working tree you can simply delete\n>>         it.  The working tree's entry in the repository's $GIT_DIR/worktrees\n>>         directory will eventually be removed automatically (see\n>>         `gc.pruneworktreesexpire` in linkgit::git-config[1]), or you can run\n>>         `git prune --worktrees` to clean up any stale entries in\n>>         $GIT_DIR/worktrees.\n> \n> Ditto about qualifying 'git prune --worktrees' with \"in the main work tree\".\n> \n>>> +After you move a linked working directory to another file system, or\n>>> +on a file system that does not support hard link, execute any git\n>>> +command (e.g. `git status`) in the new working directory so that it\n>>> +could update its location in $GIT_DIR/worktrees and not be\n>>> +accidentally pruned.\n>>\n>> It took me a couple of seconds to parse that.  How about:\n>>\n>>         If you move a linked working directory to another file system, or\n>>         within a file system that does not support hard links, you need to\n>>         run at least one git command inside the moved linked working\n> \n> I trip over \"moved linked\" every time I read it. I think there's\n> sufficient context in the 'if' clause leading to this that \"moved\" can\n> be dropped.\n\nFine by me, as are your other suggestions.\n\n\t\tM.\n\n\n>>         directory (e.g. `git status`) in order to update its entry in\n>>         $GIT_DIR/worktrees so that it does not get automatically removed.\n>>\n>>> +To stop `git prune --worktrees` from deleting a specific working\n>>> +directory (e.g. because it's on a portable device), you could add the\n>>> +file 'locked' to $GIT_DIR/worktrees. For example, if `.git` file of\n>>> +the new working directory points to `/path/main/worktrees/test-next`,\n>>> +the full path of the 'locked' file would be\n>>> +`/path/main/worktrees/test-next/locked`. See\n>>> +linkgit:gitrepository-layout[5] for details.\n>>\n>> Sorry, I can't help rewriting this one too:\n>>\n>>         To prevent `git prune --worktrees` from deleting a\n>>         $GIT_DIR/worktrees entry (which can be useful in some situations,\n>>         such as when the entry's working tree is stored on a portable\n>>         device), add a file named 'locked' to the entry's directory.  For\n>>         example, if a linked working tree's `.git` file points to\n>>         `/path/main/.git/worktrees/test-next` then a file named\n>>         `/path/main/.git/worktrees/test-next/locked` will prevent the\n>>         `test-next` entry from being pruned.  See\n>>         linkgit:gitrepository-layout[5] for details.\n> \n> Each time I read this (or Duy's original), the first question that\n> pops into my head is \"should 'locked' be empty or have content, and if\n> content, what content?\" gitrepository-layout.txt does explain the\n> content (if the reader bothers to chase the link), but perhaps it is\n> worth an explanatory sentence here?\n> \n>> I also suggest this paragraph get updated in commit 28, but just change the\n>> first clause, before \"(which can be ...\":\n>>\n>>         To prevent a $GIT_DIR/worktrees entry from being pruned (which ...\n>>\n>> Thanks for all this work!\n>>\n>>                 M.\n> --\n> To unsubscribe from this list: send the line \"unsubscribe git\" in\n> the body of a message to majordomo@vger.kernel.org\n> More majordomo info at  http://vger.kernel.org/majordomo-info.html\n> \n"},{"id":"249706","messageId":"20140921024101.GA6275@lanh","threadId":"37450","inReplyTo":"5411B98C.1090905@xiplink.com","subject":"Re: [PATCH v2 22/32] checkout: support checking out into a new working directory","fromName":"Duy Nguyen","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-21T02:41:02Z","receivedAt":"2014-09-21T02:41:02Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"On Thu, Sep 11, 2014 at 11:02:36AM -0400, Marc Branchaud wrote:\n> Um, didn't you say in [1] that you'd use the text I posted in [2]?\n> \n> [1] http://article.gmane.org/gmane.comp.version-control.git/256446\n> [2] http://article.gmane.org/gmane.comp.version-control.git/256323\n\nThat's the problem with updating a bit one day and a bit another\nday. Thanks for checking. Does this look ok?\n\n-- 8< --\nSubject: [PATCH] checkout: support checking out into a new working directory\n\n\"git checkout --to\" sets up a new working directory with a .git file\npointing to $GIT_DIR/worktrees/<id>. It then executes \"git checkout\"\nagain on the new worktree with the same arguments except \"--to\" is\ntaken out. The second checkout execution, which is not contaminated\nwith any info from the current repository, will actually check out and\neverything that normal \"git checkout\" does.\n\nHelped-by: Marc Branchaud <marcnarc@xiplink.com>\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n Documentation/git-checkout.txt         | 46 ++++++++++++++++\n Documentation/git.txt                  |  3 +-\n Documentation/gitrepository-layout.txt |  7 +++\n builtin/checkout.c                     | 95 +++++++++++++++++++++++++++++++++-\n path.c                                 |  2 +-\n t/t2025-checkout-to.sh (new +x)        | 63 ++++++++++++++++++++++\n 6 files changed, 212 insertions(+), 4 deletions(-)\n create mode 100755 t/t2025-checkout-to.sh\n\ndiff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt\nindex 33ad2ad..c101575 100644\n--- a/Documentation/git-checkout.txt\n+++ b/Documentation/git-checkout.txt\n@@ -225,6 +225,13 @@ This means that you can use `git checkout -p` to selectively discard\n edits from your current working tree. See the ``Interactive Mode''\n section of linkgit:git-add[1] to learn how to operate the `--patch` mode.\n \n+--to=<path>::\n+\tCheck out a branch in a separate working directory at\n+\t`<path>`. A new working directory is linked to the current\n+\trepository, sharing everything except working directory\n+\tspecific files such as HEAD, index... See \"MULTIPLE WORKING\n+\tTREES\" section for more information.\n+\n <branch>::\n \tBranch to checkout; if it refers to a branch (i.e., a name that,\n \twhen prepended with \"refs/heads/\", is a valid ref), then that\n@@ -388,6 +395,45 @@ $ git reflog -2 HEAD # or\n $ git log -g -2 HEAD\n ------------\n \n+MULTIPLE WORKING TREES\n+----------------------\n+\n+A git repository can support multiple working trees, allowing you to check\n+out more than one branch at a time.  With `git checkout --to` a new working\n+tree is associated with the repository.  This new working tree is called a\n+\"linked working tree\" as opposed to the \"main working tree\" prepared by \"git\n+init\" or \"git clone\".  A repository has one main working tree (if it's not a\n+bare repository) and zero or more linked working trees.\n+\n+Each linked working tree has a private sub-directory in the repository's\n+$GIT_DIR/worktrees directory.  The private sub-directory's name is usually\n+the base name of the linked working tree's path, possibly appended with a\n+number to make it unique.  For example, when `$GIT_DIR=/path/main/.git` the\n+command `git checkout --to /path/other/test-next next` creates the linked\n+working tree in `/path/other/test-next` and also creates a\n+`$GIT_DIR/worktrees/test-next` directory (or `$GIT_DIR/worktrees/test-next1`\n+if `test-next` is already taken).\n+\n+Within a linked working tree, $GIT_DIR is set to point to this private\n+directory (e.g. `/path/main/.git/worktrees/test-next` in the example) and\n+$GIT_COMMON_DIR is set to point back to the main working tree's $GIT_DIR\n+(e.g. `/path/main/.git`). These settings are made in a `.git` file located at\n+the top directory of the linked working tree.\n+\n+Path resolution via `git rev-parse --git-path` uses either\n+$GIT_DIR or $GIT_COMMON_DIR depending on the path. For example, in the\n+linked working tree `git rev-parse --git-path HEAD` returns\n+`/path/main/.git/worktrees/test-next/HEAD` (not\n+`/path/other/test-next/.git/HEAD` or `/path/main/.git/HEAD`) while `git\n+rev-parse --git-path refs/heads/master` uses\n+$GIT_COMMON_DIR and returns `/path/main/.git/refs/heads/master`,\n+since refs are shared across all working trees.\n+\n+See linkgit:gitrepository-layout[5] for more information. The rule of\n+thumb is do not make any assumption about whether a path belongs to\n+$GIT_DIR or $GIT_COMMON_DIR when you need to directly access something\n+inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.\n+\n EXAMPLES\n --------\n \ndiff --git a/Documentation/git.txt b/Documentation/git.txt\nindex 176d37c..54c9fb6 100644\n--- a/Documentation/git.txt\n+++ b/Documentation/git.txt\n@@ -806,7 +806,8 @@ Git so take care if using Cogito etc.\n \tIf this variable is set to a path, non-worktree files that are\n \tnormally in $GIT_DIR will be taken from this path\n \tinstead. Worktree-specific files such as HEAD or index are\n-\ttaken from $GIT_DIR. See linkgit:gitrepository-layout[5] for\n+\ttaken from $GIT_DIR. See linkgit:gitrepository-layout[5] and\n+\tthe section 'MULTIPLE CHECKOUT MODE' in linkgit:checkout[1]\n \tdetails. This variable has lower precedence than other path\n \tvariables such as GIT_INDEX_FILE, GIT_OBJECT_DIRECTORY...\n \ndiff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt\nindex 2dc5667..8228450 100644\n--- a/Documentation/gitrepository-layout.txt\n+++ b/Documentation/gitrepository-layout.txt\n@@ -252,6 +252,13 @@ modules::\n \tdirectory is ignored if $GIT_COMMON_DIR is set and\n \t\"$GIT_COMMON_DIR/modules\" will be used instead.\n \n+worktrees::\n+\tContains worktree specific information of linked\n+\tcheckouts. Each subdirectory contains the worktree-related\n+\tpart of a linked checkout. This directory is ignored if\n+\t$GIT_COMMON_DIR is set and \"$GIT_COMMON_DIR/worktrees\" will be\n+\tused instead.\n+\n SEE ALSO\n --------\n linkgit:git-init[1],\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 220f80e..ad10f99 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -48,6 +48,10 @@ struct checkout_opts {\n \tconst char *prefix;\n \tstruct pathspec pathspec;\n \tstruct tree *source_tree;\n+\n+\tconst char *new_worktree;\n+\tconst char **saved_argv;\n+\tint new_worktree_mode;\n };\n \n static int post_checkout_hook(struct commit *old, struct commit *new,\n@@ -249,6 +253,9 @@ static int checkout_paths(const struct checkout_opts *opts,\n \t\tdie(_(\"Cannot update paths and switch to branch '%s' at the same time.\"),\n \t\t    opts->new_branch);\n \n+\tif (opts->new_worktree)\n+\t\tdie(_(\"'%s' cannot be used with updating paths\"), \"--to\");\n+\n \tif (opts->patch_mode)\n \t\treturn run_add_interactive(revision, \"--patch=checkout\",\n \t\t\t\t\t   &opts->pathspec);\n@@ -484,7 +491,7 @@ static int merge_working_tree(const struct checkout_opts *opts,\n \t\t\ttopts.dir->flags |= DIR_SHOW_IGNORED;\n \t\t\tsetup_standard_excludes(topts.dir);\n \t\t}\n-\t\ttree = parse_tree_indirect(old->commit ?\n+\t\ttree = parse_tree_indirect(old->commit && !opts->new_worktree_mode ?\n \t\t\t\t\t   old->commit->object.sha1 :\n \t\t\t\t\t   EMPTY_TREE_SHA1_BIN);\n \t\tinit_tree_desc(&trees[0], tree->buffer, tree->size);\n@@ -800,7 +807,8 @@ static int switch_branches(const struct checkout_opts *opts,\n \t\treturn ret;\n \t}\n \n-\tif (!opts->quiet && !old.path && old.commit && new->commit != old.commit)\n+\tif (!opts->quiet && !old.path && old.commit &&\n+\t    new->commit != old.commit && !opts->new_worktree_mode)\n \t\torphaned_commit_warning(old.commit, new->commit);\n \n \tupdate_refs_for_switch(opts, &old, new);\n@@ -810,6 +818,76 @@ static int switch_branches(const struct checkout_opts *opts,\n \treturn ret || writeout_error;\n }\n \n+static int prepare_linked_checkout(const struct checkout_opts *opts,\n+\t\t\t\t   struct branch_info *new)\n+{\n+\tstruct strbuf sb_git = STRBUF_INIT, sb_repo = STRBUF_INIT;\n+\tstruct strbuf sb = STRBUF_INIT;\n+\tconst char *path = opts->new_worktree, *name;\n+\tstruct stat st;\n+\tstruct child_process cp;\n+\tint counter = 0, len;\n+\n+\tif (!new->commit)\n+\t\tdie(_(\"no branch specified\"));\n+\tif (file_exists(path))\n+\t\tdie(_(\"'%s' already exists\"), path);\n+\n+\tlen = strlen(path);\n+\twhile (len && is_dir_sep(path[len - 1]))\n+\t\tlen--;\n+\n+\tfor (name = path + len - 1; name > path; name--)\n+\t\tif (is_dir_sep(*name)) {\n+\t\t\tname++;\n+\t\t\tbreak;\n+\t\t}\n+\tstrbuf_addstr(&sb_repo,\n+\t\t      git_path(\"worktrees/%.*s\", (int)(path + len - name), name));\n+\tlen = sb_repo.len;\n+\tif (safe_create_leading_directories_const(sb_repo.buf))\n+\t\tdie_errno(_(\"could not create leading directories of '%s'\"),\n+\t\t\t  sb_repo.buf);\n+\twhile (!stat(sb_repo.buf, &st)) {\n+\t\tcounter++;\n+\t\tstrbuf_setlen(&sb_repo, len);\n+\t\tstrbuf_addf(&sb_repo, \"%d\", counter);\n+\t}\n+\tname = strrchr(sb_repo.buf, '/') + 1;\n+\tif (mkdir(sb_repo.buf, 0777))\n+\t\tdie_errno(_(\"could not create directory of '%s'\"), sb_repo.buf);\n+\n+\tstrbuf_addf(&sb_git, \"%s/.git\", path);\n+\tif (safe_create_leading_directories_const(sb_git.buf))\n+\t\tdie_errno(_(\"could not create leading directories of '%s'\"),\n+\t\t\t  sb_git.buf);\n+\n+\twrite_file(sb_git.buf, 1, \"gitdir: %s/worktrees/%s\\n\",\n+\t\t   real_path(get_git_common_dir()), name);\n+\t/*\n+\t * This is to keep resolve_ref() happy. We need a valid HEAD\n+\t * or is_git_directory() will reject the directory. Any valid\n+\t * value would do because this value will be ignored and\n+\t * replaced at the next (real) checkout.\n+\t */\n+\tstrbuf_addf(&sb, \"%s/HEAD\", sb_repo.buf);\n+\twrite_file(sb.buf, 1, \"%s\\n\", sha1_to_hex(new->commit->object.sha1));\n+\tstrbuf_reset(&sb);\n+\tstrbuf_addf(&sb, \"%s/commondir\", sb_repo.buf);\n+\twrite_file(sb.buf, 1, \"../..\\n\");\n+\n+\tif (!opts->quiet)\n+\t\tfprintf_ln(stderr, _(\"Enter %s (identifier %s)\"), path, name);\n+\n+\tsetenv(\"GIT_CHECKOUT_NEW_WORKTREE\", \"1\", 1);\n+\tsetenv(GIT_DIR_ENVIRONMENT, sb_git.buf, 1);\n+\tsetenv(GIT_WORK_TREE_ENVIRONMENT, path, 1);\n+\tmemset(&cp, 0, sizeof(cp));\n+\tcp.git_cmd = 1;\n+\tcp.argv = opts->saved_argv;\n+\treturn run_command(&cp);\n+}\n+\n static int git_checkout_config(const char *var, const char *value, void *cb)\n {\n \tif (!strcmp(var, \"diff.ignoresubmodules\")) {\n@@ -1071,6 +1149,9 @@ static int checkout_branch(struct checkout_opts *opts,\n \t\tdie(_(\"Cannot switch branch to a non-commit '%s'\"),\n \t\t    new->name);\n \n+\tif (opts->new_worktree)\n+\t\treturn prepare_linked_checkout(opts, new);\n+\n \tif (!new->commit && opts->new_branch) {\n \t\tunsigned char rev[20];\n \t\tint flag;\n@@ -1113,6 +1194,8 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)\n \t\t\t N_(\"do not limit pathspecs to sparse entries only\")),\n \t\tOPT_HIDDEN_BOOL(0, \"guess\", &dwim_new_local_branch,\n \t\t\t\tN_(\"second guess 'git checkout no-such-branch'\")),\n+\t\tOPT_FILENAME(0, \"to\", &opts.new_worktree,\n+\t\t\t   N_(\"check a branch out in a separate working directory\")),\n \t\tOPT_END(),\n \t};\n \n@@ -1121,6 +1204,9 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)\n \topts.overwrite_ignore = 1;\n \topts.prefix = prefix;\n \n+\topts.saved_argv = xmalloc(sizeof(const char *) * (argc + 2));\n+\tmemcpy(opts.saved_argv, argv, sizeof(const char *) * (argc + 1));\n+\n \tgitmodules_config();\n \tgit_config(git_checkout_config, &opts);\n \n@@ -1129,6 +1215,11 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)\n \targc = parse_options(argc, argv, prefix, options, checkout_usage,\n \t\t\t     PARSE_OPT_KEEP_DASHDASH);\n \n+\t/* recursive execution from checkout_new_worktree() */\n+\topts.new_worktree_mode = getenv(\"GIT_CHECKOUT_NEW_WORKTREE\") != NULL;\n+\tif (opts.new_worktree_mode)\n+\t\topts.new_worktree = NULL;\n+\n \tif (conflict_style) {\n \t\topts.merge = 1; /* implied */\n \t\tgit_xmerge_config(\"merge.conflictstyle\", conflict_style, NULL);\ndiff --git a/path.c b/path.c\nindex 94db501..72eca6d 100644\n--- a/path.c\n+++ b/path.c\n@@ -92,7 +92,7 @@ static void replace_dir(struct strbuf *buf, int len, const char *newdir)\n \n static const char *common_list[] = {\n \t\"/branches\", \"/hooks\", \"/info\", \"/logs\", \"/lost-found\", \"/modules\",\n-\t\"/objects\", \"/refs\", \"/remotes\", \"/rr-cache\", \"/svn\",\n+\t\"/objects\", \"/refs\", \"/remotes\", \"/worktrees\", \"/rr-cache\", \"/svn\",\n \t\"config\", \"gc.pid\", \"packed-refs\", \"shallow\",\n \tNULL\n };\ndiff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh\nnew file mode 100755\nindex 0000000..8c73b18\n--- /dev/null\n+++ b/t/t2025-checkout-to.sh\n@@ -0,0 +1,63 @@\n+#!/bin/sh\n+\n+test_description='test git checkout --to'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'setup' '\n+\ttest_commit init\n+'\n+\n+test_expect_success 'checkout --to not updating paths' '\n+\ttest_must_fail git checkout --to -- init.t\n+'\n+\n+test_expect_success 'checkout --to an existing worktree' '\n+\tmkdir existing &&\n+\ttest_must_fail git checkout --detach --to existing master\n+'\n+\n+test_expect_success 'checkout --to a new worktree' '\n+\tgit checkout --to here master &&\n+\t(\n+\t\tcd here &&\n+\t\ttest_cmp ../init.t init.t &&\n+\t\tgit symbolic-ref HEAD >actual &&\n+\t\techo refs/heads/master >expect &&\n+\t\ttest_cmp expect actual &&\n+\t\tgit fsck\n+\t)\n+'\n+\n+test_expect_success 'checkout --to a new worktree from a subdir' '\n+\t(\n+\t\tmkdir sub &&\n+\t\tcd sub &&\n+\t\tgit checkout --detach --to here master &&\n+\t\tcd here &&\n+\t\ttest_cmp ../../init.t init.t\n+\t)\n+'\n+\n+test_expect_success 'checkout --to from a linked checkout' '\n+\t(\n+\t\tcd here &&\n+\t\tgit checkout --to nested-here master\n+\t\tcd nested-here &&\n+\t\tgit fsck\n+\t)\n+'\n+\n+test_expect_success 'checkout --to a new worktree creating new branch' '\n+\tgit checkout --to there -b newmaster master &&\n+\t(\n+\t\tcd there &&\n+\t\ttest_cmp ../init.t init.t &&\n+\t\tgit symbolic-ref HEAD >actual &&\n+\t\techo refs/heads/newmaster >expect &&\n+\t\ttest_cmp expect actual &&\n+\t\tgit fsck\n+\t)\n+'\n+\n+test_done\n-- \n2.1.0.rc0.78.gc0d8480\n-- 8<--\n"},{"id":"249707","messageId":"CACsJy8BLEfB6RQZTxqF82-tJmKQgnNC12QvKu8KZq=Vr059s3g@mail.gmail.com","threadId":"37450","inReplyTo":"CAPig+cQGeEzrx9N1Rrxhf--gphWSam88BV06AJTA=6-YZvQ4Tg@mail.gmail.com","subject":"Re: [PATCH v2 23/32] prune: strategies for linked checkouts","fromName":"Duy Nguyen","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-21T02:54:20Z","receivedAt":"2014-09-21T02:54:20Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"On Fri, Sep 12, 2014 at 10:06 AM, Eric Sunshine <sunshine@sunshineco.com> wrote:\n> One minor addition for clarification would be to mention that the 'git\n> prune --worktrees' invocation applies to the main worktree:\n>\n>     When you are done with a linked working tree, you can simply delete\n>     it. You can clean up any stale $GIT_DIR/worktrees entries via\n>     `git prune --worktrees` in the main worktree.\n\n\nThe command should work from any worktree (of course not the worktree\nthat you just deleted). I try to make all operations work on all\nworktrees. How should I write this?\n\n>>> +To stop `git prune --worktrees` from deleting a specific working\n>>> +directory (e.g. because it's on a portable device), you could add the\n>>> +file 'locked' to $GIT_DIR/worktrees. For example, if `.git` file of\n>>> +the new working directory points to `/path/main/worktrees/test-next`,\n>>> +the full path of the 'locked' file would be\n>>> +`/path/main/worktrees/test-next/locked`. See\n>>> +linkgit:gitrepository-layout[5] for details.\n>>\n>> Sorry, I can't help rewriting this one too:\n>>\n>>         To prevent `git prune --worktrees` from deleting a\n>>         $GIT_DIR/worktrees entry (which can be useful in some situations,\n>>         such as when the entry's working tree is stored on a portable\n>>         device), add a file named 'locked' to the entry's directory.  For\n>>         example, if a linked working tree's `.git` file points to\n>>         `/path/main/.git/worktrees/test-next` then a file named\n>>         `/path/main/.git/worktrees/test-next/locked` will prevent the\n>>         `test-next` entry from being pruned.  See\n>>         linkgit:gitrepository-layout[5] for details.\n>\n> Each time I read this (or Duy's original), the first question that\n> pops into my head is \"should 'locked' be empty or have content, and if\n> content, what content?\" gitrepository-layout.txt does explain the\n> content (if the reader bothers to chase the link), but perhaps it is\n> worth an explanatory sentence here?\n\nWill do.\n-- \nDuy\n"},{"id":"249709","messageId":"CAPig+cRX6z2ReKqES_2SP4mVruCDNRyo62_deo3ukuUYcYaKiw@mail.gmail.com","threadId":"37450","inReplyTo":"CACsJy8BLEfB6RQZTxqF82-tJmKQgnNC12QvKu8KZq=Vr059s3g@mail.gmail.com","subject":"Re: [PATCH v2 23/32] prune: strategies for linked checkouts","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2014-09-21T03:01:51Z","receivedAt":"2014-09-21T03:01:51Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Sat, Sep 20, 2014 at 10:54 PM, Duy Nguyen <pclouds@gmail.com> wrote:\n> On Fri, Sep 12, 2014 at 10:06 AM, Eric Sunshine <sunshine@sunshineco.com> wrote:\n>> One minor addition for clarification would be to mention that the 'git\n>> prune --worktrees' invocation applies to the main worktree:\n>>\n>>     When you are done with a linked working tree, you can simply delete\n>>     it. You can clean up any stale $GIT_DIR/worktrees entries via\n>>     `git prune --worktrees` in the main worktree.\n>\n>\n> The command should work from any worktree (of course not the worktree\n> that you just deleted). I try to make all operations work on all\n> worktrees. How should I write this?\n\nI realized that a bit after writing the above. You could qualify it as:\n\n    When you are done with a linked working tree, you can simply delete\n    it. You can clean up any stale $GIT_DIR/worktrees entries via\n    `git prune --worktrees` in the main worktree or any linked tree.\n\nBut the \"...in the main worktree or any linked tree\" is likely\nredundant, in which case Marc's rewrite is probably sufficient.\n"},{"id":"249710","messageId":"CAPig+cRbfxUHmxbxxXLw=WKuV3JvBPEhuT4CvrZEOhWR-fbMHQ@mail.gmail.com","threadId":"37450","inReplyTo":"20140921024101.GA6275@lanh","subject":"Re: [PATCH v2 22/32] checkout: support checking out into a new working directory","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2014-09-21T03:10:47Z","receivedAt":"2014-09-21T03:10:47Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Sat, Sep 20, 2014 at 10:41 PM, Duy Nguyen <pclouds@gmail.com> wrote:\n> On Thu, Sep 11, 2014 at 11:02:36AM -0400, Marc Branchaud wrote:\n>> Um, didn't you say in [1] that you'd use the text I posted in [2]?\n>>\n>> [1] http://article.gmane.org/gmane.comp.version-control.git/256446\n>> [2] http://article.gmane.org/gmane.comp.version-control.git/256323\n>\n> That's the problem with updating a bit one day and a bit another\n> day. Thanks for checking. Does this look ok?\n>\n> -- 8< --\n> Subject: [PATCH] checkout: support checking out into a new working directory\n>\n> \"git checkout --to\" sets up a new working directory with a .git file\n> pointing to $GIT_DIR/worktrees/<id>. It then executes \"git checkout\"\n> again on the new worktree with the same arguments except \"--to\" is\n> taken out. The second checkout execution, which is not contaminated\n> with any info from the current repository, will actually check out and\n> everything that normal \"git checkout\" does.\n>\n> Helped-by: Marc Branchaud <marcnarc@xiplink.com>\n> Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n> ---\n> diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt\n> index 33ad2ad..c101575 100644\n> --- a/Documentation/git-checkout.txt\n> +++ b/Documentation/git-checkout.txt\n> @@ -388,6 +395,45 @@ $ git reflog -2 HEAD # or\n>  $ git log -g -2 HEAD\n>  ------------\n>\n> +MULTIPLE WORKING TREES\n> +----------------------\n> +\n> +A git repository can support multiple working trees, allowing you to check\n> +out more than one branch at a time.  With `git checkout --to` a new working\n> +tree is associated with the repository.  This new working tree is called a\n> +\"linked working tree\" as opposed to the \"main working tree\" prepared by \"git\n> +init\" or \"git clone\".  A repository has one main working tree (if it's not a\n> +bare repository) and zero or more linked working trees.\n> +\n> +Each linked working tree has a private sub-directory in the repository's\n> +$GIT_DIR/worktrees directory.  The private sub-directory's name is usually\n> +the base name of the linked working tree's path, possibly appended with a\n> +number to make it unique.  For example, when `$GIT_DIR=/path/main/.git` the\n> +command `git checkout --to /path/other/test-next next` creates the linked\n> +working tree in `/path/other/test-next` and also creates a\n> +`$GIT_DIR/worktrees/test-next` directory (or `$GIT_DIR/worktrees/test-next1`\n> +if `test-next` is already taken).\n> +\n> +Within a linked working tree, $GIT_DIR is set to point to this private\n> +directory (e.g. `/path/main/.git/worktrees/test-next` in the example) and\n> +$GIT_COMMON_DIR is set to point back to the main working tree's $GIT_DIR\n> +(e.g. `/path/main/.git`). These settings are made in a `.git` file located at\n> +the top directory of the linked working tree.\n> +\n> +Path resolution via `git rev-parse --git-path` uses either\n> +$GIT_DIR or $GIT_COMMON_DIR depending on the path. For example, in the\n> +linked working tree `git rev-parse --git-path HEAD` returns\n> +`/path/main/.git/worktrees/test-next/HEAD` (not\n> +`/path/other/test-next/.git/HEAD` or `/path/main/.git/HEAD`) while `git\n> +rev-parse --git-path refs/heads/master` uses\n> +$GIT_COMMON_DIR and returns `/path/main/.git/refs/heads/master`,\n> +since refs are shared across all working trees.\n> +\n> +See linkgit:gitrepository-layout[5] for more information. The rule of\n> +thumb is do not make any assumption about whether a path belongs to\n> +$GIT_DIR or $GIT_COMMON_DIR when you need to directly access something\n> +inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.\n\nWould it make sense for this \"rule of thumb\" summary to be presented\nfirst, and then the explanation of that rule after, rather than the\nreverse as is currently the case?\n\n> diff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh\n> new file mode 100755\n> index 0000000..8c73b18\n> --- /dev/null\n> +++ b/t/t2025-checkout-to.sh\n> @@ -0,0 +1,63 @@\n> +test_expect_success 'checkout --to from a linked checkout' '\n> +       (\n> +               cd here &&\n> +               git checkout --to nested-here master\n\nBroken &&-chain.\n\n> +               cd nested-here &&\n> +               git fsck\n> +       )\n> +'\n> +\n> +test_expect_success 'checkout --to a new worktree creating new branch' '\n> +       git checkout --to there -b newmaster master &&\n> +       (\n> +               cd there &&\n> +               test_cmp ../init.t init.t &&\n> +               git symbolic-ref HEAD >actual &&\n> +               echo refs/heads/newmaster >expect &&\n> +               test_cmp expect actual &&\n> +               git fsck\n> +       )\n> +'\n> +\n> +test_done\n> --\n> 2.1.0.rc0.78.gc0d8480\n"},{"id":"249720","messageId":"CACsJy8CH8mQjexFU1jO1orzajiwTXy4wJ022nq1PEY2GLUGAZA@mail.gmail.com","threadId":"37450","inReplyTo":"CAPig+cRbfxUHmxbxxXLw=WKuV3JvBPEhuT4CvrZEOhWR-fbMHQ@mail.gmail.com","subject":"Re: [PATCH v2 22/32] checkout: support checking out into a new working directory","fromName":"Duy Nguyen","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-21T09:50:21Z","receivedAt":"2014-09-21T09:50:21Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"On Sun, Sep 21, 2014 at 10:10 AM, Eric Sunshine <sunshine@sunshineco.com> wrote:\n> Would it make sense for this \"rule of thumb\" summary to be presented\n> first, and then the explanation of that rule after, rather than the\n> reverse as is currently the case?\n\nYou mean like this?\n\ndiff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt\nindex c101575..fd77631 100644\n--- a/Documentation/git-checkout.txt\n+++ b/Documentation/git-checkout.txt\n@@ -420,6 +420,11 @@ $GIT_COMMON_DIR is set to point back to the main\nworking tree's $GIT_DIR\n (e.g. `/path/main/.git`). These settings are made in a `.git` file located at\n the top directory of the linked working tree.\n\n+See linkgit:gitrepository-layout[5] for more information. The rule of\n+thumb is do not make any assumption about whether a path belongs to\n+$GIT_DIR or $GIT_COMMON_DIR when you need to directly access something\n+inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.\n+\n Path resolution via `git rev-parse --git-path` uses either\n $GIT_DIR or $GIT_COMMON_DIR depending on the path. For example, in the\n linked working tree `git rev-parse --git-path HEAD` returns\n@@ -429,11 +434,6 @@ rev-parse --git-path refs/heads/master` uses\n $GIT_COMMON_DIR and returns `/path/main/.git/refs/heads/master`,\n since refs are shared across all working trees.\n\n-See linkgit:gitrepository-layout[5] for more information. The rule of\n-thumb is do not make any assumption about whether a path belongs to\n-$GIT_DIR or $GIT_COMMON_DIR when you need to directly access something\n-inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.\n-\n EXAMPLES\n --------\n\n\n\n-- \nDuy\n"},{"id":"249722","messageId":"20140921102936.GA9438@lanh","threadId":"37450","inReplyTo":"CAPig+cRX6z2ReKqES_2SP4mVruCDNRyo62_deo3ukuUYcYaKiw@mail.gmail.com","subject":"Re: [PATCH v2 23/32] prune: strategies for linked checkouts","fromName":"Duy Nguyen","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-21T10:29:36Z","receivedAt":"2014-09-21T10:29:36Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Here we go again. Thanks both for the suggestions.\n\n-- 8< --\nSubject: [PATCH] prune: strategies for linked checkouts\n\n(alias R=$GIT_COMMON_DIR/worktrees/<id>)\n\n - linked checkouts are supposed to keep its location in $R/gitdir up\n   to date. The use case is auto fixup after a manual checkout move.\n\n - linked checkouts are supposed to update mtime of $R/gitdir. If\n   $R/gitdir's mtime is older than a limit, and it points to nowhere,\n   worktrees/<id> is to be pruned.\n\n - If $R/locked exists, worktrees/<id> is not supposed to be pruned. If\n   $R/locked exists and $R/gitdir's mtime is older than a really long\n   limit, warn about old unused repo.\n\n - \"git checkout --to\" is supposed to make a hard link named $R/link\n   pointing to the .git file on supported file systems to help detect\n   the user manually deleting the checkout. If $R/link exists and its\n   link count is greated than 1, the repo is kept.\n\nHelped-by: Marc Branchaud <marcnarc@xiplink.com>\nHelped-by: Eric Sunshine <sunshine@sunshineco.com>\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n Documentation/git-checkout.txt             | 20 +++++++\n Documentation/git-prune.txt                |  3 +\n Documentation/gitrepository-layout.txt     | 19 ++++++\n builtin/checkout.c                         | 19 +++++-\n builtin/prune.c                            | 95 ++++++++++++++++++++++++++++++\n setup.c                                    | 13 ++++\n t/t2026-prune-linked-checkouts.sh (new +x) | 84 ++++++++++++++++++++++++++\n 7 files changed, 251 insertions(+), 2 deletions(-)\n create mode 100755 t/t2026-prune-linked-checkouts.sh\n\ndiff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt\nindex c101575..0fd3bab 100644\n--- a/Documentation/git-checkout.txt\n+++ b/Documentation/git-checkout.txt\n@@ -434,6 +434,26 @@ thumb is do not make any assumption about whether a path belongs to\n $GIT_DIR or $GIT_COMMON_DIR when you need to directly access something\n inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.\n \n+When you are done with a linked working tree you can simply delete it.\n+You can clean up any stale $GIT_DIR/worktrees entries via `git prune\n+--worktrees` in the main worktree or any linked worktree.\n+\n+If you move a linked working directory to another file system, or\n+within a file system that does not support hard links, you need to run\n+at least one git command inside the linked working directory\n+(e.g. `git status`) in order to update its entry in $GIT_DIR/worktrees\n+so that it does not get automatically removed.\n+\n+To prevent `git prune --worktrees` from deleting a $GIT_DIR/worktrees\n+entry (which can be useful in some situations, such as when the\n+entry's working tree is stored on a portable device), add a file named\n+'locked' to the entry's directory. The file contains the reason in\n+plain text. For example, if a linked working tree's `.git` file points\n+to `/path/main/.git/worktrees/test-next` then a file named\n+`/path/main/.git/worktrees/test-next/locked` will prevent the\n+`test-next` entry from being pruned.  See\n+linkgit:gitrepository-layout[5] for details.\n+\n EXAMPLES\n --------\n \ndiff --git a/Documentation/git-prune.txt b/Documentation/git-prune.txt\nindex 7a493c8..a0ea381 100644\n--- a/Documentation/git-prune.txt\n+++ b/Documentation/git-prune.txt\n@@ -48,6 +48,9 @@ OPTIONS\n --expire <time>::\n \tOnly expire loose objects older than <time>.\n \n+--worktrees::\n+\tPrune stale worktree information in $GIT_DIR/worktrees.\n+\n <head>...::\n \tIn addition to objects\n \treachable from any of our references, keep objects\ndiff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt\nindex 8228450..2b30a92 100644\n--- a/Documentation/gitrepository-layout.txt\n+++ b/Documentation/gitrepository-layout.txt\n@@ -259,6 +259,25 @@ worktrees::\n \t$GIT_COMMON_DIR is set and \"$GIT_COMMON_DIR/worktrees\" will be\n \tused instead.\n \n+worktrees/<id>/gitdir::\n+\tA text file containing the absolute path back to the .git file\n+\tthat points to here. This is used to check if the linked\n+\trepository has been manually removed and there is no need to\n+\tkeep this directory any more. mtime of this file should be\n+\tupdated every time the linked repository is accessed.\n+\n+worktrees/<id>/locked::\n+\tIf this file exists, the linked repository may be on a\n+\tportable device and not available. It does not mean that the\n+\tlinked repository is gone and `worktrees/<id>` could be\n+\tremoved. The file's content contains a reason string on why\n+\tthe repository is locked.\n+\n+worktrees/<id>/link::\n+\tIf this file exists, it is a hard link to the linked .git\n+\tfile. It is used to detect if the linked repository is\n+\tmanually removed.\n+\n SEE ALSO\n --------\n linkgit:git-init[1],\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex ad10f99..ab46af9 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -826,7 +826,7 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,\n \tconst char *path = opts->new_worktree, *name;\n \tstruct stat st;\n \tstruct child_process cp;\n-\tint counter = 0, len;\n+\tint counter = 0, len, ret;\n \n \tif (!new->commit)\n \t\tdie(_(\"no branch specified\"));\n@@ -857,11 +857,21 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,\n \tif (mkdir(sb_repo.buf, 0777))\n \t\tdie_errno(_(\"could not create directory of '%s'\"), sb_repo.buf);\n \n+\t/*\n+\t * lock the incomplete repo so prune won't delete it, unlock\n+\t * after the preparation is over.\n+\t */\n+\tstrbuf_addf(&sb, \"%s/locked\", sb_repo.buf);\n+\twrite_file(sb.buf, 1, \"initializing\\n\");\n+\n \tstrbuf_addf(&sb_git, \"%s/.git\", path);\n \tif (safe_create_leading_directories_const(sb_git.buf))\n \t\tdie_errno(_(\"could not create leading directories of '%s'\"),\n \t\t\t  sb_git.buf);\n \n+\tstrbuf_reset(&sb);\n+\tstrbuf_addf(&sb, \"%s/gitdir\", sb_repo.buf);\n+\twrite_file(sb.buf, 1, \"%s\\n\", real_path(sb_git.buf));\n \twrite_file(sb_git.buf, 1, \"gitdir: %s/worktrees/%s\\n\",\n \t\t   real_path(get_git_common_dir()), name);\n \t/*\n@@ -870,6 +880,7 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,\n \t * value would do because this value will be ignored and\n \t * replaced at the next (real) checkout.\n \t */\n+\tstrbuf_reset(&sb);\n \tstrbuf_addf(&sb, \"%s/HEAD\", sb_repo.buf);\n \twrite_file(sb.buf, 1, \"%s\\n\", sha1_to_hex(new->commit->object.sha1));\n \tstrbuf_reset(&sb);\n@@ -885,7 +896,11 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,\n \tmemset(&cp, 0, sizeof(cp));\n \tcp.git_cmd = 1;\n \tcp.argv = opts->saved_argv;\n-\treturn run_command(&cp);\n+\tret = run_command(&cp);\n+\tstrbuf_reset(&sb);\n+\tstrbuf_addf(&sb, \"%s/locked\", sb_repo.buf);\n+\tunlink_or_warn(sb.buf);\n+\treturn ret;\n }\n \n static int git_checkout_config(const char *var, const char *value, void *cb)\ndiff --git a/builtin/prune.c b/builtin/prune.c\nindex 144a3bd..cf56110 100644\n--- a/builtin/prune.c\n+++ b/builtin/prune.c\n@@ -112,6 +112,91 @@ static void prune_object_dir(const char *path)\n \t}\n }\n \n+static int prune_worktree(const char *id, struct strbuf *reason)\n+{\n+\tstruct stat st;\n+\tchar *path;\n+\tint fd, len;\n+\n+\tif (!is_directory(git_path(\"worktrees/%s\", id))) {\n+\t\tstrbuf_addf(reason, _(\"Removing worktrees/%s: not a valid directory\"), id);\n+\t\treturn 1;\n+\t}\n+\tif (file_exists(git_path(\"worktrees/%s/locked\", id)))\n+\t\treturn 0;\n+\tif (stat(git_path(\"worktrees/%s/gitdir\", id), &st)) {\n+\t\tstrbuf_addf(reason, _(\"Removing worktrees/%s: gitdir file does not exist\"), id);\n+\t\treturn 1;\n+\t}\n+\tfd = open(git_path(\"worktrees/%s/gitdir\", id), O_RDONLY);\n+\tif (fd < 0) {\n+\t\tstrbuf_addf(reason, _(\"Removing worktrees/%s: unable to read gitdir file (%s)\"),\n+\t\t\t    id, strerror(errno));\n+\t\treturn 1;\n+\t}\n+\tlen = st.st_size;\n+\tpath = xmalloc(len + 1);\n+\tread_in_full(fd, path, len);\n+\tclose(fd);\n+\twhile (len && (path[len - 1] == '\\n' || path[len - 1] == '\\r'))\n+\t\tlen--;\n+\tif (!len) {\n+\t\tstrbuf_addf(reason, _(\"Removing worktrees/%s: invalid gitdir file\"), id);\n+\t\tfree(path);\n+\t\treturn 1;\n+\t}\n+\tpath[len] = '\\0';\n+\tif (!file_exists(path)) {\n+\t\tstruct stat st_link;\n+\t\tfree(path);\n+\t\t/*\n+\t\t * the repo is moved manually and has not been\n+\t\t * accessed since?\n+\t\t */\n+\t\tif (!stat(git_path(\"worktrees/%s/link\", id), &st_link) &&\n+\t\t    st_link.st_nlink > 1)\n+\t\t\treturn 0;\n+\t\tstrbuf_addf(reason, _(\"Removing worktrees/%s: gitdir file points to non-existent location\"), id);\n+\t\treturn 1;\n+\t}\n+\tfree(path);\n+\treturn st.st_mtime <= expire;\n+}\n+\n+static void prune_worktrees(void)\n+{\n+\tstruct strbuf reason = STRBUF_INIT;\n+\tstruct strbuf path = STRBUF_INIT;\n+\tDIR *dir = opendir(git_path(\"worktrees\"));\n+\tstruct dirent *d;\n+\tint ret;\n+\tif (!dir)\n+\t\treturn;\n+\twhile ((d = readdir(dir)) != NULL) {\n+\t\tif (!strcmp(d->d_name, \".\") || !strcmp(d->d_name, \"..\"))\n+\t\t\tcontinue;\n+\t\tstrbuf_reset(&reason);\n+\t\tif (!prune_worktree(d->d_name, &reason))\n+\t\t\tcontinue;\n+\t\tif (show_only || verbose)\n+\t\t\tprintf(\"%s\\n\", reason.buf);\n+\t\tif (show_only)\n+\t\t\tcontinue;\n+\t\tstrbuf_reset(&path);\n+\t\tstrbuf_addstr(&path, git_path(\"worktrees/%s\", d->d_name));\n+\t\tret = remove_dir_recursively(&path, 0);\n+\t\tif (ret < 0 && errno == ENOTDIR)\n+\t\t\tret = unlink(path.buf);\n+\t\tif (ret)\n+\t\t\terror(_(\"failed to remove: %s\"), strerror(errno));\n+\t}\n+\tclosedir(dir);\n+\tif (!show_only)\n+\t\trmdir(git_path(\"worktrees\"));\n+\tstrbuf_release(&reason);\n+\tstrbuf_release(&path);\n+}\n+\n /*\n  * Write errors (particularly out of space) can result in\n  * failed temporary packs (and more rarely indexes and other\n@@ -138,10 +223,12 @@ int cmd_prune(int argc, const char **argv, const char *prefix)\n {\n \tstruct rev_info revs;\n \tstruct progress *progress = NULL;\n+\tint do_prune_worktrees = 0;\n \tconst struct option options[] = {\n \t\tOPT__DRY_RUN(&show_only, N_(\"do not remove, show only\")),\n \t\tOPT__VERBOSE(&verbose, N_(\"report pruned objects\")),\n \t\tOPT_BOOL(0, \"progress\", &show_progress, N_(\"show progress\")),\n+\t\tOPT_BOOL(0, \"worktrees\", &do_prune_worktrees, N_(\"prune .git/worktrees\")),\n \t\tOPT_EXPIRY_DATE(0, \"expire\", &expire,\n \t\t\t\tN_(\"expire objects older than <time>\")),\n \t\tOPT_END()\n@@ -154,6 +241,14 @@ int cmd_prune(int argc, const char **argv, const char *prefix)\n \tinit_revisions(&revs, prefix);\n \n \targc = parse_options(argc, argv, prefix, options, prune_usage, 0);\n+\n+\tif (do_prune_worktrees) {\n+\t\tif (argc)\n+\t\t\tdie(_(\"--worktrees does not take extra arguments\"));\n+\t\tprune_worktrees();\n+\t\treturn 0;\n+\t}\n+\n \twhile (argc--) {\n \t\tunsigned char sha1[20];\n \t\tconst char *name = *argv++;\ndiff --git a/setup.c b/setup.c\nindex b99bdd3..fb61860 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -390,6 +390,17 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)\n \treturn ret;\n }\n \n+static void update_linked_gitdir(const char *gitfile, const char *gitdir)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\tstruct stat st;\n+\n+\tstrbuf_addf(&path, \"%s/gitfile\", gitdir);\n+\tif (stat(path.buf, &st) || st.st_mtime + 24 * 3600 < time(NULL))\n+\t\twrite_file(path.buf, 0, \"%s\\n\", gitfile);\n+\tstrbuf_release(&path);\n+}\n+\n /*\n  * Try to read the location of the git directory from the .git file,\n  * return path to git directory if found.\n@@ -438,6 +449,8 @@ const char *read_gitfile(const char *path)\n \n \tif (!is_git_directory(dir))\n \t\tdie(\"Not a git repository: %s\", dir);\n+\n+\tupdate_linked_gitdir(path, dir);\n \tpath = real_path(dir);\n \n \tfree(buf);\ndiff --git a/t/t2026-prune-linked-checkouts.sh b/t/t2026-prune-linked-checkouts.sh\nnew file mode 100755\nindex 0000000..3622800\n--- /dev/null\n+++ b/t/t2026-prune-linked-checkouts.sh\n@@ -0,0 +1,84 @@\n+#!/bin/sh\n+\n+test_description='prune $GIT_DIR/worktrees'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'prune --worktrees on normal repo' '\n+\tgit prune --worktrees &&\n+\ttest_must_fail git prune --worktrees abc\n+'\n+\n+test_expect_success 'prune files inside $GIT_DIR/worktrees' '\n+\tmkdir .git/worktrees &&\n+\t: >.git/worktrees/abc &&\n+\tgit prune --worktrees --verbose >actual &&\n+\tcat >expect <<EOF &&\n+Removing worktrees/abc: not a valid directory\n+EOF\n+\ttest_i18ncmp expect actual &&\n+\t! test -f .git/worktrees/abc &&\n+\t! test -d .git/worktrees\n+'\n+\n+test_expect_success 'prune directories without gitdir' '\n+\tmkdir -p .git/worktrees/def/abc &&\n+\t: >.git/worktrees/def/def &&\n+\tcat >expect <<EOF &&\n+Removing worktrees/def: gitdir file does not exist\n+EOF\n+\tgit prune --worktrees --verbose >actual &&\n+\ttest_i18ncmp expect actual &&\n+\t! test -d .git/worktrees/def &&\n+\t! test -d .git/worktrees\n+'\n+\n+test_expect_success POSIXPERM 'prune directories with unreadable gitdir' '\n+\tmkdir -p .git/worktrees/def/abc &&\n+\t: >.git/worktrees/def/def &&\n+\t: >.git/worktrees/def/gitdir &&\n+\tchmod u-r .git/worktrees/def/gitdir &&\n+\tgit prune --worktrees --verbose >actual &&\n+\ttest_i18ngrep \"Removing worktrees/def: unable to read gitdir file\" actual &&\n+\t! test -d .git/worktrees/def &&\n+\t! test -d .git/worktrees\n+'\n+\n+test_expect_success 'prune directories with invalid gitdir' '\n+\tmkdir -p .git/worktrees/def/abc &&\n+\t: >.git/worktrees/def/def &&\n+\t: >.git/worktrees/def/gitdir &&\n+\tgit prune --worktrees --verbose >actual &&\n+\ttest_i18ngrep \"Removing worktrees/def: invalid gitdir file\" actual &&\n+\t! test -d .git/worktrees/def &&\n+\t! test -d .git/worktrees\n+'\n+\n+test_expect_success 'prune directories with gitdir pointing to nowhere' '\n+\tmkdir -p .git/worktrees/def/abc &&\n+\t: >.git/worktrees/def/def &&\n+\techo \"$TRASH_DIRECTORY\"/nowhere >.git/worktrees/def/gitdir &&\n+\tgit prune --worktrees --verbose >actual &&\n+\ttest_i18ngrep \"Removing worktrees/def: gitdir file points to non-existent location\" actual &&\n+\t! test -d .git/worktrees/def &&\n+\t! test -d .git/worktrees\n+'\n+\n+test_expect_success 'not prune locked checkout' '\n+\ttest_when_finished rm -r .git/worktrees\n+\tmkdir -p .git/worktrees/ghi &&\n+\t: >.git/worktrees/ghi/locked &&\n+\tgit prune --worktrees &&\n+\ttest -d .git/worktrees/ghi\n+'\n+\n+test_expect_success 'not prune recent checkouts' '\n+\ttest_when_finished rm -r .git/worktrees\n+\tmkdir zz &&\n+\tmkdir -p .git/worktrees/jlm &&\n+\techo \"$TRASH_DIRECTORY\"/zz >.git/worktrees/jlm/gitdir &&\n+\tgit prune --worktrees --verbose --expire=2.days.ago &&\n+\ttest -d .git/worktrees/jlm\n+'\n+\n+test_done\n-- \n2.1.0.rc0.78.gc0d8480\n\n-- 8< --\n"},{"id":"249723","messageId":"20140921104330.GA10475@lanh","threadId":"37450","inReplyTo":"1410388928-32265-29-git-send-email-pclouds@gmail.com","subject":"Re: [PATCH v2 28/32] gc: support prune --worktrees","fromName":"Duy Nguyen","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-21T10:43:30Z","receivedAt":"2014-09-21T10:43:30Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"And this is the update as suggested in 23/32 [1]\n\n[1] http://thread.gmane.org/gmane.comp.version-control.git/256210/focus=256849\n\n-- 8< --\nSubject: [PATCH] gc: support prune --worktrees\n\nHelped-by: Marc Branchaud <marcnarc@xiplink.com>\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n Documentation/config.txt       |  7 +++++++\n Documentation/git-checkout.txt | 11 +++++++----\n builtin/gc.c                   | 10 ++++++++++\n 3 files changed, 24 insertions(+), 4 deletions(-)\n\ndiff --git a/Documentation/config.txt b/Documentation/config.txt\nindex 98b8ef0..8351c8a 100644\n--- a/Documentation/config.txt\n+++ b/Documentation/config.txt\n@@ -1212,6 +1212,13 @@ gc.pruneexpire::\n \t\"now\" may be used to disable this  grace period and always prune\n \tunreachable objects immediately.\n \n+gc.pruneworktreesexpire::\n+\tWhen 'git gc' is run, it will call\n+\t'prune --worktrees --expire 3.months.ago'.\n+\tOverride the grace period with this config variable. The value\n+\t\"now\" may be used to disable the grace period and prune\n+\t$GIT_DIR/worktrees immediately.\n+\n gc.reflogexpire::\n gc.<pattern>.reflogexpire::\n \t'git reflog expire' removes reflog entries older than\ndiff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt\nindex 0fd3bab..6c14710 100644\n--- a/Documentation/git-checkout.txt\n+++ b/Documentation/git-checkout.txt\n@@ -435,8 +435,11 @@ $GIT_DIR or $GIT_COMMON_DIR when you need to directly access something\n inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.\n \n When you are done with a linked working tree you can simply delete it.\n-You can clean up any stale $GIT_DIR/worktrees entries via `git prune\n---worktrees` in the main worktree or any linked worktree.\n+The working tree's entry in the repository's $GIT_DIR/worktrees\n+directory will eventually be removed automatically (see\n+`gc.pruneworktreesexpire` in linkgit::git-config[1]), or you can run\n+`git prune --worktrees` in the main worktree or any linked worktree to\n+clean up any stale entries in $GIT_DIR/worktrees.\n \n If you move a linked working directory to another file system, or\n within a file system that does not support hard links, you need to run\n@@ -444,8 +447,8 @@ at least one git command inside the linked working directory\n (e.g. `git status`) in order to update its entry in $GIT_DIR/worktrees\n so that it does not get automatically removed.\n \n-To prevent `git prune --worktrees` from deleting a $GIT_DIR/worktrees\n-entry (which can be useful in some situations, such as when the\n+To prevent a $GIT_DIR/worktrees entry from from being pruned (which\n+can be useful in some situations, such as when the\n entry's working tree is stored on a portable device), add a file named\n 'locked' to the entry's directory. The file contains the reason in\n plain text. For example, if a linked working tree's `.git` file points\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 849a87c..35542f3 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -33,11 +33,13 @@ static int gc_auto_threshold = 6700;\n static int gc_auto_pack_limit = 50;\n static int detach_auto = 1;\n static const char *prune_expire = \"2.weeks.ago\";\n+static const char *prune_worktrees_expire = \"3.months.ago\";\n \n static struct argv_array pack_refs_cmd = ARGV_ARRAY_INIT;\n static struct argv_array reflog = ARGV_ARRAY_INIT;\n static struct argv_array repack = ARGV_ARRAY_INIT;\n static struct argv_array prune = ARGV_ARRAY_INIT;\n+static struct argv_array prune_worktrees = ARGV_ARRAY_INIT;\n static struct argv_array rerere = ARGV_ARRAY_INIT;\n \n static char *pidfile;\n@@ -83,6 +85,7 @@ static void gc_config(void)\n \tgit_config_get_int(\"gc.autopacklimit\", &gc_auto_pack_limit);\n \tgit_config_get_bool(\"gc.autodetach\", &detach_auto);\n \tgit_config_date_string(\"gc.pruneexpire\", &prune_expire);\n+\tgit_config_date_string(\"gc.pruneworktreesexpire\", &prune_worktrees_expire);\n \tgit_config(git_default_config, NULL);\n }\n \n@@ -290,6 +293,7 @@ int cmd_gc(int argc, const char **argv, const char *prefix)\n \targv_array_pushl(&reflog, \"reflog\", \"expire\", \"--all\", NULL);\n \targv_array_pushl(&repack, \"repack\", \"-d\", \"-l\", NULL);\n \targv_array_pushl(&prune, \"prune\", \"--expire\", NULL);\n+\targv_array_pushl(&prune_worktrees, \"prune\", \"--worktrees\", \"--expire\", NULL);\n \targv_array_pushl(&rerere, \"rerere\", \"gc\", NULL);\n \n \tgc_config();\n@@ -359,6 +363,12 @@ int cmd_gc(int argc, const char **argv, const char *prefix)\n \t\t\treturn error(FAILED_RUN, prune.argv[0]);\n \t}\n \n+\tif (prune_worktrees_expire) {\n+\t\targv_array_push(&prune_worktrees, prune_worktrees_expire);\n+\t\tif (run_command_v_opt(prune_worktrees.argv, RUN_GIT_CMD))\n+\t\t\treturn error(FAILED_RUN, prune_worktrees.argv[0]);\n+\t}\n+\n \tif (run_command_v_opt(rerere.argv, RUN_GIT_CMD))\n \t\treturn error(FAILED_RUN, rerere.argv[0]);\n \n-- \n2.1.0.rc0.78.gc0d8480\n\n\n\n-- 8< --\n"},{"id":"249752","messageId":"54208E01.2060205@xiplink.com","threadId":"37450","inReplyTo":"CACsJy8CH8mQjexFU1jO1orzajiwTXy4wJ022nq1PEY2GLUGAZA@mail.gmail.com","subject":"Re: [PATCH v2 22/32] checkout: support checking out into a new working directory","fromName":"Marc Branchaud","fromEmail":"marcnarc@xiplink.com","sentAt":"2014-09-22T21:00:49Z","receivedAt":"2014-09-22T21:00:49Z","isPatch":true,"sender":{"key":"marcnarc@xiplink.com","avatar":"https://avatars.githubusercontent.com/u/14980203?v=4"},"body":"On 14-09-21 05:50 AM, Duy Nguyen wrote:\n> On Sun, Sep 21, 2014 at 10:10 AM, Eric Sunshine <sunshine@sunshineco.com> wrote:\n>> Would it make sense for this \"rule of thumb\" summary to be presented\n>> first, and then the explanation of that rule after, rather than the\n>> reverse as is currently the case?\n> \n> You mean like this?\n\n*shrug*  To me it seems better to leave it at the end here.\n\nIf we really want people to follow this advice, it should appear where\nGIT_DIR is officially documented.  I think this is the Environment Variables\nsection of the Documentation/git.txt file.  Probably GIT_COMMON_DIR should\nalso be documented there.\n\n(Sorry, I don't have time to whip up a patch right now.)\n\n\t\tM.\n\n\n> diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt\n> index c101575..fd77631 100644\n> --- a/Documentation/git-checkout.txt\n> +++ b/Documentation/git-checkout.txt\n> @@ -420,6 +420,11 @@ $GIT_COMMON_DIR is set to point back to the main\n> working tree's $GIT_DIR\n>  (e.g. `/path/main/.git`). These settings are made in a `.git` file located at\n>  the top directory of the linked working tree.\n> \n> +See linkgit:gitrepository-layout[5] for more information. The rule of\n> +thumb is do not make any assumption about whether a path belongs to\n> +$GIT_DIR or $GIT_COMMON_DIR when you need to directly access something\n> +inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.\n> +\n>  Path resolution via `git rev-parse --git-path` uses either\n>  $GIT_DIR or $GIT_COMMON_DIR depending on the path. For example, in the\n>  linked working tree `git rev-parse --git-path HEAD` returns\n> @@ -429,11 +434,6 @@ rev-parse --git-path refs/heads/master` uses\n>  $GIT_COMMON_DIR and returns `/path/main/.git/refs/heads/master`,\n>  since refs are shared across all working trees.\n> \n> -See linkgit:gitrepository-layout[5] for more information. The rule of\n> -thumb is do not make any assumption about whether a path belongs to\n> -$GIT_DIR or $GIT_COMMON_DIR when you need to directly access something\n> -inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.\n> -\n>  EXAMPLES\n>  --------\n> \n> \n> \n"},{"id":"249753","messageId":"54208F3E.1030609@xiplink.com","threadId":"37450","inReplyTo":"20140921102936.GA9438@lanh","subject":"Re: [PATCH v2 23/32] prune: strategies for linked checkouts","fromName":"Marc Branchaud","fromEmail":"marcnarc@xiplink.com","sentAt":"2014-09-22T21:06:06Z","receivedAt":"2014-09-22T21:06:06Z","isPatch":true,"sender":{"key":"marcnarc@xiplink.com","avatar":"https://avatars.githubusercontent.com/u/14980203?v=4"},"body":"On 14-09-21 06:29 AM, Duy Nguyen wrote:\n> Here we go again. Thanks both for the suggestions.\n\nThe documentation looks good to me.\n\nFWIW:\nSigned-off-by: Marc Branchaud <marcnarc@xiplink.com>\n\n\t\tM.\n\n\n> -- 8< --\n> Subject: [PATCH] prune: strategies for linked checkouts\n> \n> (alias R=$GIT_COMMON_DIR/worktrees/<id>)\n> \n>  - linked checkouts are supposed to keep its location in $R/gitdir up\n>    to date. The use case is auto fixup after a manual checkout move.\n> \n>  - linked checkouts are supposed to update mtime of $R/gitdir. If\n>    $R/gitdir's mtime is older than a limit, and it points to nowhere,\n>    worktrees/<id> is to be pruned.\n> \n>  - If $R/locked exists, worktrees/<id> is not supposed to be pruned. If\n>    $R/locked exists and $R/gitdir's mtime is older than a really long\n>    limit, warn about old unused repo.\n> \n>  - \"git checkout --to\" is supposed to make a hard link named $R/link\n>    pointing to the .git file on supported file systems to help detect\n>    the user manually deleting the checkout. If $R/link exists and its\n>    link count is greated than 1, the repo is kept.\n> \n> Helped-by: Marc Branchaud <marcnarc@xiplink.com>\n> Helped-by: Eric Sunshine <sunshine@sunshineco.com>\n> Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n> ---\n>  Documentation/git-checkout.txt             | 20 +++++++\n>  Documentation/git-prune.txt                |  3 +\n>  Documentation/gitrepository-layout.txt     | 19 ++++++\n>  builtin/checkout.c                         | 19 +++++-\n>  builtin/prune.c                            | 95 ++++++++++++++++++++++++++++++\n>  setup.c                                    | 13 ++++\n>  t/t2026-prune-linked-checkouts.sh (new +x) | 84 ++++++++++++++++++++++++++\n>  7 files changed, 251 insertions(+), 2 deletions(-)\n>  create mode 100755 t/t2026-prune-linked-checkouts.sh\n> \n> diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt\n> index c101575..0fd3bab 100644\n> --- a/Documentation/git-checkout.txt\n> +++ b/Documentation/git-checkout.txt\n> @@ -434,6 +434,26 @@ thumb is do not make any assumption about whether a path belongs to\n>  $GIT_DIR or $GIT_COMMON_DIR when you need to directly access something\n>  inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.\n>  \n> +When you are done with a linked working tree you can simply delete it.\n> +You can clean up any stale $GIT_DIR/worktrees entries via `git prune\n> +--worktrees` in the main worktree or any linked worktree.\n> +\n> +If you move a linked working directory to another file system, or\n> +within a file system that does not support hard links, you need to run\n> +at least one git command inside the linked working directory\n> +(e.g. `git status`) in order to update its entry in $GIT_DIR/worktrees\n> +so that it does not get automatically removed.\n> +\n> +To prevent `git prune --worktrees` from deleting a $GIT_DIR/worktrees\n> +entry (which can be useful in some situations, such as when the\n> +entry's working tree is stored on a portable device), add a file named\n> +'locked' to the entry's directory. The file contains the reason in\n> +plain text. For example, if a linked working tree's `.git` file points\n> +to `/path/main/.git/worktrees/test-next` then a file named\n> +`/path/main/.git/worktrees/test-next/locked` will prevent the\n> +`test-next` entry from being pruned.  See\n> +linkgit:gitrepository-layout[5] for details.\n> +\n>  EXAMPLES\n>  --------\n>  \n> diff --git a/Documentation/git-prune.txt b/Documentation/git-prune.txt\n> index 7a493c8..a0ea381 100644\n> --- a/Documentation/git-prune.txt\n> +++ b/Documentation/git-prune.txt\n> @@ -48,6 +48,9 @@ OPTIONS\n>  --expire <time>::\n>  \tOnly expire loose objects older than <time>.\n>  \n> +--worktrees::\n> +\tPrune stale worktree information in $GIT_DIR/worktrees.\n> +\n>  <head>...::\n>  \tIn addition to objects\n>  \treachable from any of our references, keep objects\n> diff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt\n> index 8228450..2b30a92 100644\n> --- a/Documentation/gitrepository-layout.txt\n> +++ b/Documentation/gitrepository-layout.txt\n> @@ -259,6 +259,25 @@ worktrees::\n>  \t$GIT_COMMON_DIR is set and \"$GIT_COMMON_DIR/worktrees\" will be\n>  \tused instead.\n>  \n> +worktrees/<id>/gitdir::\n> +\tA text file containing the absolute path back to the .git file\n> +\tthat points to here. This is used to check if the linked\n> +\trepository has been manually removed and there is no need to\n> +\tkeep this directory any more. mtime of this file should be\n> +\tupdated every time the linked repository is accessed.\n> +\n> +worktrees/<id>/locked::\n> +\tIf this file exists, the linked repository may be on a\n> +\tportable device and not available. It does not mean that the\n> +\tlinked repository is gone and `worktrees/<id>` could be\n> +\tremoved. The file's content contains a reason string on why\n> +\tthe repository is locked.\n> +\n> +worktrees/<id>/link::\n> +\tIf this file exists, it is a hard link to the linked .git\n> +\tfile. It is used to detect if the linked repository is\n> +\tmanually removed.\n> +\n>  SEE ALSO\n>  --------\n>  linkgit:git-init[1],\n> diff --git a/builtin/checkout.c b/builtin/checkout.c\n> index ad10f99..ab46af9 100644\n> --- a/builtin/checkout.c\n> +++ b/builtin/checkout.c\n> @@ -826,7 +826,7 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,\n>  \tconst char *path = opts->new_worktree, *name;\n>  \tstruct stat st;\n>  \tstruct child_process cp;\n> -\tint counter = 0, len;\n> +\tint counter = 0, len, ret;\n>  \n>  \tif (!new->commit)\n>  \t\tdie(_(\"no branch specified\"));\n> @@ -857,11 +857,21 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,\n>  \tif (mkdir(sb_repo.buf, 0777))\n>  \t\tdie_errno(_(\"could not create directory of '%s'\"), sb_repo.buf);\n>  \n> +\t/*\n> +\t * lock the incomplete repo so prune won't delete it, unlock\n> +\t * after the preparation is over.\n> +\t */\n> +\tstrbuf_addf(&sb, \"%s/locked\", sb_repo.buf);\n> +\twrite_file(sb.buf, 1, \"initializing\\n\");\n> +\n>  \tstrbuf_addf(&sb_git, \"%s/.git\", path);\n>  \tif (safe_create_leading_directories_const(sb_git.buf))\n>  \t\tdie_errno(_(\"could not create leading directories of '%s'\"),\n>  \t\t\t  sb_git.buf);\n>  \n> +\tstrbuf_reset(&sb);\n> +\tstrbuf_addf(&sb, \"%s/gitdir\", sb_repo.buf);\n> +\twrite_file(sb.buf, 1, \"%s\\n\", real_path(sb_git.buf));\n>  \twrite_file(sb_git.buf, 1, \"gitdir: %s/worktrees/%s\\n\",\n>  \t\t   real_path(get_git_common_dir()), name);\n>  \t/*\n> @@ -870,6 +880,7 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,\n>  \t * value would do because this value will be ignored and\n>  \t * replaced at the next (real) checkout.\n>  \t */\n> +\tstrbuf_reset(&sb);\n>  \tstrbuf_addf(&sb, \"%s/HEAD\", sb_repo.buf);\n>  \twrite_file(sb.buf, 1, \"%s\\n\", sha1_to_hex(new->commit->object.sha1));\n>  \tstrbuf_reset(&sb);\n> @@ -885,7 +896,11 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,\n>  \tmemset(&cp, 0, sizeof(cp));\n>  \tcp.git_cmd = 1;\n>  \tcp.argv = opts->saved_argv;\n> -\treturn run_command(&cp);\n> +\tret = run_command(&cp);\n> +\tstrbuf_reset(&sb);\n> +\tstrbuf_addf(&sb, \"%s/locked\", sb_repo.buf);\n> +\tunlink_or_warn(sb.buf);\n> +\treturn ret;\n>  }\n>  \n>  static int git_checkout_config(const char *var, const char *value, void *cb)\n> diff --git a/builtin/prune.c b/builtin/prune.c\n> index 144a3bd..cf56110 100644\n> --- a/builtin/prune.c\n> +++ b/builtin/prune.c\n> @@ -112,6 +112,91 @@ static void prune_object_dir(const char *path)\n>  \t}\n>  }\n>  \n> +static int prune_worktree(const char *id, struct strbuf *reason)\n> +{\n> +\tstruct stat st;\n> +\tchar *path;\n> +\tint fd, len;\n> +\n> +\tif (!is_directory(git_path(\"worktrees/%s\", id))) {\n> +\t\tstrbuf_addf(reason, _(\"Removing worktrees/%s: not a valid directory\"), id);\n> +\t\treturn 1;\n> +\t}\n> +\tif (file_exists(git_path(\"worktrees/%s/locked\", id)))\n> +\t\treturn 0;\n> +\tif (stat(git_path(\"worktrees/%s/gitdir\", id), &st)) {\n> +\t\tstrbuf_addf(reason, _(\"Removing worktrees/%s: gitdir file does not exist\"), id);\n> +\t\treturn 1;\n> +\t}\n> +\tfd = open(git_path(\"worktrees/%s/gitdir\", id), O_RDONLY);\n> +\tif (fd < 0) {\n> +\t\tstrbuf_addf(reason, _(\"Removing worktrees/%s: unable to read gitdir file (%s)\"),\n> +\t\t\t    id, strerror(errno));\n> +\t\treturn 1;\n> +\t}\n> +\tlen = st.st_size;\n> +\tpath = xmalloc(len + 1);\n> +\tread_in_full(fd, path, len);\n> +\tclose(fd);\n> +\twhile (len && (path[len - 1] == '\\n' || path[len - 1] == '\\r'))\n> +\t\tlen--;\n> +\tif (!len) {\n> +\t\tstrbuf_addf(reason, _(\"Removing worktrees/%s: invalid gitdir file\"), id);\n> +\t\tfree(path);\n> +\t\treturn 1;\n> +\t}\n> +\tpath[len] = '\\0';\n> +\tif (!file_exists(path)) {\n> +\t\tstruct stat st_link;\n> +\t\tfree(path);\n> +\t\t/*\n> +\t\t * the repo is moved manually and has not been\n> +\t\t * accessed since?\n> +\t\t */\n> +\t\tif (!stat(git_path(\"worktrees/%s/link\", id), &st_link) &&\n> +\t\t    st_link.st_nlink > 1)\n> +\t\t\treturn 0;\n> +\t\tstrbuf_addf(reason, _(\"Removing worktrees/%s: gitdir file points to non-existent location\"), id);\n> +\t\treturn 1;\n> +\t}\n> +\tfree(path);\n> +\treturn st.st_mtime <= expire;\n> +}\n> +\n> +static void prune_worktrees(void)\n> +{\n> +\tstruct strbuf reason = STRBUF_INIT;\n> +\tstruct strbuf path = STRBUF_INIT;\n> +\tDIR *dir = opendir(git_path(\"worktrees\"));\n> +\tstruct dirent *d;\n> +\tint ret;\n> +\tif (!dir)\n> +\t\treturn;\n> +\twhile ((d = readdir(dir)) != NULL) {\n> +\t\tif (!strcmp(d->d_name, \".\") || !strcmp(d->d_name, \"..\"))\n> +\t\t\tcontinue;\n> +\t\tstrbuf_reset(&reason);\n> +\t\tif (!prune_worktree(d->d_name, &reason))\n> +\t\t\tcontinue;\n> +\t\tif (show_only || verbose)\n> +\t\t\tprintf(\"%s\\n\", reason.buf);\n> +\t\tif (show_only)\n> +\t\t\tcontinue;\n> +\t\tstrbuf_reset(&path);\n> +\t\tstrbuf_addstr(&path, git_path(\"worktrees/%s\", d->d_name));\n> +\t\tret = remove_dir_recursively(&path, 0);\n> +\t\tif (ret < 0 && errno == ENOTDIR)\n> +\t\t\tret = unlink(path.buf);\n> +\t\tif (ret)\n> +\t\t\terror(_(\"failed to remove: %s\"), strerror(errno));\n> +\t}\n> +\tclosedir(dir);\n> +\tif (!show_only)\n> +\t\trmdir(git_path(\"worktrees\"));\n> +\tstrbuf_release(&reason);\n> +\tstrbuf_release(&path);\n> +}\n> +\n>  /*\n>   * Write errors (particularly out of space) can result in\n>   * failed temporary packs (and more rarely indexes and other\n> @@ -138,10 +223,12 @@ int cmd_prune(int argc, const char **argv, const char *prefix)\n>  {\n>  \tstruct rev_info revs;\n>  \tstruct progress *progress = NULL;\n> +\tint do_prune_worktrees = 0;\n>  \tconst struct option options[] = {\n>  \t\tOPT__DRY_RUN(&show_only, N_(\"do not remove, show only\")),\n>  \t\tOPT__VERBOSE(&verbose, N_(\"report pruned objects\")),\n>  \t\tOPT_BOOL(0, \"progress\", &show_progress, N_(\"show progress\")),\n> +\t\tOPT_BOOL(0, \"worktrees\", &do_prune_worktrees, N_(\"prune .git/worktrees\")),\n>  \t\tOPT_EXPIRY_DATE(0, \"expire\", &expire,\n>  \t\t\t\tN_(\"expire objects older than <time>\")),\n>  \t\tOPT_END()\n> @@ -154,6 +241,14 @@ int cmd_prune(int argc, const char **argv, const char *prefix)\n>  \tinit_revisions(&revs, prefix);\n>  \n>  \targc = parse_options(argc, argv, prefix, options, prune_usage, 0);\n> +\n> +\tif (do_prune_worktrees) {\n> +\t\tif (argc)\n> +\t\t\tdie(_(\"--worktrees does not take extra arguments\"));\n> +\t\tprune_worktrees();\n> +\t\treturn 0;\n> +\t}\n> +\n>  \twhile (argc--) {\n>  \t\tunsigned char sha1[20];\n>  \t\tconst char *name = *argv++;\n> diff --git a/setup.c b/setup.c\n> index b99bdd3..fb61860 100644\n> --- a/setup.c\n> +++ b/setup.c\n> @@ -390,6 +390,17 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)\n>  \treturn ret;\n>  }\n>  \n> +static void update_linked_gitdir(const char *gitfile, const char *gitdir)\n> +{\n> +\tstruct strbuf path = STRBUF_INIT;\n> +\tstruct stat st;\n> +\n> +\tstrbuf_addf(&path, \"%s/gitfile\", gitdir);\n> +\tif (stat(path.buf, &st) || st.st_mtime + 24 * 3600 < time(NULL))\n> +\t\twrite_file(path.buf, 0, \"%s\\n\", gitfile);\n> +\tstrbuf_release(&path);\n> +}\n> +\n>  /*\n>   * Try to read the location of the git directory from the .git file,\n>   * return path to git directory if found.\n> @@ -438,6 +449,8 @@ const char *read_gitfile(const char *path)\n>  \n>  \tif (!is_git_directory(dir))\n>  \t\tdie(\"Not a git repository: %s\", dir);\n> +\n> +\tupdate_linked_gitdir(path, dir);\n>  \tpath = real_path(dir);\n>  \n>  \tfree(buf);\n> diff --git a/t/t2026-prune-linked-checkouts.sh b/t/t2026-prune-linked-checkouts.sh\n> new file mode 100755\n> index 0000000..3622800\n> --- /dev/null\n> +++ b/t/t2026-prune-linked-checkouts.sh\n> @@ -0,0 +1,84 @@\n> +#!/bin/sh\n> +\n> +test_description='prune $GIT_DIR/worktrees'\n> +\n> +. ./test-lib.sh\n> +\n> +test_expect_success 'prune --worktrees on normal repo' '\n> +\tgit prune --worktrees &&\n> +\ttest_must_fail git prune --worktrees abc\n> +'\n> +\n> +test_expect_success 'prune files inside $GIT_DIR/worktrees' '\n> +\tmkdir .git/worktrees &&\n> +\t: >.git/worktrees/abc &&\n> +\tgit prune --worktrees --verbose >actual &&\n> +\tcat >expect <<EOF &&\n> +Removing worktrees/abc: not a valid directory\n> +EOF\n> +\ttest_i18ncmp expect actual &&\n> +\t! test -f .git/worktrees/abc &&\n> +\t! test -d .git/worktrees\n> +'\n> +\n> +test_expect_success 'prune directories without gitdir' '\n> +\tmkdir -p .git/worktrees/def/abc &&\n> +\t: >.git/worktrees/def/def &&\n> +\tcat >expect <<EOF &&\n> +Removing worktrees/def: gitdir file does not exist\n> +EOF\n> +\tgit prune --worktrees --verbose >actual &&\n> +\ttest_i18ncmp expect actual &&\n> +\t! test -d .git/worktrees/def &&\n> +\t! test -d .git/worktrees\n> +'\n> +\n> +test_expect_success POSIXPERM 'prune directories with unreadable gitdir' '\n> +\tmkdir -p .git/worktrees/def/abc &&\n> +\t: >.git/worktrees/def/def &&\n> +\t: >.git/worktrees/def/gitdir &&\n> +\tchmod u-r .git/worktrees/def/gitdir &&\n> +\tgit prune --worktrees --verbose >actual &&\n> +\ttest_i18ngrep \"Removing worktrees/def: unable to read gitdir file\" actual &&\n> +\t! test -d .git/worktrees/def &&\n> +\t! test -d .git/worktrees\n> +'\n> +\n> +test_expect_success 'prune directories with invalid gitdir' '\n> +\tmkdir -p .git/worktrees/def/abc &&\n> +\t: >.git/worktrees/def/def &&\n> +\t: >.git/worktrees/def/gitdir &&\n> +\tgit prune --worktrees --verbose >actual &&\n> +\ttest_i18ngrep \"Removing worktrees/def: invalid gitdir file\" actual &&\n> +\t! test -d .git/worktrees/def &&\n> +\t! test -d .git/worktrees\n> +'\n> +\n> +test_expect_success 'prune directories with gitdir pointing to nowhere' '\n> +\tmkdir -p .git/worktrees/def/abc &&\n> +\t: >.git/worktrees/def/def &&\n> +\techo \"$TRASH_DIRECTORY\"/nowhere >.git/worktrees/def/gitdir &&\n> +\tgit prune --worktrees --verbose >actual &&\n> +\ttest_i18ngrep \"Removing worktrees/def: gitdir file points to non-existent location\" actual &&\n> +\t! test -d .git/worktrees/def &&\n> +\t! test -d .git/worktrees\n> +'\n> +\n> +test_expect_success 'not prune locked checkout' '\n> +\ttest_when_finished rm -r .git/worktrees\n> +\tmkdir -p .git/worktrees/ghi &&\n> +\t: >.git/worktrees/ghi/locked &&\n> +\tgit prune --worktrees &&\n> +\ttest -d .git/worktrees/ghi\n> +'\n> +\n> +test_expect_success 'not prune recent checkouts' '\n> +\ttest_when_finished rm -r .git/worktrees\n> +\tmkdir zz &&\n> +\tmkdir -p .git/worktrees/jlm &&\n> +\techo \"$TRASH_DIRECTORY\"/zz >.git/worktrees/jlm/gitdir &&\n> +\tgit prune --worktrees --verbose --expire=2.days.ago &&\n> +\ttest -d .git/worktrees/jlm\n> +'\n> +\n> +test_done\n> \n"},{"id":"249754","messageId":"54208F58.1040604@xiplink.com","threadId":"37450","inReplyTo":"20140921104330.GA10475@lanh","subject":"Re: [PATCH v2 28/32] gc: support prune --worktrees","fromName":"Marc Branchaud","fromEmail":"marcnarc@xiplink.com","sentAt":"2014-09-22T21:06:32Z","receivedAt":"2014-09-22T21:06:32Z","isPatch":true,"sender":{"key":"marcnarc@xiplink.com","avatar":"https://avatars.githubusercontent.com/u/14980203?v=4"},"body":"On 14-09-21 06:43 AM, Duy Nguyen wrote:\n> And this is the update as suggested in 23/32 [1]\n> \n> [1] http://thread.gmane.org/gmane.comp.version-control.git/256210/focus=256849\n\nLooks good!\n\nFWIW:\nSigned-off-by: Marc Branchaud <marcnarc@xiplink.com>\n\n\t\tM.\n\n\n> -- 8< --\n> Subject: [PATCH] gc: support prune --worktrees\n> \n> Helped-by: Marc Branchaud <marcnarc@xiplink.com>\n> Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n> ---\n>  Documentation/config.txt       |  7 +++++++\n>  Documentation/git-checkout.txt | 11 +++++++----\n>  builtin/gc.c                   | 10 ++++++++++\n>  3 files changed, 24 insertions(+), 4 deletions(-)\n> \n> diff --git a/Documentation/config.txt b/Documentation/config.txt\n> index 98b8ef0..8351c8a 100644\n> --- a/Documentation/config.txt\n> +++ b/Documentation/config.txt\n> @@ -1212,6 +1212,13 @@ gc.pruneexpire::\n>  \t\"now\" may be used to disable this  grace period and always prune\n>  \tunreachable objects immediately.\n>  \n> +gc.pruneworktreesexpire::\n> +\tWhen 'git gc' is run, it will call\n> +\t'prune --worktrees --expire 3.months.ago'.\n> +\tOverride the grace period with this config variable. The value\n> +\t\"now\" may be used to disable the grace period and prune\n> +\t$GIT_DIR/worktrees immediately.\n> +\n>  gc.reflogexpire::\n>  gc.<pattern>.reflogexpire::\n>  \t'git reflog expire' removes reflog entries older than\n> diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt\n> index 0fd3bab..6c14710 100644\n> --- a/Documentation/git-checkout.txt\n> +++ b/Documentation/git-checkout.txt\n> @@ -435,8 +435,11 @@ $GIT_DIR or $GIT_COMMON_DIR when you need to directly access something\n>  inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.\n>  \n>  When you are done with a linked working tree you can simply delete it.\n> -You can clean up any stale $GIT_DIR/worktrees entries via `git prune\n> ---worktrees` in the main worktree or any linked worktree.\n> +The working tree's entry in the repository's $GIT_DIR/worktrees\n> +directory will eventually be removed automatically (see\n> +`gc.pruneworktreesexpire` in linkgit::git-config[1]), or you can run\n> +`git prune --worktrees` in the main worktree or any linked worktree to\n> +clean up any stale entries in $GIT_DIR/worktrees.\n>  \n>  If you move a linked working directory to another file system, or\n>  within a file system that does not support hard links, you need to run\n> @@ -444,8 +447,8 @@ at least one git command inside the linked working directory\n>  (e.g. `git status`) in order to update its entry in $GIT_DIR/worktrees\n>  so that it does not get automatically removed.\n>  \n> -To prevent `git prune --worktrees` from deleting a $GIT_DIR/worktrees\n> -entry (which can be useful in some situations, such as when the\n> +To prevent a $GIT_DIR/worktrees entry from from being pruned (which\n> +can be useful in some situations, such as when the\n>  entry's working tree is stored on a portable device), add a file named\n>  'locked' to the entry's directory. The file contains the reason in\n>  plain text. For example, if a linked working tree's `.git` file points\n> diff --git a/builtin/gc.c b/builtin/gc.c\n> index 849a87c..35542f3 100644\n> --- a/builtin/gc.c\n> +++ b/builtin/gc.c\n> @@ -33,11 +33,13 @@ static int gc_auto_threshold = 6700;\n>  static int gc_auto_pack_limit = 50;\n>  static int detach_auto = 1;\n>  static const char *prune_expire = \"2.weeks.ago\";\n> +static const char *prune_worktrees_expire = \"3.months.ago\";\n>  \n>  static struct argv_array pack_refs_cmd = ARGV_ARRAY_INIT;\n>  static struct argv_array reflog = ARGV_ARRAY_INIT;\n>  static struct argv_array repack = ARGV_ARRAY_INIT;\n>  static struct argv_array prune = ARGV_ARRAY_INIT;\n> +static struct argv_array prune_worktrees = ARGV_ARRAY_INIT;\n>  static struct argv_array rerere = ARGV_ARRAY_INIT;\n>  \n>  static char *pidfile;\n> @@ -83,6 +85,7 @@ static void gc_config(void)\n>  \tgit_config_get_int(\"gc.autopacklimit\", &gc_auto_pack_limit);\n>  \tgit_config_get_bool(\"gc.autodetach\", &detach_auto);\n>  \tgit_config_date_string(\"gc.pruneexpire\", &prune_expire);\n> +\tgit_config_date_string(\"gc.pruneworktreesexpire\", &prune_worktrees_expire);\n>  \tgit_config(git_default_config, NULL);\n>  }\n>  \n> @@ -290,6 +293,7 @@ int cmd_gc(int argc, const char **argv, const char *prefix)\n>  \targv_array_pushl(&reflog, \"reflog\", \"expire\", \"--all\", NULL);\n>  \targv_array_pushl(&repack, \"repack\", \"-d\", \"-l\", NULL);\n>  \targv_array_pushl(&prune, \"prune\", \"--expire\", NULL);\n> +\targv_array_pushl(&prune_worktrees, \"prune\", \"--worktrees\", \"--expire\", NULL);\n>  \targv_array_pushl(&rerere, \"rerere\", \"gc\", NULL);\n>  \n>  \tgc_config();\n> @@ -359,6 +363,12 @@ int cmd_gc(int argc, const char **argv, const char *prefix)\n>  \t\t\treturn error(FAILED_RUN, prune.argv[0]);\n>  \t}\n>  \n> +\tif (prune_worktrees_expire) {\n> +\t\targv_array_push(&prune_worktrees, prune_worktrees_expire);\n> +\t\tif (run_command_v_opt(prune_worktrees.argv, RUN_GIT_CMD))\n> +\t\t\treturn error(FAILED_RUN, prune_worktrees.argv[0]);\n> +\t}\n> +\n>  \tif (run_command_v_opt(rerere.argv, RUN_GIT_CMD))\n>  \t\treturn error(FAILED_RUN, rerere.argv[0]);\n>  \n> \n"},{"id":"249759","messageId":"CAPig+cTM54zuNDTJzuxHt7=e5NkLG3rXrj8P2HZUOU08yCcGEw@mail.gmail.com","threadId":"37450","inReplyTo":"20140921102936.GA9438@lanh","subject":"Re: [PATCH v2 23/32] prune: strategies for linked checkouts","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2014-09-22T22:19:55Z","receivedAt":"2014-09-22T22:19:55Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Sun, Sep 21, 2014 at 6:29 AM, Duy Nguyen <pclouds@gmail.com> wrote:\n> Here we go again. Thanks both for the suggestions.\n>\n> -- 8< --\n> Subject: [PATCH] prune: strategies for linked checkouts\n>\n> (alias R=$GIT_COMMON_DIR/worktrees/<id>)\n>\n>  - linked checkouts are supposed to keep its location in $R/gitdir up\n>    to date. The use case is auto fixup after a manual checkout move.\n>\n>  - linked checkouts are supposed to update mtime of $R/gitdir. If\n>    $R/gitdir's mtime is older than a limit, and it points to nowhere,\n>    worktrees/<id> is to be pruned.\n>\n>  - If $R/locked exists, worktrees/<id> is not supposed to be pruned. If\n>    $R/locked exists and $R/gitdir's mtime is older than a really long\n>    limit, warn about old unused repo.\n>\n>  - \"git checkout --to\" is supposed to make a hard link named $R/link\n>    pointing to the .git file on supported file systems to help detect\n>    the user manually deleting the checkout. If $R/link exists and its\n>    link count is greated than 1, the repo is kept.\n>\n> Helped-by: Marc Branchaud <marcnarc@xiplink.com>\n> Helped-by: Eric Sunshine <sunshine@sunshineco.com>\n> Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n> ---\n>  Documentation/git-checkout.txt             | 20 +++++++\n>  Documentation/git-prune.txt                |  3 +\n>  Documentation/gitrepository-layout.txt     | 19 ++++++\n>  builtin/checkout.c                         | 19 +++++-\n>  builtin/prune.c                            | 95 ++++++++++++++++++++++++++++++\n>  setup.c                                    | 13 ++++\n>  t/t2026-prune-linked-checkouts.sh (new +x) | 84 ++++++++++++++++++++++++++\n>  7 files changed, 251 insertions(+), 2 deletions(-)\n>  create mode 100755 t/t2026-prune-linked-checkouts.sh\n>\n> diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt\n> index c101575..0fd3bab 100644\n> --- a/Documentation/git-checkout.txt\n> +++ b/Documentation/git-checkout.txt\n> @@ -434,6 +434,26 @@ thumb is do not make any assumption about whether a path belongs to\n>  $GIT_DIR or $GIT_COMMON_DIR when you need to directly access something\n>  inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.\n>\n> +When you are done with a linked working tree you can simply delete it.\n> +You can clean up any stale $GIT_DIR/worktrees entries via `git prune\n> +--worktrees` in the main worktree or any linked work tree.\n\nNit: In the rest of the documentation, you have consistently spelled\nout \"working tree\", so the (poor) example you copied from my email,\nwhich says \"worktree\", is an anomaly. My bad. Plus, you have a mix of\n\"worktree\" and \"work tree\". You probably want:\n\n    ... in the main or any linked working tree.\n\n> +If you move a linked working directory to another file system, or\n> +within a file system that does not support hard links, you need to run\n> +at least one git command inside the linked working directory\n> +(e.g. `git status`) in order to update its entry in $GIT_DIR/worktrees\n> +so that it does not get automatically removed.\n> +\n> +To prevent `git prune --worktrees` from deleting a $GIT_DIR/worktrees\n> +entry (which can be useful in some situations, such as when the\n> +entry's working tree is stored on a portable device), add a file named\n> +'locked' to the entry's directory. The file contains the reason in\n> +plain text. For example, if a linked working tree's `.git` file points\n> +to `/path/main/.git/worktrees/test-next` then a file named\n> +`/path/main/.git/worktrees/test-next/locked` will prevent the\n> +`test-next` entry from being pruned.  See\n> +linkgit:gitrepository-layout[5] for details.\n> +\n>  EXAMPLES\n>  --------\n>\n> diff --git a/Documentation/git-prune.txt b/Documentation/git-prune.txt\n> index 7a493c8..a0ea381 100644\n> --- a/Documentation/git-prune.txt\n> +++ b/Documentation/git-prune.txt\n> @@ -48,6 +48,9 @@ OPTIONS\n>  --expire <time>::\n>         Only expire loose objects older than <time>.\n>\n> +--worktrees::\n> +       Prune stale worktree information in $GIT_DIR/worktrees.\n> +\n>  <head>...::\n>         In addition to objects\n>         reachable from any of our references, keep objects\n> diff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt\n> index 8228450..2b30a92 100644\n> --- a/Documentation/gitrepository-layout.txt\n> +++ b/Documentation/gitrepository-layout.txt\n> @@ -259,6 +259,25 @@ worktrees::\n>         $GIT_COMMON_DIR is set and \"$GIT_COMMON_DIR/worktrees\" will be\n>         used instead.\n>\n> +worktrees/<id>/gitdir::\n> +       A text file containing the absolute path back to the .git file\n> +       that points to here. This is used to check if the linked\n> +       repository has been manually removed and there is no need to\n> +       keep this directory any more. mtime of this file should be\n> +       updated every time the linked repository is accessed.\n> +\n> +worktrees/<id>/locked::\n> +       If this file exists, the linked repository may be on a\n> +       portable device and not available. It does not mean that the\n> +       linked repository is gone and `worktrees/<id>` could be\n> +       removed. The file's content contains a reason string on why\n> +       the repository is locked.\n> +\n> +worktrees/<id>/link::\n> +       If this file exists, it is a hard link to the linked .git\n> +       file. It is used to detect if the linked repository is\n> +       manually removed.\n> +\n>  SEE ALSO\n>  --------\n>  linkgit:git-init[1],\n> diff --git a/builtin/checkout.c b/builtin/checkout.c\n> index ad10f99..ab46af9 100644\n> --- a/builtin/checkout.c\n> +++ b/builtin/checkout.c\n> @@ -826,7 +826,7 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,\n>         const char *path = opts->new_worktree, *name;\n>         struct stat st;\n>         struct child_process cp;\n> -       int counter = 0, len;\n> +       int counter = 0, len, ret;\n>\n>         if (!new->commit)\n>                 die(_(\"no branch specified\"));\n> @@ -857,11 +857,21 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,\n>         if (mkdir(sb_repo.buf, 0777))\n>                 die_errno(_(\"could not create directory of '%s'\"), sb_repo.buf);\n>\n> +       /*\n> +        * lock the incomplete repo so prune won't delete it, unlock\n> +        * after the preparation is over.\n> +        */\n> +       strbuf_addf(&sb, \"%s/locked\", sb_repo.buf);\n> +       write_file(sb.buf, 1, \"initializing\\n\");\n> +\n>         strbuf_addf(&sb_git, \"%s/.git\", path);\n>         if (safe_create_leading_directories_const(sb_git.buf))\n>                 die_errno(_(\"could not create leading directories of '%s'\"),\n>                           sb_git.buf);\n>\n> +       strbuf_reset(&sb);\n> +       strbuf_addf(&sb, \"%s/gitdir\", sb_repo.buf);\n> +       write_file(sb.buf, 1, \"%s\\n\", real_path(sb_git.buf));\n>         write_file(sb_git.buf, 1, \"gitdir: %s/worktrees/%s\\n\",\n>                    real_path(get_git_common_dir()), name);\n>         /*\n> @@ -870,6 +880,7 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,\n>          * value would do because this value will be ignored and\n>          * replaced at the next (real) checkout.\n>          */\n> +       strbuf_reset(&sb);\n>         strbuf_addf(&sb, \"%s/HEAD\", sb_repo.buf);\n>         write_file(sb.buf, 1, \"%s\\n\", sha1_to_hex(new->commit->object.sha1));\n>         strbuf_reset(&sb);\n> @@ -885,7 +896,11 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,\n>         memset(&cp, 0, sizeof(cp));\n>         cp.git_cmd = 1;\n>         cp.argv = opts->saved_argv;\n> -       return run_command(&cp);\n> +       ret = run_command(&cp);\n> +       strbuf_reset(&sb);\n> +       strbuf_addf(&sb, \"%s/locked\", sb_repo.buf);\n> +       unlink_or_warn(sb.buf);\n> +       return ret;\n>  }\n>\n>  static int git_checkout_config(const char *var, const char *value, void *cb)\n> diff --git a/builtin/prune.c b/builtin/prune.c\n> index 144a3bd..cf56110 100644\n> --- a/builtin/prune.c\n> +++ b/builtin/prune.c\n> @@ -112,6 +112,91 @@ static void prune_object_dir(const char *path)\n>         }\n>  }\n>\n> +static int prune_worktree(const char *id, struct strbuf *reason)\n> +{\n> +       struct stat st;\n> +       char *path;\n> +       int fd, len;\n> +\n> +       if (!is_directory(git_path(\"worktrees/%s\", id))) {\n> +               strbuf_addf(reason, _(\"Removing worktrees/%s: not a valid directory\"), id);\n> +               return 1;\n> +       }\n> +       if (file_exists(git_path(\"worktrees/%s/locked\", id)))\n> +               return 0;\n> +       if (stat(git_path(\"worktrees/%s/gitdir\", id), &st)) {\n> +               strbuf_addf(reason, _(\"Removing worktrees/%s: gitdir file does not exist\"), id);\n> +               return 1;\n> +       }\n> +       fd = open(git_path(\"worktrees/%s/gitdir\", id), O_RDONLY);\n> +       if (fd < 0) {\n> +               strbuf_addf(reason, _(\"Removing worktrees/%s: unable to read gitdir file (%s)\"),\n> +                           id, strerror(errno));\n> +               return 1;\n> +       }\n> +       len = st.st_size;\n> +       path = xmalloc(len + 1);\n> +       read_in_full(fd, path, len);\n> +       close(fd);\n> +       while (len && (path[len - 1] == '\\n' || path[len - 1] == '\\r'))\n> +               len--;\n> +       if (!len) {\n> +               strbuf_addf(reason, _(\"Removing worktrees/%s: invalid gitdir file\"), id);\n> +               free(path);\n> +               return 1;\n> +       }\n> +       path[len] = '\\0';\n> +       if (!file_exists(path)) {\n> +               struct stat st_link;\n> +               free(path);\n> +               /*\n> +                * the repo is moved manually and has not been\n> +                * accessed since?\n> +                */\n> +               if (!stat(git_path(\"worktrees/%s/link\", id), &st_link) &&\n> +                   st_link.st_nlink > 1)\n> +                       return 0;\n> +               strbuf_addf(reason, _(\"Removing worktrees/%s: gitdir file points to non-existent location\"), id);\n> +               return 1;\n> +       }\n> +       free(path);\n> +       return st.st_mtime <= expire;\n> +}\n> +\n> +static void prune_worktrees(void)\n> +{\n> +       struct strbuf reason = STRBUF_INIT;\n> +       struct strbuf path = STRBUF_INIT;\n> +       DIR *dir = opendir(git_path(\"worktrees\"));\n> +       struct dirent *d;\n> +       int ret;\n> +       if (!dir)\n> +               return;\n> +       while ((d = readdir(dir)) != NULL) {\n> +               if (!strcmp(d->d_name, \".\") || !strcmp(d->d_name, \"..\"))\n> +                       continue;\n> +               strbuf_reset(&reason);\n> +               if (!prune_worktree(d->d_name, &reason))\n> +                       continue;\n> +               if (show_only || verbose)\n> +                       printf(\"%s\\n\", reason.buf);\n> +               if (show_only)\n> +                       continue;\n> +               strbuf_reset(&path);\n> +               strbuf_addstr(&path, git_path(\"worktrees/%s\", d->d_name));\n> +               ret = remove_dir_recursively(&path, 0);\n> +               if (ret < 0 && errno == ENOTDIR)\n> +                       ret = unlink(path.buf);\n> +               if (ret)\n> +                       error(_(\"failed to remove: %s\"), strerror(errno));\n> +       }\n> +       closedir(dir);\n> +       if (!show_only)\n> +               rmdir(git_path(\"worktrees\"));\n> +       strbuf_release(&reason);\n> +       strbuf_release(&path);\n> +}\n> +\n>  /*\n>   * Write errors (particularly out of space) can result in\n>   * failed temporary packs (and more rarely indexes and other\n> @@ -138,10 +223,12 @@ int cmd_prune(int argc, const char **argv, const char *prefix)\n>  {\n>         struct rev_info revs;\n>         struct progress *progress = NULL;\n> +       int do_prune_worktrees = 0;\n>         const struct option options[] = {\n>                 OPT__DRY_RUN(&show_only, N_(\"do not remove, show only\")),\n>                 OPT__VERBOSE(&verbose, N_(\"report pruned objects\")),\n>                 OPT_BOOL(0, \"progress\", &show_progress, N_(\"show progress\")),\n> +               OPT_BOOL(0, \"worktrees\", &do_prune_worktrees, N_(\"prune .git/worktrees\")),\n>                 OPT_EXPIRY_DATE(0, \"expire\", &expire,\n>                                 N_(\"expire objects older than <time>\")),\n>                 OPT_END()\n> @@ -154,6 +241,14 @@ int cmd_prune(int argc, const char **argv, const char *prefix)\n>         init_revisions(&revs, prefix);\n>\n>         argc = parse_options(argc, argv, prefix, options, prune_usage, 0);\n> +\n> +       if (do_prune_worktrees) {\n> +               if (argc)\n> +                       die(_(\"--worktrees does not take extra arguments\"));\n> +               prune_worktrees();\n> +               return 0;\n> +       }\n> +\n>         while (argc--) {\n>                 unsigned char sha1[20];\n>                 const char *name = *argv++;\n> diff --git a/setup.c b/setup.c\n> index b99bdd3..fb61860 100644\n> --- a/setup.c\n> +++ b/setup.c\n> @@ -390,6 +390,17 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)\n>         return ret;\n>  }\n>\n> +static void update_linked_gitdir(const char *gitfile, const char *gitdir)\n> +{\n> +       struct strbuf path = STRBUF_INIT;\n> +       struct stat st;\n> +\n> +       strbuf_addf(&path, \"%s/gitfile\", gitdir);\n> +       if (stat(path.buf, &st) || st.st_mtime + 24 * 3600 < time(NULL))\n> +               write_file(path.buf, 0, \"%s\\n\", gitfile);\n> +       strbuf_release(&path);\n> +}\n> +\n>  /*\n>   * Try to read the location of the git directory from the .git file,\n>   * return path to git directory if found.\n> @@ -438,6 +449,8 @@ const char *read_gitfile(const char *path)\n>\n>         if (!is_git_directory(dir))\n>                 die(\"Not a git repository: %s\", dir);\n> +\n> +       update_linked_gitdir(path, dir);\n>         path = real_path(dir);\n>\n>         free(buf);\n> diff --git a/t/t2026-prune-linked-checkouts.sh b/t/t2026-prune-linked-checkouts.sh\n> new file mode 100755\n> index 0000000..3622800\n> --- /dev/null\n> +++ b/t/t2026-prune-linked-checkouts.sh\n> @@ -0,0 +1,84 @@\n> +#!/bin/sh\n> +\n> +test_description='prune $GIT_DIR/worktrees'\n> +\n> +. ./test-lib.sh\n> +\n> +test_expect_success 'prune --worktrees on normal repo' '\n> +       git prune --worktrees &&\n> +       test_must_fail git prune --worktrees abc\n> +'\n> +\n> +test_expect_success 'prune files inside $GIT_DIR/worktrees' '\n> +       mkdir .git/worktrees &&\n> +       : >.git/worktrees/abc &&\n> +       git prune --worktrees --verbose >actual &&\n> +       cat >expect <<EOF &&\n> +Removing worktrees/abc: not a valid directory\n> +EOF\n> +       test_i18ncmp expect actual &&\n> +       ! test -f .git/worktrees/abc &&\n> +       ! test -d .git/worktrees\n> +'\n> +\n> +test_expect_success 'prune directories without gitdir' '\n> +       mkdir -p .git/worktrees/def/abc &&\n> +       : >.git/worktrees/def/def &&\n> +       cat >expect <<EOF &&\n> +Removing worktrees/def: gitdir file does not exist\n> +EOF\n> +       git prune --worktrees --verbose >actual &&\n> +       test_i18ncmp expect actual &&\n> +       ! test -d .git/worktrees/def &&\n> +       ! test -d .git/worktrees\n> +'\n> +\n> +test_expect_success POSIXPERM 'prune directories with unreadable gitdir' '\n> +       mkdir -p .git/worktrees/def/abc &&\n> +       : >.git/worktrees/def/def &&\n> +       : >.git/worktrees/def/gitdir &&\n> +       chmod u-r .git/worktrees/def/gitdir &&\n> +       git prune --worktrees --verbose >actual &&\n> +       test_i18ngrep \"Removing worktrees/def: unable to read gitdir file\" actual &&\n> +       ! test -d .git/worktrees/def &&\n> +       ! test -d .git/worktrees\n> +'\n> +\n> +test_expect_success 'prune directories with invalid gitdir' '\n> +       mkdir -p .git/worktrees/def/abc &&\n> +       : >.git/worktrees/def/def &&\n> +       : >.git/worktrees/def/gitdir &&\n> +       git prune --worktrees --verbose >actual &&\n> +       test_i18ngrep \"Removing worktrees/def: invalid gitdir file\" actual &&\n> +       ! test -d .git/worktrees/def &&\n> +       ! test -d .git/worktrees\n> +'\n> +\n> +test_expect_success 'prune directories with gitdir pointing to nowhere' '\n> +       mkdir -p .git/worktrees/def/abc &&\n> +       : >.git/worktrees/def/def &&\n> +       echo \"$TRASH_DIRECTORY\"/nowhere >.git/worktrees/def/gitdir &&\n> +       git prune --worktrees --verbose >actual &&\n> +       test_i18ngrep \"Removing worktrees/def: gitdir file points to non-existent location\" actual &&\n> +       ! test -d .git/worktrees/def &&\n> +       ! test -d .git/worktrees\n> +'\n> +\n> +test_expect_success 'not prune locked checkout' '\n> +       test_when_finished rm -r .git/worktrees\n> +       mkdir -p .git/worktrees/ghi &&\n> +       : >.git/worktrees/ghi/locked &&\n> +       git prune --worktrees &&\n> +       test -d .git/worktrees/ghi\n> +'\n> +\n> +test_expect_success 'not prune recent checkouts' '\n> +       test_when_finished rm -r .git/worktrees\n> +       mkdir zz &&\n> +       mkdir -p .git/worktrees/jlm &&\n> +       echo \"$TRASH_DIRECTORY\"/zz >.git/worktrees/jlm/gitdir &&\n> +       git prune --worktrees --verbose --expire=2.days.ago &&\n> +       test -d .git/worktrees/jlm\n> +'\n> +\n> +test_done\n> --\n> 2.1.0.rc0.78.gc0d8480\n>\n> -- 8< --\n"},{"id":"249761","messageId":"CAPig+cTVO0JvBB_gBRkFBhGKh8Y=iZ40j4x+K8X0d9fm04nMgw@mail.gmail.com","threadId":"37450","inReplyTo":"54208E01.2060205@xiplink.com","subject":"Re: [PATCH v2 22/32] checkout: support checking out into a new working directory","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2014-09-22T23:01:41Z","receivedAt":"2014-09-22T23:01:41Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Mon, Sep 22, 2014 at 5:00 PM, Marc Branchaud <marcnarc@xiplink.com> wrote:\n> On 14-09-21 05:50 AM, Duy Nguyen wrote:\n>> On Sun, Sep 21, 2014 at 10:10 AM, Eric Sunshine <sunshine@sunshineco.com> wrote:\n>>> Would it make sense for this \"rule of thumb\" summary to be presented\n>>> first, and then the explanation of that rule after, rather than the\n>>> reverse as is currently the case?\n>>\n>> You mean like this?\n\nNot quite. It was a genuine question rather than an outright\nsuggestion for change. It is typical to present a high-level overview\nand then the low-level details. This case reverses that and presents\nthe details first and then the overview (in the form of --git-path).\nThe reader wades through several paragraphs of detailed explanation of\nthe low-level structure of the .git directory, only to be told at the\nend to use --git-path and not worry about those details. Thus, the\norganization struck me as a bit odd.\n\n> *shrug*  To me it seems better to leave it at the end here.\n\nThe text does work as-is, and presents the necessary information; and\nI don't want to turn this into a bike-shedding session, so feel free\nto ignore my question.\n"},{"id":"249865","messageId":"xmqqk34r9z3r.fsf@gitster.dls.corp.google.com","threadId":"37450","inReplyTo":"1410388928-32265-1-git-send-email-pclouds@gmail.com","subject":"Re: [PATCH v2 00/32] nd/multiple-work-trees","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2014-09-25T21:20:08Z","receivedAt":"2014-09-25T21:20:08Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"It has been a while since the last review exchanges were seen.  Will\nit be time for v3 soon?\n"},{"id":"249927","messageId":"1411867366-3821-1-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"xmqqk34r9z3r.fsf@gitster.dls.corp.google.com","subject":"[PATCH v3 00/32] nd/multiple-work-trees","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-28T01:22:14Z","receivedAt":"2014-09-28T01:22:14Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"On Fri, Sep 26, 2014 at 4:20 AM, Junio C Hamano <gitster@pobox.com> wrote:\n> It has been a while since the last review exchanges were seen.  Will\n> it be time for v3 soon?\n\nSorry I've been slow on picking up feedback from v2. v3 is rebased on\nlatest master. Other changes are mostly *.txt, and one broken &&\nchain.\n\nDennis Kaarsemaker (1):\n  checkout: don't require a work tree when checking out into a new one\n\nNguyễn Thái Ngọc Duy (31):\n  path.c: make get_pathname() return strbuf instead of static buffer\n  path.c: make get_pathname() call sites return const char *\n  git_snpath(): retire and replace with strbuf_git_path()\n  path.c: rename vsnpath() to do_git_path()\n  path.c: group git_path(), git_pathdup() and strbuf_git_path() together\n  git_path(): be aware of file relocation in $GIT_DIR\n  *.sh: respect $GIT_INDEX_FILE\n  reflog: avoid constructing .lock path with git_path\n  fast-import: use git_path() for accessing .git dir instead of get_git_dir()\n  commit: use SEQ_DIR instead of hardcoding \"sequencer\"\n  $GIT_COMMON_DIR: a new environment variable\n  git-sh-setup.sh: use rev-parse --git-path to get $GIT_DIR/objects\n  *.sh: avoid hardcoding $GIT_DIR/hooks/...\n  git-stash: avoid hardcoding $GIT_DIR/logs/....\n  setup.c: convert is_git_directory() to use strbuf\n  setup.c: detect $GIT_COMMON_DIR in is_git_directory()\n  setup.c: convert check_repository_format_gently to use strbuf\n  setup.c: detect $GIT_COMMON_DIR check_repository_format_gently()\n  setup.c: support multi-checkout repo setup\n  wrapper.c: wrapper to open a file, fprintf then close\n  use new wrapper write_file() for simple file writing\n  checkout: support checking out into a new working directory\n  prune: strategies for linked checkouts\n  checkout: reject if the branch is already checked out elsewhere\n  checkout: clean up half-prepared directories in --to mode\n  gc: style change -- no SP before closing parenthesis\n  gc: factor out gc.pruneexpire parsing code\n  gc: support prune --worktrees\n  count-objects: report unused files in $GIT_DIR/worktrees/...\n  git_path(): keep \"info/sparse-checkout\" per work-tree\n  t2025: add a test to make sure grafts is working from a linked checkout\n\n Documentation/config.txt                   |   9 +\n Documentation/git-checkout.txt             |  69 ++++++++\n Documentation/git-prune.txt                |   3 +\n Documentation/git-rev-parse.txt            |  10 ++\n Documentation/git.txt                      |   9 +\n Documentation/gitrepository-layout.txt     |  78 +++++++--\n builtin/branch.c                           |   4 +-\n builtin/checkout.c                         | 260 ++++++++++++++++++++++++++++-\n builtin/clone.c                            |   9 +-\n builtin/commit.c                           |   2 +-\n builtin/count-objects.c                    |   4 +-\n builtin/fetch.c                            |   5 +-\n builtin/fsck.c                             |   4 +-\n builtin/gc.c                               |  34 ++--\n builtin/init-db.c                          |   7 +-\n builtin/prune.c                            |  95 +++++++++++\n builtin/receive-pack.c                     |   2 +-\n builtin/reflog.c                           |   2 +-\n builtin/remote.c                           |   2 +-\n builtin/repack.c                           |   8 +-\n builtin/rev-parse.c                        |  11 ++\n cache.h                                    |  17 +-\n daemon.c                                   |  11 +-\n environment.c                              |  33 +++-\n fast-import.c                              |   7 +-\n git-am.sh                                  |  22 +--\n git-pull.sh                                |   2 +-\n git-rebase--interactive.sh                 |   6 +-\n git-rebase--merge.sh                       |   6 +-\n git-rebase.sh                              |   4 +-\n git-sh-setup.sh                            |   2 +-\n git-stash.sh                               |   6 +-\n git.c                                      |   2 +-\n notes-merge.c                              |   6 +-\n path.c                                     | 234 +++++++++++++++++---------\n refs.c                                     |  86 ++++++----\n refs.h                                     |   2 +-\n run-command.c                              |   4 +-\n run-command.h                              |   2 +-\n setup.c                                    | 124 +++++++++++---\n sha1_file.c                                |   2 +-\n submodule.c                                |   9 +-\n t/t0060-path-utils.sh                      |  36 ++++\n t/t1501-worktree.sh                        |  76 +++++++++\n t/t1510-repo-setup.sh                      |   1 +\n t/t2025-checkout-to.sh (new +x)            | 117 +++++++++++++\n t/t2026-prune-linked-checkouts.sh (new +x) |  84 ++++++++++\n templates/hooks--applypatch-msg.sample     |   4 +-\n templates/hooks--pre-applypatch.sample     |   4 +-\n trace.c                                    |   1 +\n transport.c                                |   8 +-\n wrapper.c                                  |  31 ++++\n 52 files changed, 1306 insertions(+), 270 deletions(-)\n create mode 100755 t/t2025-checkout-to.sh\n create mode 100755 t/t2026-prune-linked-checkouts.sh\n\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249928","messageId":"1411867366-3821-2-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1411867366-3821-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v3 01/32] path.c: make get_pathname() return strbuf instead of static buffer","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-28T01:22:15Z","receivedAt":"2014-09-28T01:22:15Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"We've been avoiding PATH_MAX whenever possible. This patch makes\nget_pathname() return a strbuf and updates the callers to take\nadvantage of this. The code is simplified as we no longer need to\nworry about buffer overflow.\n\nvsnpath() behavior is changed slightly: previously it always clears\nthe buffer before writing, now it just appends. Fortunately this is a\nstatic function and all of its callers prepare the buffer properly:\ngit_path() gets the buffer from get_pathname() which resets the\nbuffer, the remaining call sites start with STRBUF_INIT'd buffer.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n path.c | 120 ++++++++++++++++++++++++++++-------------------------------------\n 1 file changed, 51 insertions(+), 69 deletions(-)\n\ndiff --git a/path.c b/path.c\nindex f68df0c..015c0e4 100644\n--- a/path.c\n+++ b/path.c\n@@ -16,11 +16,15 @@ static int get_st_mode_bits(const char *path, int *mode)\n \n static char bad_path[] = \"/bad-path/\";\n \n-static char *get_pathname(void)\n+static struct strbuf *get_pathname(void)\n {\n-\tstatic char pathname_array[4][PATH_MAX];\n+\tstatic struct strbuf pathname_array[4] = {\n+\t\tSTRBUF_INIT, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT\n+\t};\n \tstatic int index;\n-\treturn pathname_array[3 & ++index];\n+\tstruct strbuf *sb = &pathname_array[3 & ++index];\n+\tstrbuf_reset(sb);\n+\treturn sb;\n }\n \n static char *cleanup_path(char *path)\n@@ -34,6 +38,13 @@ static char *cleanup_path(char *path)\n \treturn path;\n }\n \n+static void strbuf_cleanup_path(struct strbuf *sb)\n+{\n+\tchar *path = cleanup_path(sb->buf);\n+\tif (path > sb->buf)\n+\t\tstrbuf_remove(sb, 0, path - sb->buf);\n+}\n+\n char *mksnpath(char *buf, size_t n, const char *fmt, ...)\n {\n \tva_list args;\n@@ -49,85 +60,70 @@ char *mksnpath(char *buf, size_t n, const char *fmt, ...)\n \treturn cleanup_path(buf);\n }\n \n-static char *vsnpath(char *buf, size_t n, const char *fmt, va_list args)\n+static void vsnpath(struct strbuf *buf, const char *fmt, va_list args)\n {\n \tconst char *git_dir = get_git_dir();\n-\tsize_t len;\n-\n-\tlen = strlen(git_dir);\n-\tif (n < len + 1)\n-\t\tgoto bad;\n-\tmemcpy(buf, git_dir, len);\n-\tif (len && !is_dir_sep(git_dir[len-1]))\n-\t\tbuf[len++] = '/';\n-\tlen += vsnprintf(buf + len, n - len, fmt, args);\n-\tif (len >= n)\n-\t\tgoto bad;\n-\treturn cleanup_path(buf);\n-bad:\n-\tstrlcpy(buf, bad_path, n);\n-\treturn buf;\n+\tstrbuf_addstr(buf, git_dir);\n+\tif (buf->len && !is_dir_sep(buf->buf[buf->len - 1]))\n+\t\tstrbuf_addch(buf, '/');\n+\tstrbuf_vaddf(buf, fmt, args);\n+\tstrbuf_cleanup_path(buf);\n }\n \n char *git_snpath(char *buf, size_t n, const char *fmt, ...)\n {\n-\tchar *ret;\n+\tstruct strbuf sb = STRBUF_INIT;\n \tva_list args;\n \tva_start(args, fmt);\n-\tret = vsnpath(buf, n, fmt, args);\n+\tvsnpath(&sb, fmt, args);\n \tva_end(args);\n-\treturn ret;\n+\tif (sb.len >= n)\n+\t\tstrlcpy(buf, bad_path, n);\n+\telse\n+\t\tmemcpy(buf, sb.buf, sb.len + 1);\n+\tstrbuf_release(&sb);\n+\treturn buf;\n }\n \n char *git_pathdup(const char *fmt, ...)\n {\n-\tchar path[PATH_MAX], *ret;\n+\tstruct strbuf path = STRBUF_INIT;\n \tva_list args;\n \tva_start(args, fmt);\n-\tret = vsnpath(path, sizeof(path), fmt, args);\n+\tvsnpath(&path, fmt, args);\n \tva_end(args);\n-\treturn xstrdup(ret);\n+\treturn strbuf_detach(&path, NULL);\n }\n \n char *mkpathdup(const char *fmt, ...)\n {\n-\tchar *path;\n \tstruct strbuf sb = STRBUF_INIT;\n \tva_list args;\n-\n \tva_start(args, fmt);\n \tstrbuf_vaddf(&sb, fmt, args);\n \tva_end(args);\n-\tpath = xstrdup(cleanup_path(sb.buf));\n-\n-\tstrbuf_release(&sb);\n-\treturn path;\n+\tstrbuf_cleanup_path(&sb);\n+\treturn strbuf_detach(&sb, NULL);\n }\n \n char *mkpath(const char *fmt, ...)\n {\n \tva_list args;\n-\tunsigned len;\n-\tchar *pathname = get_pathname();\n-\n+\tstruct strbuf *pathname = get_pathname();\n \tva_start(args, fmt);\n-\tlen = vsnprintf(pathname, PATH_MAX, fmt, args);\n+\tstrbuf_vaddf(pathname, fmt, args);\n \tva_end(args);\n-\tif (len >= PATH_MAX)\n-\t\treturn bad_path;\n-\treturn cleanup_path(pathname);\n+\treturn cleanup_path(pathname->buf);\n }\n \n char *git_path(const char *fmt, ...)\n {\n-\tchar *pathname = get_pathname();\n+\tstruct strbuf *pathname = get_pathname();\n \tva_list args;\n-\tchar *ret;\n-\n \tva_start(args, fmt);\n-\tret = vsnpath(pathname, PATH_MAX, fmt, args);\n+\tvsnpath(pathname, fmt, args);\n \tva_end(args);\n-\treturn ret;\n+\treturn pathname->buf;\n }\n \n void home_config_paths(char **global, char **xdg, char *file)\n@@ -160,41 +156,27 @@ void home_config_paths(char **global, char **xdg, char *file)\n \n char *git_path_submodule(const char *path, const char *fmt, ...)\n {\n-\tchar *pathname = get_pathname();\n-\tstruct strbuf buf = STRBUF_INIT;\n+\tstruct strbuf *buf = get_pathname();\n \tconst char *git_dir;\n \tva_list args;\n-\tunsigned len;\n-\n-\tlen = strlen(path);\n-\tif (len > PATH_MAX-100)\n-\t\treturn bad_path;\n \n-\tstrbuf_addstr(&buf, path);\n-\tif (len && path[len-1] != '/')\n-\t\tstrbuf_addch(&buf, '/');\n-\tstrbuf_addstr(&buf, \".git\");\n+\tstrbuf_addstr(buf, path);\n+\tif (buf->len && buf->buf[buf->len - 1] != '/')\n+\t\tstrbuf_addch(buf, '/');\n+\tstrbuf_addstr(buf, \".git\");\n \n-\tgit_dir = read_gitfile(buf.buf);\n+\tgit_dir = read_gitfile(buf->buf);\n \tif (git_dir) {\n-\t\tstrbuf_reset(&buf);\n-\t\tstrbuf_addstr(&buf, git_dir);\n+\t\tstrbuf_reset(buf);\n+\t\tstrbuf_addstr(buf, git_dir);\n \t}\n-\tstrbuf_addch(&buf, '/');\n-\n-\tif (buf.len >= PATH_MAX)\n-\t\treturn bad_path;\n-\tmemcpy(pathname, buf.buf, buf.len + 1);\n-\n-\tstrbuf_release(&buf);\n-\tlen = strlen(pathname);\n+\tstrbuf_addch(buf, '/');\n \n \tva_start(args, fmt);\n-\tlen += vsnprintf(pathname + len, PATH_MAX - len, fmt, args);\n+\tstrbuf_vaddf(buf, fmt, args);\n \tva_end(args);\n-\tif (len >= PATH_MAX)\n-\t\treturn bad_path;\n-\treturn cleanup_path(pathname);\n+\tstrbuf_cleanup_path(buf);\n+\treturn buf->buf;\n }\n \n int validate_headref(const char *path)\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249929","messageId":"1411867366-3821-3-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1411867366-3821-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v3 02/32] path.c: make get_pathname() call sites return const char *","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-28T01:22:16Z","receivedAt":"2014-09-28T01:22:16Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Before the previous commit, get_pathname returns an array of PATH_MAX\nlength. Even if git_path() and similar functions does not use the\nwhole array, git_path() caller can, in theory.\n\nAfter the commit, get_pathname() may return a buffer that has just\nenough room for the returned string and git_path() caller should never\nwrite beyond that.\n\nMake git_path(), mkpath() and git_path_submodule() return a const\nbuffer to make sure callers do not write in it at all.\n\nThis could have been part of the previous commit, but the \"const\"\nconversion is too much distraction from the core changes in path.c.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n builtin/checkout.c     | 2 +-\n builtin/clone.c        | 9 +++++----\n builtin/fetch.c        | 5 +++--\n builtin/fsck.c         | 4 ++--\n builtin/receive-pack.c | 2 +-\n builtin/remote.c       | 2 +-\n builtin/repack.c       | 8 +++++---\n cache.h                | 6 +++---\n fast-import.c          | 2 +-\n notes-merge.c          | 6 +++---\n path.c                 | 6 +++---\n refs.c                 | 8 ++++----\n run-command.c          | 4 ++--\n run-command.h          | 2 +-\n sha1_file.c            | 2 +-\n 15 files changed, 36 insertions(+), 32 deletions(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 8afdf2b..d402d7a 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -589,7 +589,7 @@ static void update_refs_for_switch(const struct checkout_opts *opts,\n \t\t\tif (opts->new_branch_log && !log_all_ref_updates) {\n \t\t\t\tint temp;\n \t\t\t\tchar log_file[PATH_MAX];\n-\t\t\t\tchar *ref_name = mkpath(\"refs/heads/%s\", opts->new_orphan_branch);\n+\t\t\t\tconst char *ref_name = mkpath(\"refs/heads/%s\", opts->new_orphan_branch);\n \n \t\t\t\ttemp = log_all_ref_updates;\n \t\t\t\tlog_all_ref_updates = 1;\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 3927edf..180bc89 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -289,16 +289,17 @@ static void copy_alternates(struct strbuf *src, struct strbuf *dst,\n \tstruct strbuf line = STRBUF_INIT;\n \n \twhile (strbuf_getline(&line, in, '\\n') != EOF) {\n-\t\tchar *abs_path, abs_buf[PATH_MAX];\n+\t\tchar *abs_path;\n \t\tif (!line.len || line.buf[0] == '#')\n \t\t\tcontinue;\n \t\tif (is_absolute_path(line.buf)) {\n \t\t\tadd_to_alternates_file(line.buf);\n \t\t\tcontinue;\n \t\t}\n-\t\tabs_path = mkpath(\"%s/objects/%s\", src_repo, line.buf);\n-\t\tnormalize_path_copy(abs_buf, abs_path);\n-\t\tadd_to_alternates_file(abs_buf);\n+\t\tabs_path = mkpathdup(\"%s/objects/%s\", src_repo, line.buf);\n+\t\tnormalize_path_copy(abs_path, abs_path);\n+\t\tadd_to_alternates_file(abs_path);\n+\t\tfree(abs_path);\n \t}\n \tstrbuf_release(&line);\n \tfclose(in);\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 159fb7e..3f8c8f5 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -573,7 +573,8 @@ static int store_updated_refs(const char *raw_url, const char *remote_name,\n \tstruct strbuf note = STRBUF_INIT;\n \tconst char *what, *kind;\n \tstruct ref *rm;\n-\tchar *url, *filename = dry_run ? \"/dev/null\" : git_path(\"FETCH_HEAD\");\n+\tchar *url;\n+\tconst char *filename = dry_run ? \"/dev/null\" : git_path(\"FETCH_HEAD\");\n \tint want_status;\n \n \tfp = fopen(filename, \"a\");\n@@ -807,7 +808,7 @@ static void check_not_current_branch(struct ref *ref_map)\n \n static int truncate_fetch_head(void)\n {\n-\tchar *filename = git_path(\"FETCH_HEAD\");\n+\tconst char *filename = git_path(\"FETCH_HEAD\");\n \tFILE *fp = fopen(filename, \"w\");\n \n \tif (!fp)\ndiff --git a/builtin/fsck.c b/builtin/fsck.c\nindex e9ba576..0082a0e 100644\n--- a/builtin/fsck.c\n+++ b/builtin/fsck.c\n@@ -225,12 +225,12 @@ static void check_unreachable_object(struct object *obj)\n \t\t\tprintf(\"dangling %s %s\\n\", typename(obj->type),\n \t\t\t       sha1_to_hex(obj->sha1));\n \t\tif (write_lost_and_found) {\n-\t\t\tchar *filename = git_path(\"lost-found/%s/%s\",\n+\t\t\tconst char *filename = git_path(\"lost-found/%s/%s\",\n \t\t\t\tobj->type == OBJ_COMMIT ? \"commit\" : \"other\",\n \t\t\t\tsha1_to_hex(obj->sha1));\n \t\t\tFILE *f;\n \n-\t\t\tif (safe_create_leading_directories(filename)) {\n+\t\t\tif (safe_create_leading_directories_const(filename)) {\n \t\t\t\terror(\"Could not create lost-found\");\n \t\t\t\treturn;\n \t\t\t}\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex daf0600..3a49d1a 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -610,7 +610,7 @@ static void run_update_post_hook(struct command *commands)\n \tint argc;\n \tconst char **argv;\n \tstruct child_process proc = CHILD_PROCESS_INIT;\n-\tchar *hook;\n+\tconst char *hook;\n \n \thook = find_hook(\"post-update\");\n \tfor (argc = 0, cmd = commands; cmd; cmd = cmd->next) {\ndiff --git a/builtin/remote.c b/builtin/remote.c\nindex 9a4640d..d5324ed 100644\n--- a/builtin/remote.c\n+++ b/builtin/remote.c\n@@ -581,7 +581,7 @@ static int migrate_file(struct remote *remote)\n {\n \tstruct strbuf buf = STRBUF_INIT;\n \tint i;\n-\tchar *path = NULL;\n+\tconst char *path = NULL;\n \n \tstrbuf_addf(&buf, \"remote.%s.url\", remote->name);\n \tfor (i = 0; i < remote->url_nr; i++)\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex 2aae05d..614851b 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -283,7 +283,8 @@ int cmd_repack(int argc, const char **argv, const char *prefix)\n \tfailed = 0;\n \tfor_each_string_list_item(item, &names) {\n \t\tfor (ext = 0; ext < ARRAY_SIZE(exts); ext++) {\n-\t\t\tchar *fname, *fname_old;\n+\t\t\tconst char *fname_old;\n+\t\t\tchar *fname;\n \t\t\tfname = mkpathdup(\"%s/pack-%s%s\", packdir,\n \t\t\t\t\t\titem->string, exts[ext].name);\n \t\t\tif (!file_exists(fname)) {\n@@ -311,7 +312,8 @@ int cmd_repack(int argc, const char **argv, const char *prefix)\n \tif (failed) {\n \t\tstruct string_list rollback_failure = STRING_LIST_INIT_DUP;\n \t\tfor_each_string_list_item(item, &rollback) {\n-\t\t\tchar *fname, *fname_old;\n+\t\t\tconst char *fname_old;\n+\t\t\tchar *fname;\n \t\t\tfname = mkpathdup(\"%s/%s\", packdir, item->string);\n \t\t\tfname_old = mkpath(\"%s/old-%s\", packdir, item->string);\n \t\t\tif (rename(fname_old, fname))\n@@ -364,7 +366,7 @@ int cmd_repack(int argc, const char **argv, const char *prefix)\n \t/* Remove the \"old-\" files */\n \tfor_each_string_list_item(item, &names) {\n \t\tfor (ext = 0; ext < ARRAY_SIZE(exts); ext++) {\n-\t\t\tchar *fname;\n+\t\t\tconst char *fname;\n \t\t\tfname = mkpath(\"%s/old-%s%s\",\n \t\t\t\t\tpackdir,\n \t\t\t\t\titem->string,\ndiff --git a/cache.h b/cache.h\nindex 8206039..2e4d88f 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -705,9 +705,9 @@ extern char *mkpathdup(const char *fmt, ...)\n \t__attribute__((format (printf, 1, 2)));\n \n /* Return a statically allocated filename matching the sha1 signature */\n-extern char *mkpath(const char *fmt, ...) __attribute__((format (printf, 1, 2)));\n-extern char *git_path(const char *fmt, ...) __attribute__((format (printf, 1, 2)));\n-extern char *git_path_submodule(const char *path, const char *fmt, ...)\n+extern const char *mkpath(const char *fmt, ...) __attribute__((format (printf, 1, 2)));\n+extern const char *git_path(const char *fmt, ...) __attribute__((format (printf, 1, 2)));\n+extern const char *git_path_submodule(const char *path, const char *fmt, ...)\n \t__attribute__((format (printf, 2, 3)));\n \n /*\ndiff --git a/fast-import.c b/fast-import.c\nindex 96b0f42..9ddcd3b 100644\n--- a/fast-import.c\n+++ b/fast-import.c\n@@ -404,7 +404,7 @@ static void dump_marks_helper(FILE *, uintmax_t, struct mark_set *);\n \n static void write_crash_report(const char *err)\n {\n-\tchar *loc = git_path(\"fast_import_crash_%\"PRIuMAX, (uintmax_t) getpid());\n+\tconst char *loc = git_path(\"fast_import_crash_%\"PRIuMAX, (uintmax_t) getpid());\n \tFILE *rpt = fopen(loc, \"w\");\n \tstruct branch *b;\n \tunsigned long lu;\ndiff --git a/notes-merge.c b/notes-merge.c\nindex fd5fae2..a9e6b15 100644\n--- a/notes-merge.c\n+++ b/notes-merge.c\n@@ -280,7 +280,7 @@ static void check_notes_merge_worktree(struct notes_merge_options *o)\n \t\t\t\t    \"(%s exists).\", git_path(\"NOTES_MERGE_*\"));\n \t\t}\n \n-\t\tif (safe_create_leading_directories(git_path(\n+\t\tif (safe_create_leading_directories_const(git_path(\n \t\t\t\tNOTES_MERGE_WORKTREE \"/.test\")))\n \t\t\tdie_errno(\"unable to create directory %s\",\n \t\t\t\t  git_path(NOTES_MERGE_WORKTREE));\n@@ -295,8 +295,8 @@ static void write_buf_to_worktree(const unsigned char *obj,\n \t\t\t\t  const char *buf, unsigned long size)\n {\n \tint fd;\n-\tchar *path = git_path(NOTES_MERGE_WORKTREE \"/%s\", sha1_to_hex(obj));\n-\tif (safe_create_leading_directories(path))\n+\tconst char *path = git_path(NOTES_MERGE_WORKTREE \"/%s\", sha1_to_hex(obj));\n+\tif (safe_create_leading_directories_const(path))\n \t\tdie_errno(\"unable to create directory for '%s'\", path);\n \tif (file_exists(path))\n \t\tdie(\"found existing file at '%s'\", path);\ndiff --git a/path.c b/path.c\nindex 015c0e4..a7ceea2 100644\n--- a/path.c\n+++ b/path.c\n@@ -106,7 +106,7 @@ char *mkpathdup(const char *fmt, ...)\n \treturn strbuf_detach(&sb, NULL);\n }\n \n-char *mkpath(const char *fmt, ...)\n+const char *mkpath(const char *fmt, ...)\n {\n \tva_list args;\n \tstruct strbuf *pathname = get_pathname();\n@@ -116,7 +116,7 @@ char *mkpath(const char *fmt, ...)\n \treturn cleanup_path(pathname->buf);\n }\n \n-char *git_path(const char *fmt, ...)\n+const char *git_path(const char *fmt, ...)\n {\n \tstruct strbuf *pathname = get_pathname();\n \tva_list args;\n@@ -154,7 +154,7 @@ void home_config_paths(char **global, char **xdg, char *file)\n \tfree(to_free);\n }\n \n-char *git_path_submodule(const char *path, const char *fmt, ...)\n+const char *git_path_submodule(const char *path, const char *fmt, ...)\n {\n \tstruct strbuf *buf = get_pathname();\n \tconst char *git_dir;\ndiff --git a/refs.c b/refs.c\nindex 311a6b5..f616adc 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1306,7 +1306,7 @@ static int resolve_gitlink_ref_recursive(struct ref_cache *refs,\n {\n \tint fd, len;\n \tchar buffer[128], *p;\n-\tchar *path;\n+\tconst char *path;\n \n \tif (recursion > MAXDEPTH || strlen(refname) > MAXREFLEN)\n \t\treturn -1;\n@@ -2139,7 +2139,7 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname,\n \t\t\t\t\t    const unsigned char *old_sha1,\n \t\t\t\t\t    int flags, int *type_p)\n {\n-\tchar *ref_file;\n+\tconst char *ref_file;\n \tconst char *orig_refname = refname;\n \tstruct ref_lock *lock;\n \tint last_errno = 0;\n@@ -2202,7 +2202,7 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname,\n \t\tlock->force_write = 1;\n \n  retry:\n-\tswitch (safe_create_leading_directories(ref_file)) {\n+\tswitch (safe_create_leading_directories_const(ref_file)) {\n \tcase SCLD_OK:\n \t\tbreak; /* success */\n \tcase SCLD_VANISHED:\n@@ -2647,7 +2647,7 @@ static int rename_tmp_log(const char *newrefname)\n \tint attempts_remaining = 4;\n \n  retry:\n-\tswitch (safe_create_leading_directories(git_path(\"logs/%s\", newrefname))) {\n+\tswitch (safe_create_leading_directories_const(git_path(\"logs/%s\", newrefname))) {\n \tcase SCLD_OK:\n \t\tbreak; /* success */\n \tcase SCLD_VANISHED:\ndiff --git a/run-command.c b/run-command.c\nindex 761f0fd..1591534 100644\n--- a/run-command.c\n+++ b/run-command.c\n@@ -748,9 +748,9 @@ int finish_async(struct async *async)\n #endif\n }\n \n-char *find_hook(const char *name)\n+const char *find_hook(const char *name)\n {\n-\tchar *path = git_path(\"hooks/%s\", name);\n+\tconst char *path = git_path(\"hooks/%s\", name);\n \tif (access(path, X_OK) < 0)\n \t\tpath = NULL;\n \ndiff --git a/run-command.h b/run-command.h\nindex 1b135d1..ad938ec 100644\n--- a/run-command.h\n+++ b/run-command.h\n@@ -51,7 +51,7 @@ int start_command(struct child_process *);\n int finish_command(struct child_process *);\n int run_command(struct child_process *);\n \n-extern char *find_hook(const char *name);\n+extern const char *find_hook(const char *name);\n LAST_ARG_MUST_BE_NULL\n extern int run_hook_le(const char *const *env, const char *name, ...);\n extern int run_hook_ve(const char *const *env, const char *name, va_list args);\ndiff --git a/sha1_file.c b/sha1_file.c\nindex c08c0cb..50c9600 100644\n--- a/sha1_file.c\n+++ b/sha1_file.c\n@@ -404,7 +404,7 @@ void add_to_alternates_file(const char *reference)\n {\n \tstruct lock_file *lock = xcalloc(1, sizeof(struct lock_file));\n \tint fd = hold_lock_file_for_append(lock, git_path(\"objects/info/alternates\"), LOCK_DIE_ON_ERROR);\n-\tchar *alt = mkpath(\"%s\\n\", reference);\n+\tconst char *alt = mkpath(\"%s\\n\", reference);\n \twrite_or_die(fd, alt, strlen(alt));\n \tif (commit_lock_file(lock))\n \t\tdie(\"could not close alternates file\");\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249930","messageId":"1411867366-3821-4-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1411867366-3821-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v3 03/32] git_snpath(): retire and replace with strbuf_git_path()","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-28T01:22:17Z","receivedAt":"2014-09-28T01:22:17Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"In the previous patch, git_snpath() is modified to allocate a new\nstrbuf buffer because vsnpath() needs that. But that makes it\nawkward because git_snpath() receives a pre-allocated buffer from\noutside and has to copy data back. Rename it to strbuf_git_path()\nand make it receive strbuf directly.\n\nUsing git_path() in update_refs_for_switch() which used to call\ngit_snpath() is safe because that function and all of its callers do\nnot keep any pointer to the round-robin buffer pool allocated by\nget_pathname().\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n builtin/checkout.c | 13 +++++----\n cache.h            |  4 +--\n path.c             | 11 ++------\n refs.c             | 78 +++++++++++++++++++++++++++++++++---------------------\n refs.h             |  2 +-\n 5 files changed, 61 insertions(+), 47 deletions(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex d402d7a..220f80e 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -588,18 +588,21 @@ static void update_refs_for_switch(const struct checkout_opts *opts,\n \t\tif (opts->new_orphan_branch) {\n \t\t\tif (opts->new_branch_log && !log_all_ref_updates) {\n \t\t\t\tint temp;\n-\t\t\t\tchar log_file[PATH_MAX];\n-\t\t\t\tconst char *ref_name = mkpath(\"refs/heads/%s\", opts->new_orphan_branch);\n+\t\t\t\tstruct strbuf log_file = STRBUF_INIT;\n+\t\t\t\tint ret;\n+\t\t\t\tconst char *ref_name;\n \n+\t\t\t\tref_name = mkpath(\"refs/heads/%s\", opts->new_orphan_branch);\n \t\t\t\ttemp = log_all_ref_updates;\n \t\t\t\tlog_all_ref_updates = 1;\n-\t\t\t\tif (log_ref_setup(ref_name, log_file, sizeof(log_file))) {\n+\t\t\t\tret = log_ref_setup(ref_name, &log_file);\n+\t\t\t\tlog_all_ref_updates = temp;\n+\t\t\t\tstrbuf_release(&log_file);\n+\t\t\t\tif (ret) {\n \t\t\t\t\tfprintf(stderr, _(\"Can not do reflog for '%s'\\n\"),\n \t\t\t\t\t    opts->new_orphan_branch);\n-\t\t\t\t\tlog_all_ref_updates = temp;\n \t\t\t\t\treturn;\n \t\t\t\t}\n-\t\t\t\tlog_all_ref_updates = temp;\n \t\t\t}\n \t\t}\n \t\telse\ndiff --git a/cache.h b/cache.h\nindex 2e4d88f..f487c0e 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -697,8 +697,8 @@ extern int check_repository_format(void);\n \n extern char *mksnpath(char *buf, size_t n, const char *fmt, ...)\n \t__attribute__((format (printf, 3, 4)));\n-extern char *git_snpath(char *buf, size_t n, const char *fmt, ...)\n-\t__attribute__((format (printf, 3, 4)));\n+extern void strbuf_git_path(struct strbuf *sb, const char *fmt, ...)\n+\t__attribute__((format (printf, 2, 3)));\n extern char *git_pathdup(const char *fmt, ...)\n \t__attribute__((format (printf, 1, 2)));\n extern char *mkpathdup(const char *fmt, ...)\ndiff --git a/path.c b/path.c\nindex a7ceea2..47753aa 100644\n--- a/path.c\n+++ b/path.c\n@@ -70,19 +70,12 @@ static void vsnpath(struct strbuf *buf, const char *fmt, va_list args)\n \tstrbuf_cleanup_path(buf);\n }\n \n-char *git_snpath(char *buf, size_t n, const char *fmt, ...)\n+void strbuf_git_path(struct strbuf *sb, const char *fmt, ...)\n {\n-\tstruct strbuf sb = STRBUF_INIT;\n \tva_list args;\n \tva_start(args, fmt);\n-\tvsnpath(&sb, fmt, args);\n+\tvsnpath(sb, fmt, args);\n \tva_end(args);\n-\tif (sb.len >= n)\n-\t\tstrlcpy(buf, bad_path, n);\n-\telse\n-\t\tmemcpy(buf, sb.buf, sb.len + 1);\n-\tstrbuf_release(&sb);\n-\treturn buf;\n }\n \n char *git_pathdup(const char *fmt, ...)\ndiff --git a/refs.c b/refs.c\nindex f616adc..3cefbd3 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -1400,10 +1400,12 @@ static const char *handle_missing_loose_ref(const char *refname,\n /* This function needs to return a meaningful errno on failure */\n const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int reading, int *flag)\n {\n+\tstruct strbuf sb_path = STRBUF_INIT;\n \tint depth = MAXDEPTH;\n \tssize_t len;\n \tchar buffer[256];\n \tstatic char refname_buffer[256];\n+\tconst char *ret;\n \n \tif (flag)\n \t\t*flag = 0;\n@@ -1414,17 +1416,19 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea\n \t}\n \n \tfor (;;) {\n-\t\tchar path[PATH_MAX];\n+\t\tconst char *path;\n \t\tstruct stat st;\n \t\tchar *buf;\n \t\tint fd;\n \n \t\tif (--depth < 0) {\n \t\t\terrno = ELOOP;\n-\t\t\treturn NULL;\n+\t\t\tgoto fail;\n \t\t}\n \n-\t\tgit_snpath(path, sizeof(path), \"%s\", refname);\n+\t\tstrbuf_reset(&sb_path);\n+\t\tstrbuf_git_path(&sb_path, \"%s\", refname);\n+\t\tpath = sb_path.buf;\n \n \t\t/*\n \t\t * We might have to loop back here to avoid a race\n@@ -1438,10 +1442,11 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea\n \tstat_ref:\n \t\tif (lstat(path, &st) < 0) {\n \t\t\tif (errno == ENOENT)\n-\t\t\t\treturn handle_missing_loose_ref(refname, sha1,\n-\t\t\t\t\t\t\t\treading, flag);\n+\t\t\t\tret = handle_missing_loose_ref(refname, sha1,\n+\t\t\t\t\t\t\t       reading, flag);\n \t\t\telse\n-\t\t\t\treturn NULL;\n+\t\t\t\tret = NULL;\n+\t\t\tgoto done;\n \t\t}\n \n \t\t/* Follow \"normalized\" - ie \"refs/..\" symlinks by hand */\n@@ -1452,7 +1457,7 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea\n \t\t\t\t\t/* inconsistent with lstat; retry */\n \t\t\t\t\tgoto stat_ref;\n \t\t\t\telse\n-\t\t\t\t\treturn NULL;\n+\t\t\t\t\tgoto fail;\n \t\t\t}\n \t\t\tbuffer[len] = 0;\n \t\t\tif (starts_with(buffer, \"refs/\") &&\n@@ -1468,7 +1473,7 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea\n \t\t/* Is it a directory? */\n \t\tif (S_ISDIR(st.st_mode)) {\n \t\t\terrno = EISDIR;\n-\t\t\treturn NULL;\n+\t\t\tgoto fail;\n \t\t}\n \n \t\t/*\n@@ -1481,14 +1486,15 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea\n \t\t\t\t/* inconsistent with lstat; retry */\n \t\t\t\tgoto stat_ref;\n \t\t\telse\n-\t\t\t\treturn NULL;\n+\t\t\t\tgoto fail;\n \t\t}\n+\n \t\tlen = read_in_full(fd, buffer, sizeof(buffer)-1);\n \t\tif (len < 0) {\n \t\t\tint save_errno = errno;\n \t\t\tclose(fd);\n \t\t\terrno = save_errno;\n-\t\t\treturn NULL;\n+\t\t\tgoto fail;\n \t\t}\n \t\tclose(fd);\n \t\twhile (len && isspace(buffer[len-1]))\n@@ -1508,9 +1514,10 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea\n \t\t\t\tif (flag)\n \t\t\t\t\t*flag |= REF_ISBROKEN;\n \t\t\t\terrno = EINVAL;\n-\t\t\t\treturn NULL;\n+\t\t\t\tgoto fail;\n \t\t\t}\n-\t\t\treturn refname;\n+\t\t\tret = refname;\n+\t\t\tgoto done;\n \t\t}\n \t\tif (flag)\n \t\t\t*flag |= REF_ISSYMREF;\n@@ -1521,10 +1528,15 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea\n \t\t\tif (flag)\n \t\t\t\t*flag |= REF_ISBROKEN;\n \t\t\terrno = EINVAL;\n-\t\t\treturn NULL;\n+\t\t\tgoto fail;\n \t\t}\n \t\trefname = strcpy(refname_buffer, buf);\n \t}\n+fail:\n+\tret = NULL;\n+done:\n+\tstrbuf_release(&sb_path);\n+\treturn ret;\n }\n \n char *resolve_refdup(const char *ref, unsigned char *sha1, int reading, int *flag)\n@@ -2832,51 +2844,51 @@ static int copy_msg(char *buf, const char *msg)\n }\n \n /* This function must set a meaningful errno on failure */\n-int log_ref_setup(const char *refname, char *logfile, int bufsize)\n+int log_ref_setup(const char *refname, struct strbuf *logfile)\n {\n \tint logfd, oflags = O_APPEND | O_WRONLY;\n \n-\tgit_snpath(logfile, bufsize, \"logs/%s\", refname);\n+\tstrbuf_git_path(logfile, \"logs/%s\", refname);\n \tif (log_all_ref_updates &&\n \t    (starts_with(refname, \"refs/heads/\") ||\n \t     starts_with(refname, \"refs/remotes/\") ||\n \t     starts_with(refname, \"refs/notes/\") ||\n \t     !strcmp(refname, \"HEAD\"))) {\n-\t\tif (safe_create_leading_directories(logfile) < 0) {\n+\t\tif (safe_create_leading_directories(logfile->buf) < 0) {\n \t\t\tint save_errno = errno;\n-\t\t\terror(\"unable to create directory for %s\", logfile);\n+\t\t\terror(\"unable to create directory for %s\", logfile->buf);\n \t\t\terrno = save_errno;\n \t\t\treturn -1;\n \t\t}\n \t\toflags |= O_CREAT;\n \t}\n \n-\tlogfd = open(logfile, oflags, 0666);\n+\tlogfd = open(logfile->buf, oflags, 0666);\n \tif (logfd < 0) {\n \t\tif (!(oflags & O_CREAT) && errno == ENOENT)\n \t\t\treturn 0;\n \n \t\tif ((oflags & O_CREAT) && errno == EISDIR) {\n-\t\t\tif (remove_empty_directories(logfile)) {\n+\t\t\tif (remove_empty_directories(logfile->buf)) {\n \t\t\t\tint save_errno = errno;\n \t\t\t\terror(\"There are still logs under '%s'\",\n-\t\t\t\t      logfile);\n+\t\t\t\t      logfile->buf);\n \t\t\t\terrno = save_errno;\n \t\t\t\treturn -1;\n \t\t\t}\n-\t\t\tlogfd = open(logfile, oflags, 0666);\n+\t\t\tlogfd = open(logfile->buf, oflags, 0666);\n \t\t}\n \n \t\tif (logfd < 0) {\n \t\t\tint save_errno = errno;\n-\t\t\terror(\"Unable to append to %s: %s\", logfile,\n+\t\t\terror(\"Unable to append to %s: %s\", logfile->buf,\n \t\t\t      strerror(errno));\n \t\t\terrno = save_errno;\n \t\t\treturn -1;\n \t\t}\n \t}\n \n-\tadjust_shared_perm(logfile);\n+\tadjust_shared_perm(logfile->buf);\n \tclose(logfd);\n \treturn 0;\n }\n@@ -2887,20 +2899,22 @@ static int log_ref_write(const char *refname, const unsigned char *old_sha1,\n \tint logfd, result, written, oflags = O_APPEND | O_WRONLY;\n \tunsigned maxlen, len;\n \tint msglen;\n-\tchar log_file[PATH_MAX];\n+\tstruct strbuf sb_log_file = STRBUF_INIT;\n+\tconst char *log_file;\n \tchar *logrec;\n \tconst char *committer;\n \n \tif (log_all_ref_updates < 0)\n \t\tlog_all_ref_updates = !is_bare_repository();\n \n-\tresult = log_ref_setup(refname, log_file, sizeof(log_file));\n+\tresult = log_ref_setup(refname, &sb_log_file);\n \tif (result)\n-\t\treturn result;\n+\t\tgoto done;\n+\tlog_file = sb_log_file.buf;\n \n \tlogfd = open(log_file, oflags);\n \tif (logfd < 0)\n-\t\treturn 0;\n+\t\tgoto done;\n \tmsglen = msg ? strlen(msg) : 0;\n \tcommitter = git_committer_info(0);\n \tmaxlen = strlen(committer) + msglen + 100;\n@@ -2918,15 +2932,19 @@ static int log_ref_write(const char *refname, const unsigned char *old_sha1,\n \t\tclose(logfd);\n \t\terror(\"Unable to append to %s\", log_file);\n \t\terrno = save_errno;\n-\t\treturn -1;\n+\t\tresult = -1;\n+\t\tgoto done;\n \t}\n \tif (close(logfd)) {\n \t\tint save_errno = errno;\n \t\terror(\"Unable to append to %s\", log_file);\n \t\terrno = save_errno;\n-\t\treturn -1;\n+\t\tresult = -1;\n+\t\tgoto done;\n \t}\n-\treturn 0;\n+done:\n+\tstrbuf_release(&sb_log_file);\n+\treturn result;\n }\n \n int is_branch(const char *refname)\ndiff --git a/refs.h b/refs.h\nindex 10fc3a2..c990a85 100644\n--- a/refs.h\n+++ b/refs.h\n@@ -203,7 +203,7 @@ extern int write_ref_sha1(struct ref_lock *lock, const unsigned char *sha1, cons\n /*\n  * Setup reflog before using. Set errno to something meaningful on failure.\n  */\n-int log_ref_setup(const char *refname, char *logfile, int bufsize);\n+int log_ref_setup(const char *refname, struct strbuf *logfile);\n \n /** Reads log for the value of ref during at_time. **/\n extern int read_ref_at(const char *refname, unsigned long at_time, int cnt,\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249931","messageId":"1411867366-3821-5-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1411867366-3821-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v3 04/32] path.c: rename vsnpath() to do_git_path()","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-28T01:22:18Z","receivedAt":"2014-09-28T01:22:18Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"The name vsnpath() gives an impression that this is general path\nhandling function. It's not. This is the underlying implementation of\ngit_path(), git_pathdup() and strbuf_git_path() which will prefix\n$GIT_DIR in the result string.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n path.c | 8 ++++----\n 1 file changed, 4 insertions(+), 4 deletions(-)\n\ndiff --git a/path.c b/path.c\nindex 47753aa..6991103 100644\n--- a/path.c\n+++ b/path.c\n@@ -60,7 +60,7 @@ char *mksnpath(char *buf, size_t n, const char *fmt, ...)\n \treturn cleanup_path(buf);\n }\n \n-static void vsnpath(struct strbuf *buf, const char *fmt, va_list args)\n+static void do_git_path(struct strbuf *buf, const char *fmt, va_list args)\n {\n \tconst char *git_dir = get_git_dir();\n \tstrbuf_addstr(buf, git_dir);\n@@ -74,7 +74,7 @@ void strbuf_git_path(struct strbuf *sb, const char *fmt, ...)\n {\n \tva_list args;\n \tva_start(args, fmt);\n-\tvsnpath(sb, fmt, args);\n+\tdo_git_path(sb, fmt, args);\n \tva_end(args);\n }\n \n@@ -83,7 +83,7 @@ char *git_pathdup(const char *fmt, ...)\n \tstruct strbuf path = STRBUF_INIT;\n \tva_list args;\n \tva_start(args, fmt);\n-\tvsnpath(&path, fmt, args);\n+\tdo_git_path(&path, fmt, args);\n \tva_end(args);\n \treturn strbuf_detach(&path, NULL);\n }\n@@ -114,7 +114,7 @@ const char *git_path(const char *fmt, ...)\n \tstruct strbuf *pathname = get_pathname();\n \tva_list args;\n \tva_start(args, fmt);\n-\tvsnpath(pathname, fmt, args);\n+\tdo_git_path(pathname, fmt, args);\n \tva_end(args);\n \treturn pathname->buf;\n }\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249932","messageId":"1411867366-3821-6-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1411867366-3821-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v3 05/32] path.c: group git_path(), git_pathdup() and strbuf_git_path() together","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-28T01:22:19Z","receivedAt":"2014-09-28T01:22:19Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n path.c | 20 ++++++++++----------\n 1 file changed, 10 insertions(+), 10 deletions(-)\n\ndiff --git a/path.c b/path.c\nindex 6991103..df0f75b 100644\n--- a/path.c\n+++ b/path.c\n@@ -78,6 +78,16 @@ void strbuf_git_path(struct strbuf *sb, const char *fmt, ...)\n \tva_end(args);\n }\n \n+const char *git_path(const char *fmt, ...)\n+{\n+\tstruct strbuf *pathname = get_pathname();\n+\tva_list args;\n+\tva_start(args, fmt);\n+\tdo_git_path(pathname, fmt, args);\n+\tva_end(args);\n+\treturn pathname->buf;\n+}\n+\n char *git_pathdup(const char *fmt, ...)\n {\n \tstruct strbuf path = STRBUF_INIT;\n@@ -109,16 +119,6 @@ const char *mkpath(const char *fmt, ...)\n \treturn cleanup_path(pathname->buf);\n }\n \n-const char *git_path(const char *fmt, ...)\n-{\n-\tstruct strbuf *pathname = get_pathname();\n-\tva_list args;\n-\tva_start(args, fmt);\n-\tdo_git_path(pathname, fmt, args);\n-\tva_end(args);\n-\treturn pathname->buf;\n-}\n-\n void home_config_paths(char **global, char **xdg, char *file)\n {\n \tchar *xdg_home = getenv(\"XDG_CONFIG_HOME\");\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249933","messageId":"1411867366-3821-7-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1411867366-3821-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v3 06/32] git_path(): be aware of file relocation in $GIT_DIR","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-28T01:22:20Z","receivedAt":"2014-09-28T01:22:20Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"We allow the user to relocate certain paths out of $GIT_DIR via\nenvironment variables, e.g. GIT_OBJECT_DIRECTORY, GIT_INDEX_FILE and\nGIT_GRAFT_FILE. Callers are not supposed to use git_path() or\ngit_pathdup() to get those paths. Instead they must use\nget_object_directory(), get_index_file() and get_graft_file()\nrespectively. This is inconvenient and could be missed in review (for\nexample, there's git_path(\"objects/info/alternates\") somewhere in\nsha1_file.c).\n\nThis patch makes git_path() and git_pathdup() understand those\nenvironment variables. So if you set GIT_OBJECT_DIRECTORY to /foo/bar,\ngit_path(\"objects/abc\") should return /foo/bar/abc. The same is done\nfor the two remaining env variables.\n\n\"git rev-parse --git-path\" is the wrapper for script use.\n\nThis patch kinda reverts a0279e1 (setup_git_env: use git_pathdup\ninstead of xmalloc + sprintf - 2014-06-19) because using git_pathdup\nhere would result in infinite recursion:\n\n  setup_git_env() -> git_pathdup(\"objects\") -> .. -> adjust_git_path()\n  -> get_object_directory() -> oops, git_object_directory is NOT set\n  yet -> setup_git_env()\n\nI wanted to make git_pathdup_literal() that skips adjust_git_path().\nBut that won't work because later on when $GIT_COMMON_DIR is\nintroduced, git_pathdup_literal(\"objects\") needs adjust_git_path() to\nreplace $GIT_DIR with $GIT_COMMON_DIR.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n Documentation/git-rev-parse.txt |  7 ++++++\n builtin/rev-parse.c             |  7 ++++++\n cache.h                         |  1 +\n environment.c                   | 19 +++++++++++-----\n path.c                          | 49 +++++++++++++++++++++++++++++++++++++++--\n t/t0060-path-utils.sh           | 19 ++++++++++++++++\n 6 files changed, 95 insertions(+), 7 deletions(-)\n\ndiff --git a/Documentation/git-rev-parse.txt b/Documentation/git-rev-parse.txt\nindex 0b84769..0fed5a4 100644\n--- a/Documentation/git-rev-parse.txt\n+++ b/Documentation/git-rev-parse.txt\n@@ -232,6 +232,13 @@ print a message to stderr and exit with nonzero status.\n \trepository.  If <path> is a gitfile then the resolved path\n \tto the real repository is printed.\n \n+--git-path <path>::\n+\tResolve \"$GIT_DIR/<path>\" and takes other path relocation\n+\tvariables such as $GIT_OBJECT_DIRECTORY,\n+\t$GIT_INDEX_FILE... into account. For example, if\n+\t$GIT_OBJECT_DIRECTORY is set to /foo/bar then \"git rev-parse\n+\t--git-path objects/abc\" returns /foo/bar/abc.\n+\n --show-cdup::\n \tWhen the command is invoked from a subdirectory, show the\n \tpath of the top-level directory relative to the current\ndiff --git a/builtin/rev-parse.c b/builtin/rev-parse.c\nindex c911b45..a6f4697 100644\n--- a/builtin/rev-parse.c\n+++ b/builtin/rev-parse.c\n@@ -531,6 +531,13 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)\n \tfor (i = 1; i < argc; i++) {\n \t\tconst char *arg = argv[i];\n \n+\t\tif (!strcmp(arg, \"--git-path\")) {\n+\t\t\tif (!argv[i + 1])\n+\t\t\t\tdie(\"--git-path requires an argument\");\n+\t\t\tputs(git_path(\"%s\", argv[i + 1]));\n+\t\t\ti++;\n+\t\t\tcontinue;\n+\t\t}\n \t\tif (as_is) {\n \t\t\tif (show_file(arg, output_prefix) && as_is < 2)\n \t\t\t\tverify_filename(prefix, arg, 0);\ndiff --git a/cache.h b/cache.h\nindex f487c0e..161dbd5 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -635,6 +635,7 @@ extern int fsync_object_files;\n extern int core_preload_index;\n extern int core_apply_sparse_checkout;\n extern int precomposed_unicode;\n+extern int git_db_env, git_index_env, git_graft_env;\n \n /*\n  * The character that begins a commented line in user-editable file\ndiff --git a/environment.c b/environment.c\nindex 565f652..fee12a6 100644\n--- a/environment.c\n+++ b/environment.c\n@@ -83,6 +83,7 @@ static size_t namespace_len;\n \n static const char *git_dir;\n static char *git_object_dir, *git_index_file, *git_graft_file;\n+int git_db_env, git_index_env, git_graft_env;\n \n /*\n  * Repository-local GIT_* environment variables; see cache.h for details.\n@@ -124,10 +125,18 @@ static char *expand_namespace(const char *raw_namespace)\n \treturn strbuf_detach(&buf, NULL);\n }\n \n-static char *git_path_from_env(const char *envvar, const char *path)\n+static char *git_path_from_env(const char *envvar, const char *path,\n+\t\t\t       int *fromenv)\n {\n \tconst char *value = getenv(envvar);\n-\treturn value ? xstrdup(value) : git_pathdup(\"%s\", path);\n+\tif (!value) {\n+\t\tchar *buf = xmalloc(strlen(git_dir) + strlen(path) + 2);\n+\t\tsprintf(buf, \"%s/%s\", git_dir, path);\n+\t\treturn buf;\n+\t}\n+\tif (fromenv)\n+\t\t*fromenv = 1;\n+\treturn xstrdup(value);\n }\n \n static void setup_git_env(void)\n@@ -140,9 +149,9 @@ static void setup_git_env(void)\n \t\tgit_dir = DEFAULT_GIT_DIR_ENVIRONMENT;\n \tgitfile = read_gitfile(git_dir);\n \tgit_dir = xstrdup(gitfile ? gitfile : git_dir);\n-\tgit_object_dir = git_path_from_env(DB_ENVIRONMENT, \"objects\");\n-\tgit_index_file = git_path_from_env(INDEX_ENVIRONMENT, \"index\");\n-\tgit_graft_file = git_path_from_env(GRAFT_ENVIRONMENT, \"info/grafts\");\n+\tgit_object_dir = git_path_from_env(DB_ENVIRONMENT, \"objects\", &git_db_env);\n+\tgit_index_file = git_path_from_env(INDEX_ENVIRONMENT, \"index\", &git_index_env);\n+\tgit_graft_file = git_path_from_env(GRAFT_ENVIRONMENT, \"info/grafts\", &git_graft_env);\n \tif (getenv(NO_REPLACE_OBJECTS_ENVIRONMENT))\n \t\tcheck_replace_refs = 0;\n \tnamespace = expand_namespace(getenv(GIT_NAMESPACE_ENVIRONMENT));\ndiff --git a/path.c b/path.c\nindex df0f75b..4910783 100644\n--- a/path.c\n+++ b/path.c\n@@ -60,13 +60,58 @@ char *mksnpath(char *buf, size_t n, const char *fmt, ...)\n \treturn cleanup_path(buf);\n }\n \n+static int dir_prefix(const char *buf, const char *dir)\n+{\n+\tint len = strlen(dir);\n+\treturn !strncmp(buf, dir, len) &&\n+\t\t(is_dir_sep(buf[len]) || buf[len] == '\\0');\n+}\n+\n+/* $buf =~ m|$dir/+$file| but without regex */\n+static int is_dir_file(const char *buf, const char *dir, const char *file)\n+{\n+\tint len = strlen(dir);\n+\tif (strncmp(buf, dir, len) || !is_dir_sep(buf[len]))\n+\t\treturn 0;\n+\twhile (is_dir_sep(buf[len]))\n+\t\tlen++;\n+\treturn !strcmp(buf + len, file);\n+}\n+\n+static void replace_dir(struct strbuf *buf, int len, const char *newdir)\n+{\n+\tint newlen = strlen(newdir);\n+\tint need_sep = (buf->buf[len] && !is_dir_sep(buf->buf[len])) &&\n+\t\t!is_dir_sep(newdir[newlen - 1]);\n+\tif (need_sep)\n+\t\tlen--;\t /* keep one char, to be replaced with '/'  */\n+\tstrbuf_splice(buf, 0, len, newdir, newlen);\n+\tif (need_sep)\n+\t\tbuf->buf[newlen] = '/';\n+}\n+\n+static void adjust_git_path(struct strbuf *buf, int git_dir_len)\n+{\n+\tconst char *base = buf->buf + git_dir_len;\n+\tif (git_graft_env && is_dir_file(base, \"info\", \"grafts\"))\n+\t\tstrbuf_splice(buf, 0, buf->len,\n+\t\t\t      get_graft_file(), strlen(get_graft_file()));\n+\telse if (git_index_env && !strcmp(base, \"index\"))\n+\t\tstrbuf_splice(buf, 0, buf->len,\n+\t\t\t      get_index_file(), strlen(get_index_file()));\n+\telse if (git_db_env && dir_prefix(base, \"objects\"))\n+\t\treplace_dir(buf, git_dir_len + 7, get_object_directory());\n+}\n+\n static void do_git_path(struct strbuf *buf, const char *fmt, va_list args)\n {\n-\tconst char *git_dir = get_git_dir();\n-\tstrbuf_addstr(buf, git_dir);\n+\tint gitdir_len;\n+\tstrbuf_addstr(buf, get_git_dir());\n \tif (buf->len && !is_dir_sep(buf->buf[buf->len - 1]))\n \t\tstrbuf_addch(buf, '/');\n+\tgitdir_len = buf->len;\n \tstrbuf_vaddf(buf, fmt, args);\n+\tadjust_git_path(buf, gitdir_len);\n \tstrbuf_cleanup_path(buf);\n }\n \ndiff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh\nindex c0143a0..33d2818 100755\n--- a/t/t0060-path-utils.sh\n+++ b/t/t0060-path-utils.sh\n@@ -19,6 +19,14 @@ relative_path() {\n \t\"test \\\"\\$(test-path-utils relative_path '$1' '$2')\\\" = '$expected'\"\n }\n \n+test_git_path() {\n+\ttest_expect_success \"git-path $1 $2 => $3\" \"\n+\t\t$1 git rev-parse --git-path $2 >actual &&\n+\t\techo $3 >expect &&\n+\t\ttest_cmp expect actual\n+\t\"\n+}\n+\n # On Windows, we are using MSYS's bash, which mangles the paths.\n # Absolute paths are anchored at the MSYS installation directory,\n # which means that the path / accounts for this many characters:\n@@ -244,4 +252,15 @@ relative_path \"<null>\"\t\t\"<empty>\"\t./\n relative_path \"<null>\"\t\t\"<null>\"\t./\n relative_path \"<null>\"\t\t/foo/a/b\t./\n \n+test_git_path A=B                info/grafts .git/info/grafts\n+test_git_path GIT_GRAFT_FILE=foo info/grafts foo\n+test_git_path GIT_GRAFT_FILE=foo info/////grafts foo\n+test_git_path GIT_INDEX_FILE=foo index foo\n+test_git_path GIT_INDEX_FILE=foo index/foo .git/index/foo\n+test_git_path GIT_INDEX_FILE=foo index2 .git/index2\n+test_expect_success 'setup fake objects directory foo' 'mkdir foo'\n+test_git_path GIT_OBJECT_DIRECTORY=foo objects foo\n+test_git_path GIT_OBJECT_DIRECTORY=foo objects/foo foo/foo\n+test_git_path GIT_OBJECT_DIRECTORY=foo objects2 .git/objects2\n+\n test_done\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249934","messageId":"1411867366-3821-8-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1411867366-3821-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v3 07/32] *.sh: respect $GIT_INDEX_FILE","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-28T01:22:21Z","receivedAt":"2014-09-28T01:22:21Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n git-pull.sh  | 2 +-\n git-stash.sh | 2 +-\n 2 files changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/git-pull.sh b/git-pull.sh\nindex 4d4fc77..ad44226 100755\n--- a/git-pull.sh\n+++ b/git-pull.sh\n@@ -240,7 +240,7 @@ test true = \"$rebase\" && {\n \tif ! git rev-parse -q --verify HEAD >/dev/null\n \tthen\n \t\t# On an unborn branch\n-\t\tif test -f \"$GIT_DIR/index\"\n+\t\tif test -f \"$(git rev-parse --git-path index)\"\n \t\tthen\n \t\t\tdie \"$(gettext \"updating an unborn branch with changes added to the index\")\"\n \t\tfi\ndiff --git a/git-stash.sh b/git-stash.sh\nindex 0158c73..4294aa0 100755\n--- a/git-stash.sh\n+++ b/git-stash.sh\n@@ -20,7 +20,7 @@ require_work_tree\n cd_to_toplevel\n \n TMP=\"$GIT_DIR/.git-stash.$$\"\n-TMPindex=${GIT_INDEX_FILE-\"$GIT_DIR/index\"}.stash.$$\n+TMPindex=${GIT_INDEX_FILE-\"$(git rev-parse --git-path index)\"}.stash.$$\n trap 'rm -f \"$TMP-\"* \"$TMPindex\"' 0\n \n ref_stash=refs/stash\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249935","messageId":"1411867366-3821-9-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1411867366-3821-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v3 08/32] reflog: avoid constructing .lock path with git_path","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-28T01:22:22Z","receivedAt":"2014-09-28T01:22:22Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Among pathnames in $GIT_DIR, e.g. \"index\" or \"packed-refs\", we want to\nautomatically and silently map some of them to the $GIT_DIR of the\nrepository we are borrowing from via $GIT_COMMON_DIR mechanism.  When\nwe formulate the pathname for its lockfile, we want it to be in the\nsame location as its final destination.  \"index\" is not shared and\nneeds to remain in the borrowing repository, while \"packed-refs\" is\nshared and needs to go to the borrowed repository.\n\ngit_path() could be taught about the \".lock\" suffix and map\n\"index.lock\" and \"packed-refs.lock\" the same way their basenames are\nmapped, but instead the caller can help by asking where the basename\n(e.g. \"index\") is mapped to git_path() and then appending \".lock\"\nafter the mapping is done.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n builtin/reflog.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/builtin/reflog.c b/builtin/reflog.c\nindex e8a8fb1..9bd874d 100644\n--- a/builtin/reflog.c\n+++ b/builtin/reflog.c\n@@ -372,7 +372,7 @@ static int expire_reflog(const char *ref, const unsigned char *sha1, int unused,\n \tif (!reflog_exists(ref))\n \t\tgoto finish;\n \tif (!cmd->dry_run) {\n-\t\tnewlog_path = git_pathdup(\"logs/%s.lock\", ref);\n+\t\tnewlog_path = mkpathdup(\"%s.lock\", log_file);\n \t\tcb.newlog = fopen(newlog_path, \"w\");\n \t}\n \n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249936","messageId":"1411867366-3821-10-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1411867366-3821-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v3 09/32] fast-import: use git_path() for accessing .git dir instead of get_git_dir()","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-28T01:22:23Z","receivedAt":"2014-09-28T01:22:23Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"This allows git_path() to redirect info/fast-import to another place\nif needed\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n fast-import.c | 5 +----\n 1 file changed, 1 insertion(+), 4 deletions(-)\n\ndiff --git a/fast-import.c b/fast-import.c\nindex 9ddcd3b..69e4c3f 100644\n--- a/fast-import.c\n+++ b/fast-import.c\n@@ -3130,12 +3130,9 @@ static void parse_progress(void)\n \n static char* make_fast_import_path(const char *path)\n {\n-\tstruct strbuf abs_path = STRBUF_INIT;\n-\n \tif (!relative_marks_paths || is_absolute_path(path))\n \t\treturn xstrdup(path);\n-\tstrbuf_addf(&abs_path, \"%s/info/fast-import/%s\", get_git_dir(), path);\n-\treturn strbuf_detach(&abs_path, NULL);\n+\treturn xstrdup(git_path(\"info/fast-import/%s\", path));\n }\n \n static void option_import_marks(const char *marks,\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249937","messageId":"1411867366-3821-11-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1411867366-3821-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v3 10/32] commit: use SEQ_DIR instead of hardcoding \"sequencer\"","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-28T01:22:24Z","receivedAt":"2014-09-28T01:22:24Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n builtin/commit.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex b0fe784..08df727 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -169,7 +169,7 @@ static void determine_whence(struct wt_status *s)\n \t\twhence = FROM_MERGE;\n \telse if (file_exists(git_path(\"CHERRY_PICK_HEAD\"))) {\n \t\twhence = FROM_CHERRY_PICK;\n-\t\tif (file_exists(git_path(\"sequencer\")))\n+\t\tif (file_exists(git_path(SEQ_DIR)))\n \t\t\tsequencer_in_use = 1;\n \t}\n \telse\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249938","messageId":"1411867366-3821-12-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1411867366-3821-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v3 11/32] $GIT_COMMON_DIR: a new environment variable","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-28T01:22:25Z","receivedAt":"2014-09-28T01:22:25Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"This variable is intended to support multiple working directories\nattached to a repository. Such a repository may have a main working\ndirectory, created by either \"git init\" or \"git clone\" and one or more\nlinked working directories. These working directories and the main\nrepository share the same repository directory.\n\nIn linked working directories, $GIT_COMMON_DIR must be defined to point\nto the real repository directory and $GIT_DIR points to an unused\nsubdirectory inside $GIT_COMMON_DIR. File locations inside the\nrepository are reorganized from the linked worktree view point:\n\n - worktree-specific such as HEAD, logs/HEAD, index, other top-level\n   refs and unrecognized files are from $GIT_DIR.\n\n - the rest like objects, refs, info, hooks, packed-refs, shallow...\n   are from $GIT_COMMON_DIR (except info/sparse-checkout, but that's\n   a separate patch)\n\nScripts are supposed to retrieve paths in $GIT_DIR with \"git rev-parse\n--git-path\", which will take care of \"$GIT_DIR vs $GIT_COMMON_DIR\"\nbusiness.\n\nThe redirection is done by git_path(), git_pathdup() and\nstrbuf_git_path(). The selected list of paths goes to $GIT_COMMON_DIR,\nnot the other way around in case a developer adds a new\nworktree-specific file and it's accidentally promoted to be shared\nacross repositories (this includes unknown files added by third party\ncommands)\n\nThe list of known files that belong to $GIT_DIR are:\n\nADD_EDIT.patch BISECT_ANCESTORS_OK BISECT_EXPECTED_REV BISECT_LOG\nBISECT_NAMES CHERRY_PICK_HEAD COMMIT_MSG FETCH_HEAD HEAD MERGE_HEAD\nMERGE_MODE MERGE_RR NOTES_EDITMSG NOTES_MERGE_WORKTREE ORIG_HEAD\nREVERT_HEAD SQUASH_MSG TAG_EDITMSG fast_import_crash_* logs/HEAD\nnext-index-* rebase-apply rebase-merge rsync-refs-* sequencer/*\nshallow_*\n\nPath mapping is NOT done for git_path_submodule(). Multi-checkouts are\nnot supported as submodules.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n Documentation/git.txt                  |  8 ++++++\n Documentation/gitrepository-layout.txt | 45 +++++++++++++++++++++++++---------\n cache.h                                |  4 ++-\n environment.c                          | 28 +++++++++++++++------\n path.c                                 | 34 +++++++++++++++++++++++++\n t/t0060-path-utils.sh                  | 16 ++++++++++++\n 6 files changed, 116 insertions(+), 19 deletions(-)\n\ndiff --git a/Documentation/git.txt b/Documentation/git.txt\nindex 8b2c542..176d37c 100644\n--- a/Documentation/git.txt\n+++ b/Documentation/git.txt\n@@ -802,6 +802,14 @@ Git so take care if using Cogito etc.\n \tan explicit repository directory set via 'GIT_DIR' or on the\n \tcommand line.\n \n+'GIT_COMMON_DIR'::\n+\tIf this variable is set to a path, non-worktree files that are\n+\tnormally in $GIT_DIR will be taken from this path\n+\tinstead. Worktree-specific files such as HEAD or index are\n+\ttaken from $GIT_DIR. See linkgit:gitrepository-layout[5] for\n+\tdetails. This variable has lower precedence than other path\n+\tvariables such as GIT_INDEX_FILE, GIT_OBJECT_DIRECTORY...\n+\n Git Commits\n ~~~~~~~~~~~\n 'GIT_AUTHOR_NAME'::\ndiff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt\nindex 79653f3..2b5966a 100644\n--- a/Documentation/gitrepository-layout.txt\n+++ b/Documentation/gitrepository-layout.txt\n@@ -46,6 +46,9 @@ of incomplete object store is not suitable to be published for\n use with dumb transports but otherwise is OK as long as\n `objects/info/alternates` points at the object stores it\n borrows from.\n++\n+This directory is ignored if $GIT_COMMON_DIR is set and\n+\"$GIT_COMMON_DIR/objects\" will be used instead.\n \n objects/[0-9a-f][0-9a-f]::\n \tA newly created object is stored in its own file.\n@@ -92,7 +95,8 @@ refs::\n \tReferences are stored in subdirectories of this\n \tdirectory.  The 'git prune' command knows to preserve\n \tobjects reachable from refs found in this directory and\n-\tits subdirectories.\n+\tits subdirectories. This directory is ignored if $GIT_COMMON_DIR\n+\tis set and \"$GIT_COMMON_DIR/refs\" will be used instead.\n \n refs/heads/`name`::\n \trecords tip-of-the-tree commit objects of branch `name`\n@@ -114,7 +118,8 @@ refs/replace/`<obj-sha1>`::\n packed-refs::\n \trecords the same information as refs/heads/, refs/tags/,\n \tand friends record in a more efficient way.  See\n-\tlinkgit:git-pack-refs[1].\n+\tlinkgit:git-pack-refs[1]. This file is ignored if $GIT_COMMON_DIR\n+\tis set and \"$GIT_COMMON_DIR/packed-refs\" will be used instead.\n \n HEAD::\n \tA symref (see glossary) to the `refs/heads/` namespace\n@@ -133,6 +138,11 @@ being a symref to point at the current branch.  Such a state\n is often called 'detached HEAD.'  See linkgit:git-checkout[1]\n for details.\n \n+config::\n+\tRepository specific configuration file. This file is ignored\n+\tif $GIT_COMMON_DIR is set and \"$GIT_COMMON_DIR/config\" will be\n+\tused instead.\n+\n branches::\n \tA slightly deprecated way to store shorthands to be used\n \tto specify a URL to 'git fetch', 'git pull' and 'git push'.\n@@ -140,7 +150,10 @@ branches::\n \t'name' can be given to these commands in place of\n \t'repository' argument.  See the REMOTES section in\n \tlinkgit:git-fetch[1] for details.  This mechanism is legacy\n-\tand not likely to be found in modern repositories.\n+\tand not likely to be found in modern repositories. This\n+\tdirectory is ignored if $GIT_COMMON_DIR is set and\n+\t\"$GIT_COMMON_DIR/branches\" will be used instead.\n+\n \n hooks::\n \tHooks are customization scripts used by various Git\n@@ -149,7 +162,9 @@ hooks::\n \tdefault.  To enable, the `.sample` suffix has to be\n \tremoved from the filename by renaming.\n \tRead linkgit:githooks[5] for more details about\n-\teach hook.\n+\teach hook. This directory is ignored if $GIT_COMMON_DIR is set\n+\tand \"$GIT_COMMON_DIR/hooks\" will be used instead.\n+\n \n index::\n \tThe current index file for the repository.  It is\n@@ -161,7 +176,8 @@ sharedindex.<SHA-1>::\n \n info::\n \tAdditional information about the repository is recorded\n-\tin this directory.\n+\tin this directory. This directory is ignored if $GIT_COMMON_DIR\n+\tis set and \"$GIT_COMMON_DIR/index\" will be used instead.\n \n info/refs::\n \tThis file helps dumb transports discover what refs are\n@@ -201,12 +217,15 @@ remotes::\n \twhen interacting with remote repositories via 'git fetch',\n \t'git pull' and 'git push' commands.  See the REMOTES section\n \tin linkgit:git-fetch[1] for details.  This mechanism is legacy\n-\tand not likely to be found in modern repositories.\n+\tand not likely to be found in modern repositories. This\n+\tdirectory is ignored if $GIT_COMMON_DIR is set and\n+\t\"$GIT_COMMON_DIR/remotes\" will be used instead.\n \n logs::\n-\tRecords of changes made to refs are stored in this\n-\tdirectory.  See linkgit:git-update-ref[1]\n-\tfor more information.\n+\tRecords of changes made to refs are stored in this directory.\n+\tSee linkgit:git-update-ref[1] for more information. This\n+\tdirectory is ignored if $GIT_COMMON_DIR is set and\n+\t\"$GIT_COMMON_DIR/logs\" will be used instead.\n \n logs/refs/heads/`name`::\n \tRecords all changes made to the branch tip named `name`.\n@@ -217,10 +236,14 @@ logs/refs/tags/`name`::\n shallow::\n \tThis is similar to `info/grafts` but is internally used\n \tand maintained by shallow clone mechanism.  See `--depth`\n-\toption to linkgit:git-clone[1] and linkgit:git-fetch[1].\n+\toption to linkgit:git-clone[1] and linkgit:git-fetch[1]. This\n+\tfile is ignored if $GIT_COMMON_DIR is set and\n+\t\"$GIT_COMMON_DIR/shallow\" will be used instead.\n \n modules::\n-\tContains the git-repositories of the submodules.\n+\tContains the git-repositories of the submodules. This\n+\tdirectory is ignored if $GIT_COMMON_DIR is set and\n+\t\"$GIT_COMMON_DIR/modules\" will be used instead.\n \n SEE ALSO\n --------\ndiff --git a/cache.h b/cache.h\nindex 161dbd5..bf26301 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -377,6 +377,7 @@ static inline enum object_type object_type(unsigned int mode)\n \n /* Double-check local_repo_env below if you add to this list. */\n #define GIT_DIR_ENVIRONMENT \"GIT_DIR\"\n+#define GIT_COMMON_DIR_ENVIRONMENT \"GIT_COMMON_DIR\"\n #define GIT_NAMESPACE_ENVIRONMENT \"GIT_NAMESPACE\"\n #define GIT_WORK_TREE_ENVIRONMENT \"GIT_WORK_TREE\"\n #define GIT_PREFIX_ENVIRONMENT \"GIT_PREFIX\"\n@@ -430,6 +431,7 @@ extern int is_inside_git_dir(void);\n extern char *git_work_tree_cfg;\n extern int is_inside_work_tree(void);\n extern const char *get_git_dir(void);\n+extern const char *get_git_common_dir(void);\n extern int is_git_directory(const char *path);\n extern char *get_object_directory(void);\n extern char *get_index_file(void);\n@@ -635,7 +637,7 @@ extern int fsync_object_files;\n extern int core_preload_index;\n extern int core_apply_sparse_checkout;\n extern int precomposed_unicode;\n-extern int git_db_env, git_index_env, git_graft_env;\n+extern int git_db_env, git_index_env, git_graft_env, git_common_dir_env;\n \n /*\n  * The character that begins a commented line in user-editable file\ndiff --git a/environment.c b/environment.c\nindex fee12a6..78a07e4 100644\n--- a/environment.c\n+++ b/environment.c\n@@ -81,9 +81,9 @@ static char *work_tree;\n static const char *namespace;\n static size_t namespace_len;\n \n-static const char *git_dir;\n+static const char *git_dir, *git_common_dir;\n static char *git_object_dir, *git_index_file, *git_graft_file;\n-int git_db_env, git_index_env, git_graft_env;\n+int git_db_env, git_index_env, git_graft_env, git_common_dir_env;\n \n /*\n  * Repository-local GIT_* environment variables; see cache.h for details.\n@@ -125,8 +125,8 @@ static char *expand_namespace(const char *raw_namespace)\n \treturn strbuf_detach(&buf, NULL);\n }\n \n-static char *git_path_from_env(const char *envvar, const char *path,\n-\t\t\t       int *fromenv)\n+static char *git_path_from_env(const char *envvar, const char *git_dir,\n+\t\t\t       const char *path, int *fromenv)\n {\n \tconst char *value = getenv(envvar);\n \tif (!value) {\n@@ -149,9 +149,18 @@ static void setup_git_env(void)\n \t\tgit_dir = DEFAULT_GIT_DIR_ENVIRONMENT;\n \tgitfile = read_gitfile(git_dir);\n \tgit_dir = xstrdup(gitfile ? gitfile : git_dir);\n-\tgit_object_dir = git_path_from_env(DB_ENVIRONMENT, \"objects\", &git_db_env);\n-\tgit_index_file = git_path_from_env(INDEX_ENVIRONMENT, \"index\", &git_index_env);\n-\tgit_graft_file = git_path_from_env(GRAFT_ENVIRONMENT, \"info/grafts\", &git_graft_env);\n+\tgit_common_dir = getenv(GIT_COMMON_DIR_ENVIRONMENT);\n+\tif (git_common_dir) {\n+\t\tgit_common_dir_env = 1;\n+\t\tgit_common_dir = xstrdup(git_common_dir);\n+\t} else\n+\t\tgit_common_dir = git_dir;\n+\tgit_object_dir = git_path_from_env(DB_ENVIRONMENT, git_common_dir,\n+\t\t\t\t\t   \"objects\", &git_db_env);\n+\tgit_index_file = git_path_from_env(INDEX_ENVIRONMENT, git_dir,\n+\t\t\t\t\t   \"index\", &git_index_env);\n+\tgit_graft_file = git_path_from_env(GRAFT_ENVIRONMENT, git_common_dir,\n+\t\t\t\t\t   \"info/grafts\", &git_graft_env);\n \tif (getenv(NO_REPLACE_OBJECTS_ENVIRONMENT))\n \t\tcheck_replace_refs = 0;\n \tnamespace = expand_namespace(getenv(GIT_NAMESPACE_ENVIRONMENT));\n@@ -174,6 +183,11 @@ const char *get_git_dir(void)\n \treturn git_dir;\n }\n \n+const char *get_git_common_dir(void)\n+{\n+\treturn git_common_dir;\n+}\n+\n const char *get_git_namespace(void)\n {\n \tif (!namespace)\ndiff --git a/path.c b/path.c\nindex 4910783..94db501 100644\n--- a/path.c\n+++ b/path.c\n@@ -90,6 +90,38 @@ static void replace_dir(struct strbuf *buf, int len, const char *newdir)\n \t\tbuf->buf[newlen] = '/';\n }\n \n+static const char *common_list[] = {\n+\t\"/branches\", \"/hooks\", \"/info\", \"/logs\", \"/lost-found\", \"/modules\",\n+\t\"/objects\", \"/refs\", \"/remotes\", \"/rr-cache\", \"/svn\",\n+\t\"config\", \"gc.pid\", \"packed-refs\", \"shallow\",\n+\tNULL\n+};\n+\n+static void update_common_dir(struct strbuf *buf, int git_dir_len)\n+{\n+\tchar *base = buf->buf + git_dir_len;\n+\tconst char **p;\n+\n+\tif (is_dir_file(base, \"logs\", \"HEAD\"))\n+\t\treturn;\t/* keep this in $GIT_DIR */\n+\tfor (p = common_list; *p; p++) {\n+\t\tconst char *path = *p;\n+\t\tint is_dir = 0;\n+\t\tif (*path == '/') {\n+\t\t\tpath++;\n+\t\t\tis_dir = 1;\n+\t\t}\n+\t\tif (is_dir && dir_prefix(base, path)) {\n+\t\t\treplace_dir(buf, git_dir_len, get_git_common_dir());\n+\t\t\treturn;\n+\t\t}\n+\t\tif (!is_dir && !strcmp(base, path)) {\n+\t\t\treplace_dir(buf, git_dir_len, get_git_common_dir());\n+\t\t\treturn;\n+\t\t}\n+\t}\n+}\n+\n static void adjust_git_path(struct strbuf *buf, int git_dir_len)\n {\n \tconst char *base = buf->buf + git_dir_len;\n@@ -101,6 +133,8 @@ static void adjust_git_path(struct strbuf *buf, int git_dir_len)\n \t\t\t      get_index_file(), strlen(get_index_file()));\n \telse if (git_db_env && dir_prefix(base, \"objects\"))\n \t\treplace_dir(buf, git_dir_len + 7, get_object_directory());\n+\telse if (git_common_dir_env)\n+\t\tupdate_common_dir(buf, git_dir_len);\n }\n \n static void do_git_path(struct strbuf *buf, const char *fmt, va_list args)\ndiff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh\nindex 33d2818..f5d6f80 100755\n--- a/t/t0060-path-utils.sh\n+++ b/t/t0060-path-utils.sh\n@@ -262,5 +262,21 @@ test_expect_success 'setup fake objects directory foo' 'mkdir foo'\n test_git_path GIT_OBJECT_DIRECTORY=foo objects foo\n test_git_path GIT_OBJECT_DIRECTORY=foo objects/foo foo/foo\n test_git_path GIT_OBJECT_DIRECTORY=foo objects2 .git/objects2\n+test_expect_success 'setup common repository' 'git --git-dir=bar init'\n+test_git_path GIT_COMMON_DIR=bar index                    .git/index\n+test_git_path GIT_COMMON_DIR=bar HEAD                     .git/HEAD\n+test_git_path GIT_COMMON_DIR=bar logs/HEAD                .git/logs/HEAD\n+test_git_path GIT_COMMON_DIR=bar objects                  bar/objects\n+test_git_path GIT_COMMON_DIR=bar objects/bar              bar/objects/bar\n+test_git_path GIT_COMMON_DIR=bar info/exclude             bar/info/exclude\n+test_git_path GIT_COMMON_DIR=bar info/grafts              bar/info/grafts\n+test_git_path GIT_COMMON_DIR=bar remotes/bar              bar/remotes/bar\n+test_git_path GIT_COMMON_DIR=bar branches/bar             bar/branches/bar\n+test_git_path GIT_COMMON_DIR=bar logs/refs/heads/master   bar/logs/refs/heads/master\n+test_git_path GIT_COMMON_DIR=bar refs/heads/master        bar/refs/heads/master\n+test_git_path GIT_COMMON_DIR=bar hooks/me                 bar/hooks/me\n+test_git_path GIT_COMMON_DIR=bar config                   bar/config\n+test_git_path GIT_COMMON_DIR=bar packed-refs              bar/packed-refs\n+test_git_path GIT_COMMON_DIR=bar shallow                  bar/shallow\n \n test_done\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249939","messageId":"1411867366-3821-13-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1411867366-3821-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v3 12/32] git-sh-setup.sh: use rev-parse --git-path to get $GIT_DIR/objects","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-28T01:22:26Z","receivedAt":"2014-09-28T01:22:26Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"If $GIT_COMMON_DIR is set, $GIT_OBJECT_DIRECTORY should be\n$GIT_COMMON_DIR/objects, not $GIT_DIR/objects. Just let rev-parse\n--git-path handle it.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n git-sh-setup.sh | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/git-sh-setup.sh b/git-sh-setup.sh\nindex 9447980..d3dbb2f 100644\n--- a/git-sh-setup.sh\n+++ b/git-sh-setup.sh\n@@ -345,7 +345,7 @@ then\n \t\techo >&2 \"Unable to determine absolute path of git directory\"\n \t\texit 1\n \t}\n-\t: ${GIT_OBJECT_DIRECTORY=\"$GIT_DIR/objects\"}\n+\t: ${GIT_OBJECT_DIRECTORY=\"$(git rev-parse --git-path objects)\"}\n fi\n \n peel_committish () {\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249940","messageId":"1411867366-3821-14-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1411867366-3821-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v3 13/32] *.sh: avoid hardcoding $GIT_DIR/hooks/...","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-28T01:22:27Z","receivedAt":"2014-09-28T01:22:27Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"If $GIT_COMMON_DIR is set, it should be $GIT_COMMON_DIR/hooks/, not\n$GIT_DIR/hooks/. Just let rev-parse --git-path handle it.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n git-am.sh                              | 22 +++++++++++-----------\n git-rebase--interactive.sh             |  6 +++---\n git-rebase--merge.sh                   |  6 ++----\n git-rebase.sh                          |  4 ++--\n templates/hooks--applypatch-msg.sample |  4 ++--\n templates/hooks--pre-applypatch.sample |  4 ++--\n 6 files changed, 22 insertions(+), 24 deletions(-)\n\ndiff --git a/git-am.sh b/git-am.sh\nindex ee61a77..66803d1 100755\n--- a/git-am.sh\n+++ b/git-am.sh\n@@ -810,10 +810,10 @@ To restore the original branch and stop patching run \\\"\\$cmdline --abort\\\".\"\n \t\tcontinue\n \tfi\n \n-\tif test -x \"$GIT_DIR\"/hooks/applypatch-msg\n+\thook=\"$(git rev-parse --git-path hooks/applypatch-msg)\"\n+\tif test -x \"$hook\"\n \tthen\n-\t\t\"$GIT_DIR\"/hooks/applypatch-msg \"$dotest/final-commit\" ||\n-\t\tstop_here $this\n+\t\t\"$hook\" \"$dotest/final-commit\" || stop_here $this\n \tfi\n \n \tif test -f \"$dotest/final-commit\"\n@@ -887,9 +887,10 @@ did you forget to use 'git add'?\"\n \t\tstop_here_user_resolve $this\n \tfi\n \n-\tif test -x \"$GIT_DIR\"/hooks/pre-applypatch\n+\thook=\"$(git rev-parse --git-path hooks/pre-applypatch)\"\n+\tif test -x \"$hook\"\n \tthen\n-\t\t\"$GIT_DIR\"/hooks/pre-applypatch || stop_here $this\n+\t\t\"$hook\" || stop_here $this\n \tfi\n \n \ttree=$(git write-tree) &&\n@@ -916,18 +917,17 @@ did you forget to use 'git add'?\"\n \t\techo \"$(cat \"$dotest/original-commit\") $commit\" >> \"$dotest/rewritten\"\n \tfi\n \n-\tif test -x \"$GIT_DIR\"/hooks/post-applypatch\n-\tthen\n-\t\t\"$GIT_DIR\"/hooks/post-applypatch\n-\tfi\n+\thook=\"$(git rev-parse --git-path hooks/post-applypatch)\"\n+\ttest -x \"$hook\" && \"$hook\"\n \n \tgo_next\n done\n \n if test -s \"$dotest\"/rewritten; then\n     git notes copy --for-rewrite=rebase < \"$dotest\"/rewritten\n-    if test -x \"$GIT_DIR\"/hooks/post-rewrite; then\n-\t\"$GIT_DIR\"/hooks/post-rewrite rebase < \"$dotest\"/rewritten\n+    hook=\"$(git rev-parse --git-path hooks/post-rewrite)\"\n+    if test -x \"$hook\"; then\n+\t\"$hook\" rebase < \"$dotest\"/rewritten\n     fi\n fi\n \ndiff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh\nindex b64dd28..b32f797 100644\n--- a/git-rebase--interactive.sh\n+++ b/git-rebase--interactive.sh\n@@ -642,9 +642,9 @@ do_next () {\n \t\tgit notes copy --for-rewrite=rebase < \"$rewritten_list\" ||\n \t\ttrue # we don't care if this copying failed\n \t} &&\n-\tif test -x \"$GIT_DIR\"/hooks/post-rewrite &&\n-\t\ttest -s \"$rewritten_list\"; then\n-\t\t\"$GIT_DIR\"/hooks/post-rewrite rebase < \"$rewritten_list\"\n+\thook=\"$(git rev-parse --git-path hooks/post-rewrite)\"\n+\tif test -x \"$hook\" && test -s \"$rewritten_list\"; then\n+\t\t\"$hook\" rebase < \"$rewritten_list\"\n \t\ttrue # we don't care if this hook failed\n \tfi &&\n \twarn \"Successfully rebased and updated $head_name.\"\ndiff --git a/git-rebase--merge.sh b/git-rebase--merge.sh\nindex d3fb67d..2cc2a6d 100644\n--- a/git-rebase--merge.sh\n+++ b/git-rebase--merge.sh\n@@ -94,10 +94,8 @@ finish_rb_merge () {\n \tif test -s \"$state_dir\"/rewritten\n \tthen\n \t\tgit notes copy --for-rewrite=rebase <\"$state_dir\"/rewritten\n-\t\tif test -x \"$GIT_DIR\"/hooks/post-rewrite\n-\t\tthen\n-\t\t\t\"$GIT_DIR\"/hooks/post-rewrite rebase <\"$state_dir\"/rewritten\n-\t\tfi\n+\t\thook=\"$(git rev-parse --git-path hooks/post-rewrite)\"\n+\t\ttest -x \"$hook\" && \"$hook\" rebase <\"$state_dir\"/rewritten\n \tfi\n \tsay All done.\n }\ndiff --git a/git-rebase.sh b/git-rebase.sh\nindex 55da9db..fb935a0 100755\n--- a/git-rebase.sh\n+++ b/git-rebase.sh\n@@ -202,9 +202,9 @@ run_specific_rebase () {\n \n run_pre_rebase_hook () {\n \tif test -z \"$ok_to_skip_pre_rebase\" &&\n-\t   test -x \"$GIT_DIR/hooks/pre-rebase\"\n+\t   test -x \"$(git rev-parse --git-path hooks/pre-rebase)\"\n \tthen\n-\t\t\"$GIT_DIR/hooks/pre-rebase\" ${1+\"$@\"} ||\n+\t\t\"$(git rev-parse --git-path hooks/pre-rebase)\" ${1+\"$@\"} ||\n \t\tdie \"$(gettext \"The pre-rebase hook refused to rebase.\")\"\n \tfi\n }\ndiff --git a/templates/hooks--applypatch-msg.sample b/templates/hooks--applypatch-msg.sample\nindex 8b2a2fe..a5d7b84 100755\n--- a/templates/hooks--applypatch-msg.sample\n+++ b/templates/hooks--applypatch-msg.sample\n@@ -10,6 +10,6 @@\n # To enable this hook, rename this file to \"applypatch-msg\".\n \n . git-sh-setup\n-test -x \"$GIT_DIR/hooks/commit-msg\" &&\n-\texec \"$GIT_DIR/hooks/commit-msg\" ${1+\"$@\"}\n+commitmsg=\"$(git rev-parse --git-path hooks/commit-msg)\"\n+test -x \"$commitmsg\" && exec \"$commitmsg\" ${1+\"$@\"}\n :\ndiff --git a/templates/hooks--pre-applypatch.sample b/templates/hooks--pre-applypatch.sample\nindex b1f187c..4142082 100755\n--- a/templates/hooks--pre-applypatch.sample\n+++ b/templates/hooks--pre-applypatch.sample\n@@ -9,6 +9,6 @@\n # To enable this hook, rename this file to \"pre-applypatch\".\n \n . git-sh-setup\n-test -x \"$GIT_DIR/hooks/pre-commit\" &&\n-\texec \"$GIT_DIR/hooks/pre-commit\" ${1+\"$@\"}\n+precommit=\"$(git rev-parse --git-path hooks/pre-commit)\"\n+test -x \"$precommit\" && exec \"$precommit\" ${1+\"$@\"}\n :\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249941","messageId":"1411867366-3821-15-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1411867366-3821-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v3 14/32] git-stash: avoid hardcoding $GIT_DIR/logs/....","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-28T01:22:28Z","receivedAt":"2014-09-28T01:22:28Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n git-stash.sh | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/git-stash.sh b/git-stash.sh\nindex 4294aa0..2784f8b 100755\n--- a/git-stash.sh\n+++ b/git-stash.sh\n@@ -184,7 +184,7 @@ store_stash () {\n \tfi\n \n \t# Make sure the reflog for stash is kept.\n-\t: >>\"$GIT_DIR/logs/$ref_stash\"\n+\t: >>\"$(git rev-parse --git-path logs/$ref_stash)\"\n \tgit update-ref -m \"$stash_msg\" $ref_stash $w_commit\n \tret=$?\n \ttest $ret != 0 && test -z $quiet &&\n@@ -259,7 +259,7 @@ save_stash () {\n \t\tsay \"$(gettext \"No local changes to save\")\"\n \t\texit 0\n \tfi\n-\ttest -f \"$GIT_DIR/logs/$ref_stash\" ||\n+\ttest -f \"$(git rev-parse --git-path logs/$ref_stash)\" ||\n \t\tclear_stash || die \"$(gettext \"Cannot initialize stash\")\"\n \n \tcreate_stash \"$stash_msg\" $untracked\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249942","messageId":"1411867366-3821-16-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1411867366-3821-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v3 15/32] setup.c: convert is_git_directory() to use strbuf","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-28T01:22:29Z","receivedAt":"2014-09-28T01:22:29Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n setup.c | 37 +++++++++++++++++++++----------------\n 1 file changed, 21 insertions(+), 16 deletions(-)\n\ndiff --git a/setup.c b/setup.c\nindex 979b13f..afd6742 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -238,31 +238,36 @@ void verify_non_filename(const char *prefix, const char *arg)\n  */\n int is_git_directory(const char *suspect)\n {\n-\tchar path[PATH_MAX];\n-\tsize_t len = strlen(suspect);\n+\tstruct strbuf path = STRBUF_INIT;\n+\tint ret = 0;\n+\tsize_t len;\n \n-\tif (PATH_MAX <= len + strlen(\"/objects\"))\n-\t\tdie(\"Too long path: %.*s\", 60, suspect);\n-\tstrcpy(path, suspect);\n+\tstrbuf_addstr(&path, suspect);\n+\tlen = path.len;\n \tif (getenv(DB_ENVIRONMENT)) {\n \t\tif (access(getenv(DB_ENVIRONMENT), X_OK))\n-\t\t\treturn 0;\n+\t\t\tgoto done;\n \t}\n \telse {\n-\t\tstrcpy(path + len, \"/objects\");\n-\t\tif (access(path, X_OK))\n-\t\t\treturn 0;\n+\t\tstrbuf_addstr(&path, \"/objects\");\n+\t\tif (access(path.buf, X_OK))\n+\t\t\tgoto done;\n \t}\n \n-\tstrcpy(path + len, \"/refs\");\n-\tif (access(path, X_OK))\n-\t\treturn 0;\n+\tstrbuf_setlen(&path, len);\n+\tstrbuf_addstr(&path, \"/refs\");\n+\tif (access(path.buf, X_OK))\n+\t\tgoto done;\n \n-\tstrcpy(path + len, \"/HEAD\");\n-\tif (validate_headref(path))\n-\t\treturn 0;\n+\tstrbuf_setlen(&path, len);\n+\tstrbuf_addstr(&path, \"/HEAD\");\n+\tif (validate_headref(path.buf))\n+\t\tgoto done;\n \n-\treturn 1;\n+\tret = 1;\n+done:\n+\tstrbuf_release(&path);\n+\treturn ret;\n }\n \n int is_inside_git_dir(void)\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249943","messageId":"1411867366-3821-17-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1411867366-3821-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v3 16/32] setup.c: detect $GIT_COMMON_DIR in is_git_directory()","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-28T01:22:30Z","receivedAt":"2014-09-28T01:22:30Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"If the file \"$GIT_DIR/commondir\" exists, it contains the value of\n$GIT_COMMON_DIR.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n Documentation/gitrepository-layout.txt |  7 ++++++\n setup.c                                | 43 +++++++++++++++++++++++++++++-----\n 2 files changed, 44 insertions(+), 6 deletions(-)\n\ndiff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt\nindex 2b5966a..2dc5667 100644\n--- a/Documentation/gitrepository-layout.txt\n+++ b/Documentation/gitrepository-layout.txt\n@@ -240,6 +240,13 @@ shallow::\n \tfile is ignored if $GIT_COMMON_DIR is set and\n \t\"$GIT_COMMON_DIR/shallow\" will be used instead.\n \n+commondir::\n+\tIf this file exists, $GIT_COMMON_DIR (see linkgit:git[1]) will\n+\tbe set to the path specified in this file if it is not\n+\texplicitly set. If the specified path is relative, it is\n+\trelative to $GIT_DIR. The repository with commondir is\n+\tincomplete without the repository pointed by \"commondir\".\n+\n modules::\n \tContains the git-repositories of the submodules. This\n \tdirectory is ignored if $GIT_COMMON_DIR is set and\ndiff --git a/setup.c b/setup.c\nindex afd6742..6c52f75 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -224,6 +224,33 @@ void verify_non_filename(const char *prefix, const char *arg)\n \t    \"'git <command> [<revision>...] -- [<file>...]'\", arg);\n }\n \n+static void get_common_dir(struct strbuf *sb, const char *gitdir)\n+{\n+\tstruct strbuf data = STRBUF_INIT;\n+\tstruct strbuf path = STRBUF_INIT;\n+\tconst char *git_common_dir = getenv(GIT_COMMON_DIR_ENVIRONMENT);\n+\tif (git_common_dir) {\n+\t\tstrbuf_addstr(sb, git_common_dir);\n+\t\treturn;\n+\t}\n+\tstrbuf_addf(&path, \"%s/commondir\", gitdir);\n+\tif (file_exists(path.buf)) {\n+\t\tif (strbuf_read_file(&data, path.buf, 0) <= 0)\n+\t\t\tdie_errno(_(\"failed to read %s\"), path.buf);\n+\t\twhile (data.len && (data.buf[data.len - 1] == '\\n' ||\n+\t\t\t\t    data.buf[data.len - 1] == '\\r'))\n+\t\t\tdata.len--;\n+\t\tdata.buf[data.len] = '\\0';\n+\t\tstrbuf_reset(&path);\n+\t\tif (!is_absolute_path(data.buf))\n+\t\t\tstrbuf_addf(&path, \"%s/\", gitdir);\n+\t\tstrbuf_addbuf(&path, &data);\n+\t\tstrbuf_addstr(sb, real_path(path.buf));\n+\t} else\n+\t\tstrbuf_addstr(sb, gitdir);\n+\tstrbuf_release(&data);\n+\tstrbuf_release(&path);\n+}\n \n /*\n  * Test if it looks like we're at a git directory.\n@@ -242,13 +269,22 @@ int is_git_directory(const char *suspect)\n \tint ret = 0;\n \tsize_t len;\n \n-\tstrbuf_addstr(&path, suspect);\n+\t/* Check worktree-related signatures */\n+\tstrbuf_addf(&path, \"%s/HEAD\", suspect);\n+\tif (validate_headref(path.buf))\n+\t\tgoto done;\n+\n+\tstrbuf_reset(&path);\n+\tget_common_dir(&path, suspect);\n \tlen = path.len;\n+\n+\t/* Check non-worktree-related signatures */\n \tif (getenv(DB_ENVIRONMENT)) {\n \t\tif (access(getenv(DB_ENVIRONMENT), X_OK))\n \t\t\tgoto done;\n \t}\n \telse {\n+\t\tstrbuf_setlen(&path, len);\n \t\tstrbuf_addstr(&path, \"/objects\");\n \t\tif (access(path.buf, X_OK))\n \t\t\tgoto done;\n@@ -259,11 +295,6 @@ int is_git_directory(const char *suspect)\n \tif (access(path.buf, X_OK))\n \t\tgoto done;\n \n-\tstrbuf_setlen(&path, len);\n-\tstrbuf_addstr(&path, \"/HEAD\");\n-\tif (validate_headref(path.buf))\n-\t\tgoto done;\n-\n \tret = 1;\n done:\n \tstrbuf_release(&path);\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249944","messageId":"1411867366-3821-18-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1411867366-3821-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v3 17/32] setup.c: convert check_repository_format_gently to use strbuf","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-28T01:22:31Z","receivedAt":"2014-09-28T01:22:31Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n setup.c | 12 ++++++++----\n 1 file changed, 8 insertions(+), 4 deletions(-)\n\ndiff --git a/setup.c b/setup.c\nindex 6c52f75..00a23e6 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -342,7 +342,9 @@ void setup_work_tree(void)\n \n static int check_repository_format_gently(const char *gitdir, int *nongit_ok)\n {\n-\tchar repo_config[PATH_MAX+1];\n+\tstruct strbuf sb = STRBUF_INIT;\n+\tconst char *repo_config;\n+\tint ret = 0;\n \n \t/*\n \t * git_config() can't be used here because it calls git_pathdup()\n@@ -353,7 +355,8 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)\n \t * Use a gentler version of git_config() to check if this repo\n \t * is a good one.\n \t */\n-\tsnprintf(repo_config, PATH_MAX, \"%s/config\", gitdir);\n+\tstrbuf_addf(&sb, \"%s/config\", gitdir);\n+\trepo_config = sb.buf;\n \tgit_config_early(check_repository_format_version, NULL, repo_config);\n \tif (GIT_REPO_VERSION < repository_format_version) {\n \t\tif (!nongit_ok)\n@@ -363,9 +366,10 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)\n \t\t\tGIT_REPO_VERSION, repository_format_version);\n \t\twarning(\"Please upgrade Git\");\n \t\t*nongit_ok = -1;\n-\t\treturn -1;\n+\t\tret = -1;\n \t}\n-\treturn 0;\n+\tstrbuf_release(&sb);\n+\treturn ret;\n }\n \n /*\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249945","messageId":"1411867366-3821-19-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1411867366-3821-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v3 18/32] setup.c: detect $GIT_COMMON_DIR check_repository_format_gently()","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-28T01:22:32Z","receivedAt":"2014-09-28T01:22:32Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n setup.c | 6 ++++--\n 1 file changed, 4 insertions(+), 2 deletions(-)\n\ndiff --git a/setup.c b/setup.c\nindex 00a23e6..1d4f1aa 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -346,6 +346,10 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)\n \tconst char *repo_config;\n \tint ret = 0;\n \n+\tget_common_dir(&sb, gitdir);\n+\tstrbuf_addstr(&sb, \"/config\");\n+\trepo_config = sb.buf;\n+\n \t/*\n \t * git_config() can't be used here because it calls git_pathdup()\n \t * to get $GIT_CONFIG/config. That call will make setup_git_env()\n@@ -355,8 +359,6 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)\n \t * Use a gentler version of git_config() to check if this repo\n \t * is a good one.\n \t */\n-\tstrbuf_addf(&sb, \"%s/config\", gitdir);\n-\trepo_config = sb.buf;\n \tgit_config_early(check_repository_format_version, NULL, repo_config);\n \tif (GIT_REPO_VERSION < repository_format_version) {\n \t\tif (!nongit_ok)\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249946","messageId":"1411867366-3821-20-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1411867366-3821-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v3 19/32] setup.c: support multi-checkout repo setup","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-28T01:22:33Z","receivedAt":"2014-09-28T01:22:33Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"The repo setup procedure is updated to detect $GIT_DIR/commondir and\nset $GIT_COMMON_DIR properly.\n\nThe core.worktree is ignored when $GIT_COMMON_DIR is set. This is\nbecause the config file is shared in multi-checkout setup, but\ncheckout directories _are_ different. Making core.worktree effective\nin all checkouts mean it's back to a single checkout.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n Documentation/config.txt        |  2 ++\n Documentation/git-rev-parse.txt |  3 ++\n builtin/rev-parse.c             |  4 +++\n cache.h                         |  1 +\n environment.c                   |  8 ++---\n setup.c                         | 33 +++++++++++++-----\n t/t1501-worktree.sh             | 76 +++++++++++++++++++++++++++++++++++++++++\n t/t1510-repo-setup.sh           |  1 +\n trace.c                         |  1 +\n 9 files changed, 115 insertions(+), 14 deletions(-)\n\ndiff --git a/Documentation/config.txt b/Documentation/config.txt\nindex 3b5b24a..98b8ef0 100644\n--- a/Documentation/config.txt\n+++ b/Documentation/config.txt\n@@ -380,6 +380,8 @@ false), while all other repositories are assumed to be bare (bare\n \n core.worktree::\n \tSet the path to the root of the working tree.\n+\tIf GIT_COMMON_DIR environment variable is set, core.worktree\n+\tis ignored and not used for determining the root of working tree.\n \tThis can be overridden by the GIT_WORK_TREE environment\n \tvariable and the '--work-tree' command-line option.\n \tThe value can be an absolute path or relative to the path to\ndiff --git a/Documentation/git-rev-parse.txt b/Documentation/git-rev-parse.txt\nindex 0fed5a4..c10cc62 100644\n--- a/Documentation/git-rev-parse.txt\n+++ b/Documentation/git-rev-parse.txt\n@@ -215,6 +215,9 @@ If `$GIT_DIR` is not defined and the current directory\n is not detected to lie in a Git repository or work tree\n print a message to stderr and exit with nonzero status.\n \n+--git-common-dir::\n+\tShow `$GIT_COMMON_DIR` if defined, else `$GIT_DIR`.\n+\n --is-inside-git-dir::\n \tWhen the current working directory is below the repository\n \tdirectory print \"true\", otherwise \"false\".\ndiff --git a/builtin/rev-parse.c b/builtin/rev-parse.c\nindex a6f4697..1ce73e8 100644\n--- a/builtin/rev-parse.c\n+++ b/builtin/rev-parse.c\n@@ -759,6 +759,10 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)\n \t\t\t\tfree(cwd);\n \t\t\t\tcontinue;\n \t\t\t}\n+\t\t\tif (!strcmp(arg, \"--git-common-dir\")) {\n+\t\t\t\tputs(get_git_common_dir());\n+\t\t\t\tcontinue;\n+\t\t\t}\n \t\t\tif (!strcmp(arg, \"--resolve-git-dir\")) {\n \t\t\t\tconst char *gitdir = argv[++i];\n \t\t\t\tif (!gitdir)\ndiff --git a/cache.h b/cache.h\nindex bf26301..1784fc6 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -437,6 +437,7 @@ extern char *get_object_directory(void);\n extern char *get_index_file(void);\n extern char *get_graft_file(void);\n extern int set_git_dir(const char *path);\n+extern int get_common_dir(struct strbuf *sb, const char *gitdir);\n extern const char *get_git_namespace(void);\n extern const char *strip_namespace(const char *namespaced_ref);\n extern const char *get_git_work_tree(void);\ndiff --git a/environment.c b/environment.c\nindex 78a07e4..d5b0788 100644\n--- a/environment.c\n+++ b/environment.c\n@@ -141,6 +141,7 @@ static char *git_path_from_env(const char *envvar, const char *git_dir,\n \n static void setup_git_env(void)\n {\n+\tstruct strbuf sb = STRBUF_INIT;\n \tconst char *gitfile;\n \tconst char *shallow_file;\n \n@@ -149,12 +150,9 @@ static void setup_git_env(void)\n \t\tgit_dir = DEFAULT_GIT_DIR_ENVIRONMENT;\n \tgitfile = read_gitfile(git_dir);\n \tgit_dir = xstrdup(gitfile ? gitfile : git_dir);\n-\tgit_common_dir = getenv(GIT_COMMON_DIR_ENVIRONMENT);\n-\tif (git_common_dir) {\n+\tif (get_common_dir(&sb, git_dir))\n \t\tgit_common_dir_env = 1;\n-\t\tgit_common_dir = xstrdup(git_common_dir);\n-\t} else\n-\t\tgit_common_dir = git_dir;\n+\tgit_common_dir = strbuf_detach(&sb, NULL);\n \tgit_object_dir = git_path_from_env(DB_ENVIRONMENT, git_common_dir,\n \t\t\t\t\t   \"objects\", &git_db_env);\n \tgit_index_file = git_path_from_env(INDEX_ENVIRONMENT, git_dir,\ndiff --git a/setup.c b/setup.c\nindex 1d4f1aa..b99bdd3 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -224,14 +224,15 @@ void verify_non_filename(const char *prefix, const char *arg)\n \t    \"'git <command> [<revision>...] -- [<file>...]'\", arg);\n }\n \n-static void get_common_dir(struct strbuf *sb, const char *gitdir)\n+int get_common_dir(struct strbuf *sb, const char *gitdir)\n {\n \tstruct strbuf data = STRBUF_INIT;\n \tstruct strbuf path = STRBUF_INIT;\n \tconst char *git_common_dir = getenv(GIT_COMMON_DIR_ENVIRONMENT);\n+\tint ret = 0;\n \tif (git_common_dir) {\n \t\tstrbuf_addstr(sb, git_common_dir);\n-\t\treturn;\n+\t\treturn 1;\n \t}\n \tstrbuf_addf(&path, \"%s/commondir\", gitdir);\n \tif (file_exists(path.buf)) {\n@@ -246,10 +247,12 @@ static void get_common_dir(struct strbuf *sb, const char *gitdir)\n \t\t\tstrbuf_addf(&path, \"%s/\", gitdir);\n \t\tstrbuf_addbuf(&path, &data);\n \t\tstrbuf_addstr(sb, real_path(path.buf));\n+\t\tret = 1;\n \t} else\n \t\tstrbuf_addstr(sb, gitdir);\n \tstrbuf_release(&data);\n \tstrbuf_release(&path);\n+\treturn ret;\n }\n \n /*\n@@ -340,13 +343,26 @@ void setup_work_tree(void)\n \tinitialized = 1;\n }\n \n+static int check_repo_format(const char *var, const char *value, void *cb)\n+{\n+\tif (strcmp(var, \"core.repositoryformatversion\") == 0)\n+\t\trepository_format_version = git_config_int(var, value);\n+\telse if (strcmp(var, \"core.sharedrepository\") == 0)\n+\t\tshared_repository = git_config_perm(var, value);\n+\treturn 0;\n+}\n+\n static int check_repository_format_gently(const char *gitdir, int *nongit_ok)\n {\n \tstruct strbuf sb = STRBUF_INIT;\n \tconst char *repo_config;\n+\tconfig_fn_t fn;\n \tint ret = 0;\n \n-\tget_common_dir(&sb, gitdir);\n+\tif (get_common_dir(&sb, gitdir))\n+\t\tfn = check_repo_format;\n+\telse\n+\t\tfn = check_repository_format_version;\n \tstrbuf_addstr(&sb, \"/config\");\n \trepo_config = sb.buf;\n \n@@ -359,7 +375,7 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)\n \t * Use a gentler version of git_config() to check if this repo\n \t * is a good one.\n \t */\n-\tgit_config_early(check_repository_format_version, NULL, repo_config);\n+\tgit_config_early(fn, NULL, repo_config);\n \tif (GIT_REPO_VERSION < repository_format_version) {\n \t\tif (!nongit_ok)\n \t\t\tdie (\"Expected git repo version <= %d, found %d\",\n@@ -841,11 +857,10 @@ int git_config_perm(const char *var, const char *value)\n \n int check_repository_format_version(const char *var, const char *value, void *cb)\n {\n-\tif (strcmp(var, \"core.repositoryformatversion\") == 0)\n-\t\trepository_format_version = git_config_int(var, value);\n-\telse if (strcmp(var, \"core.sharedrepository\") == 0)\n-\t\tshared_repository = git_config_perm(var, value);\n-\telse if (strcmp(var, \"core.bare\") == 0) {\n+\tint ret = check_repo_format(var, value, cb);\n+\tif (ret)\n+\t\treturn ret;\n+\tif (strcmp(var, \"core.bare\") == 0) {\n \t\tis_bare_repository_cfg = git_config_bool(var, value);\n \t\tif (is_bare_repository_cfg == 1)\n \t\t\tinside_work_tree = -1;\ndiff --git a/t/t1501-worktree.sh b/t/t1501-worktree.sh\nindex 8f36aa9..e6ac7a4 100755\n--- a/t/t1501-worktree.sh\n+++ b/t/t1501-worktree.sh\n@@ -346,4 +346,80 @@ test_expect_success 'relative $GIT_WORK_TREE and git subprocesses' '\n \ttest_cmp expected actual\n '\n \n+test_expect_success 'Multi-worktree setup' '\n+\tmkdir work &&\n+\tmkdir -p repo.git/repos/foo &&\n+\tcp repo.git/HEAD repo.git/index repo.git/repos/foo &&\n+\tsane_unset GIT_DIR GIT_CONFIG GIT_WORK_TREE\n+'\n+\n+test_expect_success 'GIT_DIR set (1)' '\n+\techo \"gitdir: repo.git/repos/foo\" >gitfile &&\n+\techo ../.. >repo.git/repos/foo/commondir &&\n+\t(\n+\t\tcd work &&\n+\t\tGIT_DIR=../gitfile git rev-parse --git-common-dir >actual &&\n+\t\ttest-path-utils real_path \"$TRASH_DIRECTORY/repo.git\" >expect &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'GIT_DIR set (2)' '\n+\techo \"gitdir: repo.git/repos/foo\" >gitfile &&\n+\techo \"$TRASH_DIRECTORY/repo.git\" >repo.git/repos/foo/commondir &&\n+\t(\n+\t\tcd work &&\n+\t\tGIT_DIR=../gitfile git rev-parse --git-common-dir >actual &&\n+\t\ttest-path-utils real_path \"$TRASH_DIRECTORY/repo.git\" >expect &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'Auto discovery' '\n+\techo \"gitdir: repo.git/repos/foo\" >.git &&\n+\techo ../.. >repo.git/repos/foo/commondir &&\n+\t(\n+\t\tcd work &&\n+\t\tgit rev-parse --git-common-dir >actual &&\n+\t\ttest-path-utils real_path \"$TRASH_DIRECTORY/repo.git\" >expect &&\n+\t\ttest_cmp expect actual &&\n+\t\techo haha >data1 &&\n+\t\tgit add data1 &&\n+\t\tgit ls-files --full-name :/ | grep data1 >actual &&\n+\t\techo work/data1 >expect &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success '$GIT_DIR/common overrides core.worktree' '\n+\tmkdir elsewhere &&\n+\tgit --git-dir=repo.git config core.worktree \"$TRASH_DIRECTORY/elsewhere\" &&\n+\techo \"gitdir: repo.git/repos/foo\" >.git &&\n+\techo ../.. >repo.git/repos/foo/commondir &&\n+\t(\n+\t\tcd work &&\n+\t\tgit rev-parse --git-common-dir >actual &&\n+\t\ttest-path-utils real_path \"$TRASH_DIRECTORY/repo.git\" >expect &&\n+\t\ttest_cmp expect actual &&\n+\t\techo haha >data2 &&\n+\t\tgit add data2 &&\n+\t\tgit ls-files --full-name :/ | grep data2 >actual &&\n+\t\techo work/data2 >expect &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success '$GIT_WORK_TREE overrides $GIT_DIR/common' '\n+\techo \"gitdir: repo.git/repos/foo\" >.git &&\n+\techo ../.. >repo.git/repos/foo/commondir &&\n+\t(\n+\t\tcd work &&\n+\t\techo haha >data3 &&\n+\t\tgit --git-dir=../.git --work-tree=. add data3 &&\n+\t\tgit ls-files --full-name -- :/ | grep data3 >actual &&\n+\t\techo data3 >expect &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n test_done\ndiff --git a/t/t1510-repo-setup.sh b/t/t1510-repo-setup.sh\nindex e1b2a99..33c1a58 100755\n--- a/t/t1510-repo-setup.sh\n+++ b/t/t1510-repo-setup.sh\n@@ -106,6 +106,7 @@ setup_env () {\n expect () {\n \tcat >\"$1/expected\" <<-EOF\n \tsetup: git_dir: $2\n+\tsetup: git_common_dir: $2\n \tsetup: worktree: $3\n \tsetup: cwd: $4\n \tsetup: prefix: $5\ndiff --git a/trace.c b/trace.c\nindex b6f25a2..c073a7d 100644\n--- a/trace.c\n+++ b/trace.c\n@@ -312,6 +312,7 @@ void trace_repo_setup(const char *prefix)\n \t\tprefix = \"(null)\";\n \n \ttrace_printf_key(&key, \"setup: git_dir: %s\\n\", quote_crnl(get_git_dir()));\n+\ttrace_printf_key(&key, \"setup: git_common_dir: %s\\n\", quote_crnl(get_git_common_dir()));\n \ttrace_printf_key(&key, \"setup: worktree: %s\\n\", quote_crnl(git_work_tree));\n \ttrace_printf_key(&key, \"setup: cwd: %s\\n\", quote_crnl(cwd));\n \ttrace_printf_key(&key, \"setup: prefix: %s\\n\", quote_crnl(prefix));\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249947","messageId":"1411867366-3821-21-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1411867366-3821-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v3 20/32] wrapper.c: wrapper to open a file, fprintf then close","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-28T01:22:34Z","receivedAt":"2014-09-28T01:22:34Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n cache.h   |  2 ++\n wrapper.c | 31 +++++++++++++++++++++++++++++++\n 2 files changed, 33 insertions(+)\n\ndiff --git a/cache.h b/cache.h\nindex 1784fc6..aedfd05 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -1452,6 +1452,8 @@ static inline ssize_t write_str_in_full(int fd, const char *str)\n {\n \treturn write_in_full(fd, str, strlen(str));\n }\n+__attribute__((format (printf,3,4)))\n+extern int write_file(const char *path, int fatal, const char *fmt, ...);\n \n /* pager.c */\n extern void setup_pager(void);\ndiff --git a/wrapper.c b/wrapper.c\nindex 25074d7..d53a3b3 100644\n--- a/wrapper.c\n+++ b/wrapper.c\n@@ -537,3 +537,34 @@ char *xgetcwd(void)\n \t\tdie_errno(_(\"unable to get current working directory\"));\n \treturn strbuf_detach(&sb, NULL);\n }\n+\n+int write_file(const char *path, int fatal, const char *fmt, ...)\n+{\n+\tstruct strbuf sb = STRBUF_INIT;\n+\tva_list params;\n+\tint fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0666);\n+\tif (fd < 0) {\n+\t\tif (fatal)\n+\t\t\tdie_errno(_(\"could not open %s for writing\"), path);\n+\t\treturn -1;\n+\t}\n+\tva_start(params, fmt);\n+\tstrbuf_vaddf(&sb, fmt, params);\n+\tva_end(params);\n+\tif (write_in_full(fd, sb.buf, sb.len) != sb.len) {\n+\t\tint err = errno;\n+\t\tclose(fd);\n+\t\tstrbuf_release(&sb);\n+\t\terrno = err;\n+\t\tif (fatal)\n+\t\t\tdie_errno(_(\"could not write to %s\"), path);\n+\t\treturn -1;\n+\t}\n+\tstrbuf_release(&sb);\n+\tif (close(fd)) {\n+\t\tif (fatal)\n+\t\t\tdie_errno(_(\"could not close %s\"), path);\n+\t\treturn -1;\n+\t}\n+\treturn 0;\n+}\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249948","messageId":"1411867366-3821-22-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1411867366-3821-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v3 21/32] use new wrapper write_file() for simple file writing","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-28T01:22:35Z","receivedAt":"2014-09-28T01:22:35Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"This fixes common problems in these code about error handling,\nforgetting to close the file handle after fprintf() fails, or not\nprinting out the error string..\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n builtin/branch.c  |  4 +---\n builtin/init-db.c |  7 +------\n daemon.c          | 11 +----------\n submodule.c       |  9 ++-------\n transport.c       |  8 +++-----\n 5 files changed, 8 insertions(+), 31 deletions(-)\n\ndiff --git a/builtin/branch.c b/builtin/branch.c\nindex 9e4666f..a8b79bf 100644\n--- a/builtin/branch.c\n+++ b/builtin/branch.c\n@@ -761,7 +761,6 @@ static const char edit_description[] = \"BRANCH_DESCRIPTION\";\n \n static int edit_branch_description(const char *branch_name)\n {\n-\tFILE *fp;\n \tint status;\n \tstruct strbuf buf = STRBUF_INIT;\n \tstruct strbuf name = STRBUF_INIT;\n@@ -774,8 +773,7 @@ static int edit_branch_description(const char *branch_name)\n \t\t    \"  %s\\n\"\n \t\t    \"Lines starting with '%c' will be stripped.\\n\",\n \t\t    branch_name, comment_line_char);\n-\tfp = fopen(git_path(edit_description), \"w\");\n-\tif ((fwrite(buf.buf, 1, buf.len, fp) < buf.len) || fclose(fp)) {\n+\tif (write_file(git_path(edit_description), 0, \"%s\", buf.buf)) {\n \t\tstrbuf_release(&buf);\n \t\treturn error(_(\"could not write branch description template: %s\"),\n \t\t\t     strerror(errno));\ndiff --git a/builtin/init-db.c b/builtin/init-db.c\nindex 587a505..6b7fa5f 100644\n--- a/builtin/init-db.c\n+++ b/builtin/init-db.c\n@@ -342,7 +342,6 @@ int set_git_dir_init(const char *git_dir, const char *real_git_dir,\n static void separate_git_dir(const char *git_dir)\n {\n \tstruct stat st;\n-\tFILE *fp;\n \n \tif (!stat(git_link, &st)) {\n \t\tconst char *src;\n@@ -358,11 +357,7 @@ static void separate_git_dir(const char *git_dir)\n \t\t\tdie_errno(_(\"unable to move %s to %s\"), src, git_dir);\n \t}\n \n-\tfp = fopen(git_link, \"w\");\n-\tif (!fp)\n-\t\tdie(_(\"Could not create git link %s\"), git_link);\n-\tfprintf(fp, \"gitdir: %s\\n\", git_dir);\n-\tfclose(fp);\n+\twrite_file(git_link, 1, \"gitdir: %s\\n\", git_dir);\n }\n \n int init_db(const char *template_dir, unsigned int flags)\ndiff --git a/daemon.c b/daemon.c\nindex 4dcfff9..8420fdb 100644\n--- a/daemon.c\n+++ b/daemon.c\n@@ -1073,15 +1073,6 @@ static struct credentials *prepare_credentials(const char *user_name,\n }\n #endif\n \n-static void store_pid(const char *path)\n-{\n-\tFILE *f = fopen(path, \"w\");\n-\tif (!f)\n-\t\tdie_errno(\"cannot open pid file '%s'\", path);\n-\tif (fprintf(f, \"%\"PRIuMAX\"\\n\", (uintmax_t) getpid()) < 0 || fclose(f) != 0)\n-\t\tdie_errno(\"failed to write pid file '%s'\", path);\n-}\n-\n static int serve(struct string_list *listen_addr, int listen_port,\n     struct credentials *cred)\n {\n@@ -1292,7 +1283,7 @@ int main(int argc, char **argv)\n \t\tsanitize_stdfds();\n \n \tif (pid_file)\n-\t\tstore_pid(pid_file);\n+\t\twrite_file(pid_file, 1, \"%\"PRIuMAX\"\\n\", (uintmax_t) getpid());\n \n \t/* prepare argv for serving-processes */\n \tcld_argv = xmalloc(sizeof (char *) * (argc + 2));\ndiff --git a/submodule.c b/submodule.c\nindex 0690dc5..34094f5 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -1102,16 +1102,11 @@ void connect_work_tree_and_git_dir(const char *work_tree, const char *git_dir)\n \tstruct strbuf file_name = STRBUF_INIT;\n \tstruct strbuf rel_path = STRBUF_INIT;\n \tconst char *real_work_tree = xstrdup(real_path(work_tree));\n-\tFILE *fp;\n \n \t/* Update gitfile */\n \tstrbuf_addf(&file_name, \"%s/.git\", work_tree);\n-\tfp = fopen(file_name.buf, \"w\");\n-\tif (!fp)\n-\t\tdie(_(\"Could not create git link %s\"), file_name.buf);\n-\tfprintf(fp, \"gitdir: %s\\n\", relative_path(git_dir, real_work_tree,\n-\t\t\t\t\t\t  &rel_path));\n-\tfclose(fp);\n+\twrite_file(file_name.buf, 1, \"gitdir: %s\\n\",\n+\t\t   relative_path(git_dir, real_work_tree, &rel_path));\n \n \t/* Update core.worktree setting */\n \tstrbuf_reset(&file_name);\ndiff --git a/transport.c b/transport.c\nindex 7388bb8..0bb15ee 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -282,7 +282,6 @@ static int write_one_ref(const char *name, const unsigned char *sha1,\n {\n \tstruct strbuf *buf = data;\n \tint len = buf->len;\n-\tFILE *f;\n \n \t/* when called via for_each_ref(), flags is non-zero */\n \tif (flags && !starts_with(name, \"refs/heads/\") &&\n@@ -291,10 +290,9 @@ static int write_one_ref(const char *name, const unsigned char *sha1,\n \n \tstrbuf_addstr(buf, name);\n \tif (safe_create_leading_directories(buf->buf) ||\n-\t\t\t!(f = fopen(buf->buf, \"w\")) ||\n-\t\t\tfprintf(f, \"%s\\n\", sha1_to_hex(sha1)) < 0 ||\n-\t\t\tfclose(f))\n-\t\treturn error(\"problems writing temporary file %s\", buf->buf);\n+\t    write_file(buf->buf, 0, \"%s\\n\", sha1_to_hex(sha1)))\n+\t\treturn error(\"problems writing temporary file %s: %s\",\n+\t\t\t     buf->buf, strerror(errno));\n \tstrbuf_setlen(buf, len);\n \treturn 0;\n }\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249949","messageId":"1411867366-3821-23-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1411867366-3821-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v3 22/32] checkout: support checking out into a new working directory","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-28T01:22:36Z","receivedAt":"2014-09-28T01:22:36Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"\"git checkout --to\" sets up a new working directory with a .git file\npointing to $GIT_DIR/worktrees/<id>. It then executes \"git checkout\"\nagain on the new worktree with the same arguments except \"--to\" is\ntaken out. The second checkout execution, which is not contaminated\nwith any info from the current repository, will actually check out and\neverything that normal \"git checkout\" does.\n\nHelped-by: Marc Branchaud <marcnarc@xiplink.com>\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n Documentation/git-checkout.txt         | 46 ++++++++++++++++\n Documentation/git.txt                  |  3 +-\n Documentation/gitrepository-layout.txt |  7 +++\n builtin/checkout.c                     | 95 +++++++++++++++++++++++++++++++++-\n path.c                                 |  2 +-\n t/t2025-checkout-to.sh (new +x)        | 63 ++++++++++++++++++++++\n 6 files changed, 212 insertions(+), 4 deletions(-)\n create mode 100755 t/t2025-checkout-to.sh\n\ndiff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt\nindex 33ad2ad..c101575 100644\n--- a/Documentation/git-checkout.txt\n+++ b/Documentation/git-checkout.txt\n@@ -225,6 +225,13 @@ This means that you can use `git checkout -p` to selectively discard\n edits from your current working tree. See the ``Interactive Mode''\n section of linkgit:git-add[1] to learn how to operate the `--patch` mode.\n \n+--to=<path>::\n+\tCheck out a branch in a separate working directory at\n+\t`<path>`. A new working directory is linked to the current\n+\trepository, sharing everything except working directory\n+\tspecific files such as HEAD, index... See \"MULTIPLE WORKING\n+\tTREES\" section for more information.\n+\n <branch>::\n \tBranch to checkout; if it refers to a branch (i.e., a name that,\n \twhen prepended with \"refs/heads/\", is a valid ref), then that\n@@ -388,6 +395,45 @@ $ git reflog -2 HEAD # or\n $ git log -g -2 HEAD\n ------------\n \n+MULTIPLE WORKING TREES\n+----------------------\n+\n+A git repository can support multiple working trees, allowing you to check\n+out more than one branch at a time.  With `git checkout --to` a new working\n+tree is associated with the repository.  This new working tree is called a\n+\"linked working tree\" as opposed to the \"main working tree\" prepared by \"git\n+init\" or \"git clone\".  A repository has one main working tree (if it's not a\n+bare repository) and zero or more linked working trees.\n+\n+Each linked working tree has a private sub-directory in the repository's\n+$GIT_DIR/worktrees directory.  The private sub-directory's name is usually\n+the base name of the linked working tree's path, possibly appended with a\n+number to make it unique.  For example, when `$GIT_DIR=/path/main/.git` the\n+command `git checkout --to /path/other/test-next next` creates the linked\n+working tree in `/path/other/test-next` and also creates a\n+`$GIT_DIR/worktrees/test-next` directory (or `$GIT_DIR/worktrees/test-next1`\n+if `test-next` is already taken).\n+\n+Within a linked working tree, $GIT_DIR is set to point to this private\n+directory (e.g. `/path/main/.git/worktrees/test-next` in the example) and\n+$GIT_COMMON_DIR is set to point back to the main working tree's $GIT_DIR\n+(e.g. `/path/main/.git`). These settings are made in a `.git` file located at\n+the top directory of the linked working tree.\n+\n+Path resolution via `git rev-parse --git-path` uses either\n+$GIT_DIR or $GIT_COMMON_DIR depending on the path. For example, in the\n+linked working tree `git rev-parse --git-path HEAD` returns\n+`/path/main/.git/worktrees/test-next/HEAD` (not\n+`/path/other/test-next/.git/HEAD` or `/path/main/.git/HEAD`) while `git\n+rev-parse --git-path refs/heads/master` uses\n+$GIT_COMMON_DIR and returns `/path/main/.git/refs/heads/master`,\n+since refs are shared across all working trees.\n+\n+See linkgit:gitrepository-layout[5] for more information. The rule of\n+thumb is do not make any assumption about whether a path belongs to\n+$GIT_DIR or $GIT_COMMON_DIR when you need to directly access something\n+inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.\n+\n EXAMPLES\n --------\n \ndiff --git a/Documentation/git.txt b/Documentation/git.txt\nindex 176d37c..54c9fb6 100644\n--- a/Documentation/git.txt\n+++ b/Documentation/git.txt\n@@ -806,7 +806,8 @@ Git so take care if using Cogito etc.\n \tIf this variable is set to a path, non-worktree files that are\n \tnormally in $GIT_DIR will be taken from this path\n \tinstead. Worktree-specific files such as HEAD or index are\n-\ttaken from $GIT_DIR. See linkgit:gitrepository-layout[5] for\n+\ttaken from $GIT_DIR. See linkgit:gitrepository-layout[5] and\n+\tthe section 'MULTIPLE CHECKOUT MODE' in linkgit:checkout[1]\n \tdetails. This variable has lower precedence than other path\n \tvariables such as GIT_INDEX_FILE, GIT_OBJECT_DIRECTORY...\n \ndiff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt\nindex 2dc5667..8228450 100644\n--- a/Documentation/gitrepository-layout.txt\n+++ b/Documentation/gitrepository-layout.txt\n@@ -252,6 +252,13 @@ modules::\n \tdirectory is ignored if $GIT_COMMON_DIR is set and\n \t\"$GIT_COMMON_DIR/modules\" will be used instead.\n \n+worktrees::\n+\tContains worktree specific information of linked\n+\tcheckouts. Each subdirectory contains the worktree-related\n+\tpart of a linked checkout. This directory is ignored if\n+\t$GIT_COMMON_DIR is set and \"$GIT_COMMON_DIR/worktrees\" will be\n+\tused instead.\n+\n SEE ALSO\n --------\n linkgit:git-init[1],\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 220f80e..ad10f99 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -48,6 +48,10 @@ struct checkout_opts {\n \tconst char *prefix;\n \tstruct pathspec pathspec;\n \tstruct tree *source_tree;\n+\n+\tconst char *new_worktree;\n+\tconst char **saved_argv;\n+\tint new_worktree_mode;\n };\n \n static int post_checkout_hook(struct commit *old, struct commit *new,\n@@ -249,6 +253,9 @@ static int checkout_paths(const struct checkout_opts *opts,\n \t\tdie(_(\"Cannot update paths and switch to branch '%s' at the same time.\"),\n \t\t    opts->new_branch);\n \n+\tif (opts->new_worktree)\n+\t\tdie(_(\"'%s' cannot be used with updating paths\"), \"--to\");\n+\n \tif (opts->patch_mode)\n \t\treturn run_add_interactive(revision, \"--patch=checkout\",\n \t\t\t\t\t   &opts->pathspec);\n@@ -484,7 +491,7 @@ static int merge_working_tree(const struct checkout_opts *opts,\n \t\t\ttopts.dir->flags |= DIR_SHOW_IGNORED;\n \t\t\tsetup_standard_excludes(topts.dir);\n \t\t}\n-\t\ttree = parse_tree_indirect(old->commit ?\n+\t\ttree = parse_tree_indirect(old->commit && !opts->new_worktree_mode ?\n \t\t\t\t\t   old->commit->object.sha1 :\n \t\t\t\t\t   EMPTY_TREE_SHA1_BIN);\n \t\tinit_tree_desc(&trees[0], tree->buffer, tree->size);\n@@ -800,7 +807,8 @@ static int switch_branches(const struct checkout_opts *opts,\n \t\treturn ret;\n \t}\n \n-\tif (!opts->quiet && !old.path && old.commit && new->commit != old.commit)\n+\tif (!opts->quiet && !old.path && old.commit &&\n+\t    new->commit != old.commit && !opts->new_worktree_mode)\n \t\torphaned_commit_warning(old.commit, new->commit);\n \n \tupdate_refs_for_switch(opts, &old, new);\n@@ -810,6 +818,76 @@ static int switch_branches(const struct checkout_opts *opts,\n \treturn ret || writeout_error;\n }\n \n+static int prepare_linked_checkout(const struct checkout_opts *opts,\n+\t\t\t\t   struct branch_info *new)\n+{\n+\tstruct strbuf sb_git = STRBUF_INIT, sb_repo = STRBUF_INIT;\n+\tstruct strbuf sb = STRBUF_INIT;\n+\tconst char *path = opts->new_worktree, *name;\n+\tstruct stat st;\n+\tstruct child_process cp;\n+\tint counter = 0, len;\n+\n+\tif (!new->commit)\n+\t\tdie(_(\"no branch specified\"));\n+\tif (file_exists(path))\n+\t\tdie(_(\"'%s' already exists\"), path);\n+\n+\tlen = strlen(path);\n+\twhile (len && is_dir_sep(path[len - 1]))\n+\t\tlen--;\n+\n+\tfor (name = path + len - 1; name > path; name--)\n+\t\tif (is_dir_sep(*name)) {\n+\t\t\tname++;\n+\t\t\tbreak;\n+\t\t}\n+\tstrbuf_addstr(&sb_repo,\n+\t\t      git_path(\"worktrees/%.*s\", (int)(path + len - name), name));\n+\tlen = sb_repo.len;\n+\tif (safe_create_leading_directories_const(sb_repo.buf))\n+\t\tdie_errno(_(\"could not create leading directories of '%s'\"),\n+\t\t\t  sb_repo.buf);\n+\twhile (!stat(sb_repo.buf, &st)) {\n+\t\tcounter++;\n+\t\tstrbuf_setlen(&sb_repo, len);\n+\t\tstrbuf_addf(&sb_repo, \"%d\", counter);\n+\t}\n+\tname = strrchr(sb_repo.buf, '/') + 1;\n+\tif (mkdir(sb_repo.buf, 0777))\n+\t\tdie_errno(_(\"could not create directory of '%s'\"), sb_repo.buf);\n+\n+\tstrbuf_addf(&sb_git, \"%s/.git\", path);\n+\tif (safe_create_leading_directories_const(sb_git.buf))\n+\t\tdie_errno(_(\"could not create leading directories of '%s'\"),\n+\t\t\t  sb_git.buf);\n+\n+\twrite_file(sb_git.buf, 1, \"gitdir: %s/worktrees/%s\\n\",\n+\t\t   real_path(get_git_common_dir()), name);\n+\t/*\n+\t * This is to keep resolve_ref() happy. We need a valid HEAD\n+\t * or is_git_directory() will reject the directory. Any valid\n+\t * value would do because this value will be ignored and\n+\t * replaced at the next (real) checkout.\n+\t */\n+\tstrbuf_addf(&sb, \"%s/HEAD\", sb_repo.buf);\n+\twrite_file(sb.buf, 1, \"%s\\n\", sha1_to_hex(new->commit->object.sha1));\n+\tstrbuf_reset(&sb);\n+\tstrbuf_addf(&sb, \"%s/commondir\", sb_repo.buf);\n+\twrite_file(sb.buf, 1, \"../..\\n\");\n+\n+\tif (!opts->quiet)\n+\t\tfprintf_ln(stderr, _(\"Enter %s (identifier %s)\"), path, name);\n+\n+\tsetenv(\"GIT_CHECKOUT_NEW_WORKTREE\", \"1\", 1);\n+\tsetenv(GIT_DIR_ENVIRONMENT, sb_git.buf, 1);\n+\tsetenv(GIT_WORK_TREE_ENVIRONMENT, path, 1);\n+\tmemset(&cp, 0, sizeof(cp));\n+\tcp.git_cmd = 1;\n+\tcp.argv = opts->saved_argv;\n+\treturn run_command(&cp);\n+}\n+\n static int git_checkout_config(const char *var, const char *value, void *cb)\n {\n \tif (!strcmp(var, \"diff.ignoresubmodules\")) {\n@@ -1071,6 +1149,9 @@ static int checkout_branch(struct checkout_opts *opts,\n \t\tdie(_(\"Cannot switch branch to a non-commit '%s'\"),\n \t\t    new->name);\n \n+\tif (opts->new_worktree)\n+\t\treturn prepare_linked_checkout(opts, new);\n+\n \tif (!new->commit && opts->new_branch) {\n \t\tunsigned char rev[20];\n \t\tint flag;\n@@ -1113,6 +1194,8 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)\n \t\t\t N_(\"do not limit pathspecs to sparse entries only\")),\n \t\tOPT_HIDDEN_BOOL(0, \"guess\", &dwim_new_local_branch,\n \t\t\t\tN_(\"second guess 'git checkout no-such-branch'\")),\n+\t\tOPT_FILENAME(0, \"to\", &opts.new_worktree,\n+\t\t\t   N_(\"check a branch out in a separate working directory\")),\n \t\tOPT_END(),\n \t};\n \n@@ -1121,6 +1204,9 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)\n \topts.overwrite_ignore = 1;\n \topts.prefix = prefix;\n \n+\topts.saved_argv = xmalloc(sizeof(const char *) * (argc + 2));\n+\tmemcpy(opts.saved_argv, argv, sizeof(const char *) * (argc + 1));\n+\n \tgitmodules_config();\n \tgit_config(git_checkout_config, &opts);\n \n@@ -1129,6 +1215,11 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)\n \targc = parse_options(argc, argv, prefix, options, checkout_usage,\n \t\t\t     PARSE_OPT_KEEP_DASHDASH);\n \n+\t/* recursive execution from checkout_new_worktree() */\n+\topts.new_worktree_mode = getenv(\"GIT_CHECKOUT_NEW_WORKTREE\") != NULL;\n+\tif (opts.new_worktree_mode)\n+\t\topts.new_worktree = NULL;\n+\n \tif (conflict_style) {\n \t\topts.merge = 1; /* implied */\n \t\tgit_xmerge_config(\"merge.conflictstyle\", conflict_style, NULL);\ndiff --git a/path.c b/path.c\nindex 94db501..72eca6d 100644\n--- a/path.c\n+++ b/path.c\n@@ -92,7 +92,7 @@ static void replace_dir(struct strbuf *buf, int len, const char *newdir)\n \n static const char *common_list[] = {\n \t\"/branches\", \"/hooks\", \"/info\", \"/logs\", \"/lost-found\", \"/modules\",\n-\t\"/objects\", \"/refs\", \"/remotes\", \"/rr-cache\", \"/svn\",\n+\t\"/objects\", \"/refs\", \"/remotes\", \"/worktrees\", \"/rr-cache\", \"/svn\",\n \t\"config\", \"gc.pid\", \"packed-refs\", \"shallow\",\n \tNULL\n };\ndiff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh\nnew file mode 100755\nindex 0000000..4963415\n--- /dev/null\n+++ b/t/t2025-checkout-to.sh\n@@ -0,0 +1,63 @@\n+#!/bin/sh\n+\n+test_description='test git checkout --to'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'setup' '\n+\ttest_commit init\n+'\n+\n+test_expect_success 'checkout --to not updating paths' '\n+\ttest_must_fail git checkout --to -- init.t\n+'\n+\n+test_expect_success 'checkout --to an existing worktree' '\n+\tmkdir existing &&\n+\ttest_must_fail git checkout --detach --to existing master\n+'\n+\n+test_expect_success 'checkout --to a new worktree' '\n+\tgit checkout --to here master &&\n+\t(\n+\t\tcd here &&\n+\t\ttest_cmp ../init.t init.t &&\n+\t\tgit symbolic-ref HEAD >actual &&\n+\t\techo refs/heads/master >expect &&\n+\t\ttest_cmp expect actual &&\n+\t\tgit fsck\n+\t)\n+'\n+\n+test_expect_success 'checkout --to a new worktree from a subdir' '\n+\t(\n+\t\tmkdir sub &&\n+\t\tcd sub &&\n+\t\tgit checkout --detach --to here master &&\n+\t\tcd here &&\n+\t\ttest_cmp ../../init.t init.t\n+\t)\n+'\n+\n+test_expect_success 'checkout --to from a linked checkout' '\n+\t(\n+\t\tcd here &&\n+\t\tgit checkout --to nested-here master &&\n+\t\tcd nested-here &&\n+\t\tgit fsck\n+\t)\n+'\n+\n+test_expect_success 'checkout --to a new worktree creating new branch' '\n+\tgit checkout --to there -b newmaster master &&\n+\t(\n+\t\tcd there &&\n+\t\ttest_cmp ../init.t init.t &&\n+\t\tgit symbolic-ref HEAD >actual &&\n+\t\techo refs/heads/newmaster >expect &&\n+\t\ttest_cmp expect actual &&\n+\t\tgit fsck\n+\t)\n+'\n+\n+test_done\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249950","messageId":"1411867366-3821-24-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1411867366-3821-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v3 23/32] prune: strategies for linked checkouts","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-28T01:22:37Z","receivedAt":"2014-09-28T01:22:37Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"(alias R=$GIT_COMMON_DIR/worktrees/<id>)\n\n - linked checkouts are supposed to keep its location in $R/gitdir up\n   to date. The use case is auto fixup after a manual checkout move.\n\n - linked checkouts are supposed to update mtime of $R/gitdir. If\n   $R/gitdir's mtime is older than a limit, and it points to nowhere,\n   worktrees/<id> is to be pruned.\n\n - If $R/locked exists, worktrees/<id> is not supposed to be pruned. If\n   $R/locked exists and $R/gitdir's mtime is older than a really long\n   limit, warn about old unused repo.\n\n - \"git checkout --to\" is supposed to make a hard link named $R/link\n   pointing to the .git file on supported file systems to help detect\n   the user manually deleting the checkout. If $R/link exists and its\n   link count is greated than 1, the repo is kept.\n\nHelped-by: Marc Branchaud <marcnarc@xiplink.com>\nHelped-by: Eric Sunshine <sunshine@sunshineco.com>\nSigned-off-by: Marc Branchaud <marcnarc@xiplink.com>\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n Documentation/git-checkout.txt             | 20 +++++++\n Documentation/git-prune.txt                |  3 +\n Documentation/gitrepository-layout.txt     | 19 ++++++\n builtin/checkout.c                         | 19 +++++-\n builtin/prune.c                            | 95 ++++++++++++++++++++++++++++++\n setup.c                                    | 13 ++++\n t/t2026-prune-linked-checkouts.sh (new +x) | 84 ++++++++++++++++++++++++++\n 7 files changed, 251 insertions(+), 2 deletions(-)\n create mode 100755 t/t2026-prune-linked-checkouts.sh\n\ndiff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt\nindex c101575..35675da 100644\n--- a/Documentation/git-checkout.txt\n+++ b/Documentation/git-checkout.txt\n@@ -434,6 +434,26 @@ thumb is do not make any assumption about whether a path belongs to\n $GIT_DIR or $GIT_COMMON_DIR when you need to directly access something\n inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.\n \n+When you are done with a linked working tree you can simply delete it.\n+You can clean up any stale $GIT_DIR/worktrees entries via `git prune\n+--worktrees` in the main or any linked working tree.\n+\n+If you move a linked working directory to another file system, or\n+within a file system that does not support hard links, you need to run\n+at least one git command inside the linked working directory\n+(e.g. `git status`) in order to update its entry in $GIT_DIR/worktrees\n+so that it does not get automatically removed.\n+\n+To prevent `git prune --worktrees` from deleting a $GIT_DIR/worktrees\n+entry (which can be useful in some situations, such as when the\n+entry's working tree is stored on a portable device), add a file named\n+'locked' to the entry's directory. The file contains the reason in\n+plain text. For example, if a linked working tree's `.git` file points\n+to `/path/main/.git/worktrees/test-next` then a file named\n+`/path/main/.git/worktrees/test-next/locked` will prevent the\n+`test-next` entry from being pruned.  See\n+linkgit:gitrepository-layout[5] for details.\n+\n EXAMPLES\n --------\n \ndiff --git a/Documentation/git-prune.txt b/Documentation/git-prune.txt\nindex 7a493c8..1cf3bed 100644\n--- a/Documentation/git-prune.txt\n+++ b/Documentation/git-prune.txt\n@@ -48,6 +48,9 @@ OPTIONS\n --expire <time>::\n \tOnly expire loose objects older than <time>.\n \n+--worktrees::\n+\tPrune dead working tree information in $GIT_DIR/worktrees.\n+\n <head>...::\n \tIn addition to objects\n \treachable from any of our references, keep objects\ndiff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt\nindex 8228450..2b30a92 100644\n--- a/Documentation/gitrepository-layout.txt\n+++ b/Documentation/gitrepository-layout.txt\n@@ -259,6 +259,25 @@ worktrees::\n \t$GIT_COMMON_DIR is set and \"$GIT_COMMON_DIR/worktrees\" will be\n \tused instead.\n \n+worktrees/<id>/gitdir::\n+\tA text file containing the absolute path back to the .git file\n+\tthat points to here. This is used to check if the linked\n+\trepository has been manually removed and there is no need to\n+\tkeep this directory any more. mtime of this file should be\n+\tupdated every time the linked repository is accessed.\n+\n+worktrees/<id>/locked::\n+\tIf this file exists, the linked repository may be on a\n+\tportable device and not available. It does not mean that the\n+\tlinked repository is gone and `worktrees/<id>` could be\n+\tremoved. The file's content contains a reason string on why\n+\tthe repository is locked.\n+\n+worktrees/<id>/link::\n+\tIf this file exists, it is a hard link to the linked .git\n+\tfile. It is used to detect if the linked repository is\n+\tmanually removed.\n+\n SEE ALSO\n --------\n linkgit:git-init[1],\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex ad10f99..ab46af9 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -826,7 +826,7 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,\n \tconst char *path = opts->new_worktree, *name;\n \tstruct stat st;\n \tstruct child_process cp;\n-\tint counter = 0, len;\n+\tint counter = 0, len, ret;\n \n \tif (!new->commit)\n \t\tdie(_(\"no branch specified\"));\n@@ -857,11 +857,21 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,\n \tif (mkdir(sb_repo.buf, 0777))\n \t\tdie_errno(_(\"could not create directory of '%s'\"), sb_repo.buf);\n \n+\t/*\n+\t * lock the incomplete repo so prune won't delete it, unlock\n+\t * after the preparation is over.\n+\t */\n+\tstrbuf_addf(&sb, \"%s/locked\", sb_repo.buf);\n+\twrite_file(sb.buf, 1, \"initializing\\n\");\n+\n \tstrbuf_addf(&sb_git, \"%s/.git\", path);\n \tif (safe_create_leading_directories_const(sb_git.buf))\n \t\tdie_errno(_(\"could not create leading directories of '%s'\"),\n \t\t\t  sb_git.buf);\n \n+\tstrbuf_reset(&sb);\n+\tstrbuf_addf(&sb, \"%s/gitdir\", sb_repo.buf);\n+\twrite_file(sb.buf, 1, \"%s\\n\", real_path(sb_git.buf));\n \twrite_file(sb_git.buf, 1, \"gitdir: %s/worktrees/%s\\n\",\n \t\t   real_path(get_git_common_dir()), name);\n \t/*\n@@ -870,6 +880,7 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,\n \t * value would do because this value will be ignored and\n \t * replaced at the next (real) checkout.\n \t */\n+\tstrbuf_reset(&sb);\n \tstrbuf_addf(&sb, \"%s/HEAD\", sb_repo.buf);\n \twrite_file(sb.buf, 1, \"%s\\n\", sha1_to_hex(new->commit->object.sha1));\n \tstrbuf_reset(&sb);\n@@ -885,7 +896,11 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,\n \tmemset(&cp, 0, sizeof(cp));\n \tcp.git_cmd = 1;\n \tcp.argv = opts->saved_argv;\n-\treturn run_command(&cp);\n+\tret = run_command(&cp);\n+\tstrbuf_reset(&sb);\n+\tstrbuf_addf(&sb, \"%s/locked\", sb_repo.buf);\n+\tunlink_or_warn(sb.buf);\n+\treturn ret;\n }\n \n static int git_checkout_config(const char *var, const char *value, void *cb)\ndiff --git a/builtin/prune.c b/builtin/prune.c\nindex 144a3bd..cf56110 100644\n--- a/builtin/prune.c\n+++ b/builtin/prune.c\n@@ -112,6 +112,91 @@ static void prune_object_dir(const char *path)\n \t}\n }\n \n+static int prune_worktree(const char *id, struct strbuf *reason)\n+{\n+\tstruct stat st;\n+\tchar *path;\n+\tint fd, len;\n+\n+\tif (!is_directory(git_path(\"worktrees/%s\", id))) {\n+\t\tstrbuf_addf(reason, _(\"Removing worktrees/%s: not a valid directory\"), id);\n+\t\treturn 1;\n+\t}\n+\tif (file_exists(git_path(\"worktrees/%s/locked\", id)))\n+\t\treturn 0;\n+\tif (stat(git_path(\"worktrees/%s/gitdir\", id), &st)) {\n+\t\tstrbuf_addf(reason, _(\"Removing worktrees/%s: gitdir file does not exist\"), id);\n+\t\treturn 1;\n+\t}\n+\tfd = open(git_path(\"worktrees/%s/gitdir\", id), O_RDONLY);\n+\tif (fd < 0) {\n+\t\tstrbuf_addf(reason, _(\"Removing worktrees/%s: unable to read gitdir file (%s)\"),\n+\t\t\t    id, strerror(errno));\n+\t\treturn 1;\n+\t}\n+\tlen = st.st_size;\n+\tpath = xmalloc(len + 1);\n+\tread_in_full(fd, path, len);\n+\tclose(fd);\n+\twhile (len && (path[len - 1] == '\\n' || path[len - 1] == '\\r'))\n+\t\tlen--;\n+\tif (!len) {\n+\t\tstrbuf_addf(reason, _(\"Removing worktrees/%s: invalid gitdir file\"), id);\n+\t\tfree(path);\n+\t\treturn 1;\n+\t}\n+\tpath[len] = '\\0';\n+\tif (!file_exists(path)) {\n+\t\tstruct stat st_link;\n+\t\tfree(path);\n+\t\t/*\n+\t\t * the repo is moved manually and has not been\n+\t\t * accessed since?\n+\t\t */\n+\t\tif (!stat(git_path(\"worktrees/%s/link\", id), &st_link) &&\n+\t\t    st_link.st_nlink > 1)\n+\t\t\treturn 0;\n+\t\tstrbuf_addf(reason, _(\"Removing worktrees/%s: gitdir file points to non-existent location\"), id);\n+\t\treturn 1;\n+\t}\n+\tfree(path);\n+\treturn st.st_mtime <= expire;\n+}\n+\n+static void prune_worktrees(void)\n+{\n+\tstruct strbuf reason = STRBUF_INIT;\n+\tstruct strbuf path = STRBUF_INIT;\n+\tDIR *dir = opendir(git_path(\"worktrees\"));\n+\tstruct dirent *d;\n+\tint ret;\n+\tif (!dir)\n+\t\treturn;\n+\twhile ((d = readdir(dir)) != NULL) {\n+\t\tif (!strcmp(d->d_name, \".\") || !strcmp(d->d_name, \"..\"))\n+\t\t\tcontinue;\n+\t\tstrbuf_reset(&reason);\n+\t\tif (!prune_worktree(d->d_name, &reason))\n+\t\t\tcontinue;\n+\t\tif (show_only || verbose)\n+\t\t\tprintf(\"%s\\n\", reason.buf);\n+\t\tif (show_only)\n+\t\t\tcontinue;\n+\t\tstrbuf_reset(&path);\n+\t\tstrbuf_addstr(&path, git_path(\"worktrees/%s\", d->d_name));\n+\t\tret = remove_dir_recursively(&path, 0);\n+\t\tif (ret < 0 && errno == ENOTDIR)\n+\t\t\tret = unlink(path.buf);\n+\t\tif (ret)\n+\t\t\terror(_(\"failed to remove: %s\"), strerror(errno));\n+\t}\n+\tclosedir(dir);\n+\tif (!show_only)\n+\t\trmdir(git_path(\"worktrees\"));\n+\tstrbuf_release(&reason);\n+\tstrbuf_release(&path);\n+}\n+\n /*\n  * Write errors (particularly out of space) can result in\n  * failed temporary packs (and more rarely indexes and other\n@@ -138,10 +223,12 @@ int cmd_prune(int argc, const char **argv, const char *prefix)\n {\n \tstruct rev_info revs;\n \tstruct progress *progress = NULL;\n+\tint do_prune_worktrees = 0;\n \tconst struct option options[] = {\n \t\tOPT__DRY_RUN(&show_only, N_(\"do not remove, show only\")),\n \t\tOPT__VERBOSE(&verbose, N_(\"report pruned objects\")),\n \t\tOPT_BOOL(0, \"progress\", &show_progress, N_(\"show progress\")),\n+\t\tOPT_BOOL(0, \"worktrees\", &do_prune_worktrees, N_(\"prune .git/worktrees\")),\n \t\tOPT_EXPIRY_DATE(0, \"expire\", &expire,\n \t\t\t\tN_(\"expire objects older than <time>\")),\n \t\tOPT_END()\n@@ -154,6 +241,14 @@ int cmd_prune(int argc, const char **argv, const char *prefix)\n \tinit_revisions(&revs, prefix);\n \n \targc = parse_options(argc, argv, prefix, options, prune_usage, 0);\n+\n+\tif (do_prune_worktrees) {\n+\t\tif (argc)\n+\t\t\tdie(_(\"--worktrees does not take extra arguments\"));\n+\t\tprune_worktrees();\n+\t\treturn 0;\n+\t}\n+\n \twhile (argc--) {\n \t\tunsigned char sha1[20];\n \t\tconst char *name = *argv++;\ndiff --git a/setup.c b/setup.c\nindex b99bdd3..fb61860 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -390,6 +390,17 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)\n \treturn ret;\n }\n \n+static void update_linked_gitdir(const char *gitfile, const char *gitdir)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\tstruct stat st;\n+\n+\tstrbuf_addf(&path, \"%s/gitfile\", gitdir);\n+\tif (stat(path.buf, &st) || st.st_mtime + 24 * 3600 < time(NULL))\n+\t\twrite_file(path.buf, 0, \"%s\\n\", gitfile);\n+\tstrbuf_release(&path);\n+}\n+\n /*\n  * Try to read the location of the git directory from the .git file,\n  * return path to git directory if found.\n@@ -438,6 +449,8 @@ const char *read_gitfile(const char *path)\n \n \tif (!is_git_directory(dir))\n \t\tdie(\"Not a git repository: %s\", dir);\n+\n+\tupdate_linked_gitdir(path, dir);\n \tpath = real_path(dir);\n \n \tfree(buf);\ndiff --git a/t/t2026-prune-linked-checkouts.sh b/t/t2026-prune-linked-checkouts.sh\nnew file mode 100755\nindex 0000000..3622800\n--- /dev/null\n+++ b/t/t2026-prune-linked-checkouts.sh\n@@ -0,0 +1,84 @@\n+#!/bin/sh\n+\n+test_description='prune $GIT_DIR/worktrees'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'prune --worktrees on normal repo' '\n+\tgit prune --worktrees &&\n+\ttest_must_fail git prune --worktrees abc\n+'\n+\n+test_expect_success 'prune files inside $GIT_DIR/worktrees' '\n+\tmkdir .git/worktrees &&\n+\t: >.git/worktrees/abc &&\n+\tgit prune --worktrees --verbose >actual &&\n+\tcat >expect <<EOF &&\n+Removing worktrees/abc: not a valid directory\n+EOF\n+\ttest_i18ncmp expect actual &&\n+\t! test -f .git/worktrees/abc &&\n+\t! test -d .git/worktrees\n+'\n+\n+test_expect_success 'prune directories without gitdir' '\n+\tmkdir -p .git/worktrees/def/abc &&\n+\t: >.git/worktrees/def/def &&\n+\tcat >expect <<EOF &&\n+Removing worktrees/def: gitdir file does not exist\n+EOF\n+\tgit prune --worktrees --verbose >actual &&\n+\ttest_i18ncmp expect actual &&\n+\t! test -d .git/worktrees/def &&\n+\t! test -d .git/worktrees\n+'\n+\n+test_expect_success POSIXPERM 'prune directories with unreadable gitdir' '\n+\tmkdir -p .git/worktrees/def/abc &&\n+\t: >.git/worktrees/def/def &&\n+\t: >.git/worktrees/def/gitdir &&\n+\tchmod u-r .git/worktrees/def/gitdir &&\n+\tgit prune --worktrees --verbose >actual &&\n+\ttest_i18ngrep \"Removing worktrees/def: unable to read gitdir file\" actual &&\n+\t! test -d .git/worktrees/def &&\n+\t! test -d .git/worktrees\n+'\n+\n+test_expect_success 'prune directories with invalid gitdir' '\n+\tmkdir -p .git/worktrees/def/abc &&\n+\t: >.git/worktrees/def/def &&\n+\t: >.git/worktrees/def/gitdir &&\n+\tgit prune --worktrees --verbose >actual &&\n+\ttest_i18ngrep \"Removing worktrees/def: invalid gitdir file\" actual &&\n+\t! test -d .git/worktrees/def &&\n+\t! test -d .git/worktrees\n+'\n+\n+test_expect_success 'prune directories with gitdir pointing to nowhere' '\n+\tmkdir -p .git/worktrees/def/abc &&\n+\t: >.git/worktrees/def/def &&\n+\techo \"$TRASH_DIRECTORY\"/nowhere >.git/worktrees/def/gitdir &&\n+\tgit prune --worktrees --verbose >actual &&\n+\ttest_i18ngrep \"Removing worktrees/def: gitdir file points to non-existent location\" actual &&\n+\t! test -d .git/worktrees/def &&\n+\t! test -d .git/worktrees\n+'\n+\n+test_expect_success 'not prune locked checkout' '\n+\ttest_when_finished rm -r .git/worktrees\n+\tmkdir -p .git/worktrees/ghi &&\n+\t: >.git/worktrees/ghi/locked &&\n+\tgit prune --worktrees &&\n+\ttest -d .git/worktrees/ghi\n+'\n+\n+test_expect_success 'not prune recent checkouts' '\n+\ttest_when_finished rm -r .git/worktrees\n+\tmkdir zz &&\n+\tmkdir -p .git/worktrees/jlm &&\n+\techo \"$TRASH_DIRECTORY\"/zz >.git/worktrees/jlm/gitdir &&\n+\tgit prune --worktrees --verbose --expire=2.days.ago &&\n+\ttest -d .git/worktrees/jlm\n+'\n+\n+test_done\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249951","messageId":"1411867366-3821-25-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1411867366-3821-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v3 24/32] checkout: reject if the branch is already checked out elsewhere","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-28T01:22:38Z","receivedAt":"2014-09-28T01:22:38Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"One branch obviously can't be checked out at two places (but detached\nheads are ok). Give the user a choice in this case: --detach, -b\nnew-branch, switch branch in the other checkout first or simply 'cd'\nand continue to work there.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n builtin/checkout.c     | 86 ++++++++++++++++++++++++++++++++++++++++++++++++--\n t/t2025-checkout-to.sh | 25 ++++++++++++---\n 2 files changed, 104 insertions(+), 7 deletions(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex ab46af9..6d68913 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -430,6 +430,11 @@ struct branch_info {\n \tconst char *name; /* The short name used */\n \tconst char *path; /* The full name of a real branch */\n \tstruct commit *commit; /* The named commit */\n+\t/*\n+\t * if not null the branch is detached because it's already\n+\t * checked out in this checkout\n+\t */\n+\tchar *checkout;\n };\n \n static void setup_branch_path(struct branch_info *branch)\n@@ -958,12 +963,78 @@ static const char *unique_tracking_name(const char *name, unsigned char *sha1)\n \treturn NULL;\n }\n \n+static void check_linked_checkout(struct branch_info *new, const char *id)\n+{\n+\tstruct strbuf sb = STRBUF_INIT;\n+\tstruct strbuf path = STRBUF_INIT;\n+\tstruct strbuf gitdir = STRBUF_INIT;\n+\tconst char *start, *end;\n+\n+\tif (id)\n+\t\tstrbuf_addf(&path, \"%s/worktrees/%s/HEAD\", get_git_common_dir(), id);\n+\telse\n+\t\tstrbuf_addf(&path, \"%s/HEAD\", get_git_common_dir());\n+\n+\tif (strbuf_read_file(&sb, path.buf, 0) < 0 ||\n+\t    !skip_prefix(sb.buf, \"ref:\", &start))\n+\t\tgoto done;\n+\twhile (isspace(*start))\n+\t\tstart++;\n+\tend = start;\n+\twhile (*end && !isspace(*end))\n+\t\tend++;\n+\tif (strncmp(start, new->path, end - start) || new->path[end - start] != '\\0')\n+\t\tgoto done;\n+\tif (id) {\n+\t\tstrbuf_reset(&path);\n+\t\tstrbuf_addf(&path, \"%s/worktrees/%s/gitdir\", get_git_common_dir(), id);\n+\t\tif (strbuf_read_file(&gitdir, path.buf, 0) <= 0)\n+\t\t\tgoto done;\n+\t\tstrbuf_rtrim(&gitdir);\n+\t} else\n+\t\tstrbuf_addstr(&gitdir, get_git_common_dir());\n+\tdie(_(\"'%s' is already checked out at '%s'\"), new->name, gitdir.buf);\n+done:\n+\tstrbuf_release(&path);\n+\tstrbuf_release(&sb);\n+\tstrbuf_release(&gitdir);\n+}\n+\n+static void check_linked_checkouts(struct branch_info *new)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\tDIR *dir;\n+\tstruct dirent *d;\n+\n+\tstrbuf_addf(&path, \"%s/worktrees\", get_git_common_dir());\n+\tif ((dir = opendir(path.buf)) == NULL) {\n+\t\tstrbuf_release(&path);\n+\t\treturn;\n+\t}\n+\n+\t/*\n+\t * $GIT_COMMON_DIR/HEAD is practically outside\n+\t * $GIT_DIR so resolve_ref_unsafe() won't work (it\n+\t * uses git_path). Parse the ref ourselves.\n+\t */\n+\tcheck_linked_checkout(new, NULL);\n+\n+\twhile ((d = readdir(dir)) != NULL) {\n+\t\tif (!strcmp(d->d_name, \".\") || !strcmp(d->d_name, \"..\"))\n+\t\t\tcontinue;\n+\t\tcheck_linked_checkout(new, d->d_name);\n+\t}\n+\tstrbuf_release(&path);\n+\tclosedir(dir);\n+}\n+\n static int parse_branchname_arg(int argc, const char **argv,\n \t\t\t\tint dwim_new_local_branch_ok,\n \t\t\t\tstruct branch_info *new,\n \t\t\t\tstruct tree **source_tree,\n \t\t\t\tunsigned char rev[20],\n-\t\t\t\tconst char **new_branch)\n+\t\t\t\tconst char **new_branch,\n+\t\t\t\tint force_detach)\n {\n \tint argcount = 0;\n \tunsigned char branch_rev[20];\n@@ -1085,6 +1156,16 @@ static int parse_branchname_arg(int argc, const char **argv,\n \telse\n \t\tnew->path = NULL; /* not an existing branch */\n \n+\tif (new->path && !force_detach && !*new_branch) {\n+\t\tunsigned char sha1[20];\n+\t\tint flag;\n+\t\tchar *head_ref = resolve_refdup(\"HEAD\", sha1, 0, &flag);\n+\t\tif (head_ref &&\n+\t\t    (!(flag & REF_ISSYMREF) || strcmp(head_ref, new->path)))\n+\t\t\tcheck_linked_checkouts(new);\n+\t\tfree(head_ref);\n+\t}\n+\n \tnew->commit = lookup_commit_reference_gently(rev, 1);\n \tif (!new->commit) {\n \t\t/* not a commit */\n@@ -1291,7 +1372,8 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)\n \t\t\t!opts.new_branch;\n \t\tint n = parse_branchname_arg(argc, argv, dwim_ok,\n \t\t\t\t\t     &new, &opts.source_tree,\n-\t\t\t\t\t     rev, &opts.new_branch);\n+\t\t\t\t\t     rev, &opts.new_branch,\n+\t\t\t\t\t     opts.force_detach);\n \t\targv += n;\n \t\targc -= n;\n \t}\ndiff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh\nindex 4963415..edd3404 100755\n--- a/t/t2025-checkout-to.sh\n+++ b/t/t2025-checkout-to.sh\n@@ -18,13 +18,14 @@ test_expect_success 'checkout --to an existing worktree' '\n '\n \n test_expect_success 'checkout --to a new worktree' '\n-\tgit checkout --to here master &&\n+\tgit rev-parse HEAD >expect &&\n+\tgit checkout --detach --to here master &&\n \t(\n \t\tcd here &&\n \t\ttest_cmp ../init.t init.t &&\n-\t\tgit symbolic-ref HEAD >actual &&\n-\t\techo refs/heads/master >expect &&\n-\t\ttest_cmp expect actual &&\n+\t\ttest_must_fail git symbolic-ref HEAD &&\n+\t\tgit rev-parse HEAD >actual &&\n+\t\ttest_cmp ../expect actual &&\n \t\tgit fsck\n \t)\n '\n@@ -42,7 +43,7 @@ test_expect_success 'checkout --to a new worktree from a subdir' '\n test_expect_success 'checkout --to from a linked checkout' '\n \t(\n \t\tcd here &&\n-\t\tgit checkout --to nested-here master &&\n+\t\tgit checkout --detach --to nested-here master &&\n \t\tcd nested-here &&\n \t\tgit fsck\n \t)\n@@ -60,4 +61,18 @@ test_expect_success 'checkout --to a new worktree creating new branch' '\n \t)\n '\n \n+test_expect_success 'die the same branch is already checked out' '\n+\t(\n+\t\tcd here &&\n+\t\ttest_must_fail git checkout newmaster\n+\t)\n+'\n+\n+test_expect_success 'not die on re-checking out current branch' '\n+\t(\n+\t\tcd there &&\n+\t\tgit checkout newmaster\n+\t)\n+'\n+\n test_done\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249952","messageId":"1411867366-3821-26-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1411867366-3821-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v3 25/32] checkout: clean up half-prepared directories in --to mode","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-28T01:22:39Z","receivedAt":"2014-09-28T01:22:39Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n builtin/checkout.c     | 48 ++++++++++++++++++++++++++++++++++++++++++++++++\n t/t2025-checkout-to.sh |  6 ++++++\n 2 files changed, 54 insertions(+)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 6d68913..bd0e2d5 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -20,6 +20,7 @@\n #include \"resolve-undo.h\"\n #include \"submodule.h\"\n #include \"argv-array.h\"\n+#include \"sigchain.h\"\n \n static const char * const checkout_usage[] = {\n \tN_(\"git checkout [options] <branch>\"),\n@@ -823,6 +824,35 @@ static int switch_branches(const struct checkout_opts *opts,\n \treturn ret || writeout_error;\n }\n \n+static char *junk_work_tree;\n+static char *junk_git_dir;\n+static int is_junk;\n+static pid_t junk_pid;\n+\n+static void remove_junk(void)\n+{\n+\tstruct strbuf sb = STRBUF_INIT;\n+\tif (!is_junk || getpid() != junk_pid)\n+\t\treturn;\n+\tif (junk_git_dir) {\n+\t\tstrbuf_addstr(&sb, junk_git_dir);\n+\t\tremove_dir_recursively(&sb, 0);\n+\t\tstrbuf_reset(&sb);\n+\t}\n+\tif (junk_work_tree) {\n+\t\tstrbuf_addstr(&sb, junk_work_tree);\n+\t\tremove_dir_recursively(&sb, 0);\n+\t}\n+\tstrbuf_release(&sb);\n+}\n+\n+static void remove_junk_on_signal(int signo)\n+{\n+\tremove_junk();\n+\tsigchain_pop(signo);\n+\traise(signo);\n+}\n+\n static int prepare_linked_checkout(const struct checkout_opts *opts,\n \t\t\t\t   struct branch_info *new)\n {\n@@ -859,8 +889,15 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,\n \t\tstrbuf_addf(&sb_repo, \"%d\", counter);\n \t}\n \tname = strrchr(sb_repo.buf, '/') + 1;\n+\n+\tjunk_pid = getpid();\n+\tatexit(remove_junk);\n+\tsigchain_push_common(remove_junk_on_signal);\n+\n \tif (mkdir(sb_repo.buf, 0777))\n \t\tdie_errno(_(\"could not create directory of '%s'\"), sb_repo.buf);\n+\tjunk_git_dir = xstrdup(sb_repo.buf);\n+\tis_junk = 1;\n \n \t/*\n \t * lock the incomplete repo so prune won't delete it, unlock\n@@ -873,6 +910,7 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,\n \tif (safe_create_leading_directories_const(sb_git.buf))\n \t\tdie_errno(_(\"could not create leading directories of '%s'\"),\n \t\t\t  sb_git.buf);\n+\tjunk_work_tree = xstrdup(path);\n \n \tstrbuf_reset(&sb);\n \tstrbuf_addf(&sb, \"%s/gitdir\", sb_repo.buf);\n@@ -902,9 +940,19 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,\n \tcp.git_cmd = 1;\n \tcp.argv = opts->saved_argv;\n \tret = run_command(&cp);\n+\tif (!ret) {\n+\t\tis_junk = 0;\n+\t\tfree(junk_work_tree);\n+\t\tfree(junk_git_dir);\n+\t\tjunk_work_tree = NULL;\n+\t\tjunk_git_dir = NULL;\n+\t}\n \tstrbuf_reset(&sb);\n \tstrbuf_addf(&sb, \"%s/locked\", sb_repo.buf);\n \tunlink_or_warn(sb.buf);\n+\tstrbuf_release(&sb);\n+\tstrbuf_release(&sb_repo);\n+\tstrbuf_release(&sb_git);\n \treturn ret;\n }\n \ndiff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh\nindex edd3404..e2db078 100755\n--- a/t/t2025-checkout-to.sh\n+++ b/t/t2025-checkout-to.sh\n@@ -17,6 +17,12 @@ test_expect_success 'checkout --to an existing worktree' '\n \ttest_must_fail git checkout --detach --to existing master\n '\n \n+test_expect_success 'checkout --to refuses to checkout locked branch' '\n+\ttest_must_fail git checkout --to zere master &&\n+\t! test -d zere &&\n+\t! test -d .git/worktrees/zere\n+'\n+\n test_expect_success 'checkout --to a new worktree' '\n \tgit rev-parse HEAD >expect &&\n \tgit checkout --detach --to here master &&\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249953","messageId":"1411867366-3821-27-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1411867366-3821-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v3 26/32] gc: style change -- no SP before closing parenthesis","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-28T01:22:40Z","receivedAt":"2014-09-28T01:22:40Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n builtin/gc.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex ced1456..b690929 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -287,7 +287,7 @@ int cmd_gc(int argc, const char **argv, const char *prefix)\n \targv_array_pushl(&pack_refs_cmd, \"pack-refs\", \"--all\", \"--prune\", NULL);\n \targv_array_pushl(&reflog, \"reflog\", \"expire\", \"--all\", NULL);\n \targv_array_pushl(&repack, \"repack\", \"-d\", \"-l\", NULL);\n-\targv_array_pushl(&prune, \"prune\", \"--expire\", NULL );\n+\targv_array_pushl(&prune, \"prune\", \"--expire\", NULL);\n \targv_array_pushl(&rerere, \"rerere\", \"gc\", NULL);\n \n \tgc_config();\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249954","messageId":"1411867366-3821-28-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1411867366-3821-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v3 27/32] gc: factor out gc.pruneexpire parsing code","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-28T01:22:41Z","receivedAt":"2014-09-28T01:22:41Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n builtin/gc.c | 22 ++++++++++++----------\n 1 file changed, 12 insertions(+), 10 deletions(-)\n\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex b690929..849a87c 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -55,6 +55,17 @@ static void remove_pidfile_on_signal(int signo)\n \traise(signo);\n }\n \n+static void git_config_date_string(const char *key, const char **output)\n+{\n+\tif (git_config_get_string_const(key, output))\n+\t\treturn;\n+\tif (strcmp(*output, \"now\")) {\n+\t\tunsigned long now = approxidate(\"now\");\n+\t\tif (approxidate(*output) >= now)\n+\t\t\tgit_die_config(key, _(\"Invalid %s: '%s'\"), key, *output);\n+\t}\n+}\n+\n static void gc_config(void)\n {\n \tconst char *value;\n@@ -71,16 +82,7 @@ static void gc_config(void)\n \tgit_config_get_int(\"gc.auto\", &gc_auto_threshold);\n \tgit_config_get_int(\"gc.autopacklimit\", &gc_auto_pack_limit);\n \tgit_config_get_bool(\"gc.autodetach\", &detach_auto);\n-\n-\tif (!git_config_get_string_const(\"gc.pruneexpire\", &prune_expire)) {\n-\t\tif (strcmp(prune_expire, \"now\")) {\n-\t\t\tunsigned long now = approxidate(\"now\");\n-\t\t\tif (approxidate(prune_expire) >= now) {\n-\t\t\t\tgit_die_config(\"gc.pruneexpire\", _(\"Invalid gc.pruneexpire: '%s'\"),\n-\t\t\t\t\t\tprune_expire);\n-\t\t\t}\n-\t\t}\n-\t}\n+\tgit_config_date_string(\"gc.pruneexpire\", &prune_expire);\n \tgit_config(git_default_config, NULL);\n }\n \n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249955","messageId":"1411867366-3821-29-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1411867366-3821-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v3 28/32] gc: support prune --worktrees","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-28T01:22:42Z","receivedAt":"2014-09-28T01:22:42Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Helped-by: Marc Branchaud <marcnarc@xiplink.com>\nSigned-off-by: Marc Branchaud <marcnarc@xiplink.com>\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n Documentation/config.txt       |  7 +++++++\n Documentation/git-checkout.txt | 11 +++++++----\n builtin/gc.c                   | 10 ++++++++++\n 3 files changed, 24 insertions(+), 4 deletions(-)\n\ndiff --git a/Documentation/config.txt b/Documentation/config.txt\nindex 98b8ef0..8351c8a 100644\n--- a/Documentation/config.txt\n+++ b/Documentation/config.txt\n@@ -1212,6 +1212,13 @@ gc.pruneexpire::\n \t\"now\" may be used to disable this  grace period and always prune\n \tunreachable objects immediately.\n \n+gc.pruneworktreesexpire::\n+\tWhen 'git gc' is run, it will call\n+\t'prune --worktrees --expire 3.months.ago'.\n+\tOverride the grace period with this config variable. The value\n+\t\"now\" may be used to disable the grace period and prune\n+\t$GIT_DIR/worktrees immediately.\n+\n gc.reflogexpire::\n gc.<pattern>.reflogexpire::\n \t'git reflog expire' removes reflog entries older than\ndiff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt\nindex 35675da..0c13825 100644\n--- a/Documentation/git-checkout.txt\n+++ b/Documentation/git-checkout.txt\n@@ -435,8 +435,11 @@ $GIT_DIR or $GIT_COMMON_DIR when you need to directly access something\n inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.\n \n When you are done with a linked working tree you can simply delete it.\n-You can clean up any stale $GIT_DIR/worktrees entries via `git prune\n---worktrees` in the main or any linked working tree.\n+The working tree's entry in the repository's $GIT_DIR/worktrees\n+directory will eventually be removed automatically (see\n+`gc.pruneworktreesexpire` in linkgit::git-config[1]), or you can run\n+`git prune --worktrees` in the main or any linked working tree to\n+clean up any stale entries in $GIT_DIR/worktrees.\n \n If you move a linked working directory to another file system, or\n within a file system that does not support hard links, you need to run\n@@ -444,8 +447,8 @@ at least one git command inside the linked working directory\n (e.g. `git status`) in order to update its entry in $GIT_DIR/worktrees\n so that it does not get automatically removed.\n \n-To prevent `git prune --worktrees` from deleting a $GIT_DIR/worktrees\n-entry (which can be useful in some situations, such as when the\n+To prevent a $GIT_DIR/worktrees entry from from being pruned (which\n+can be useful in some situations, such as when the\n entry's working tree is stored on a portable device), add a file named\n 'locked' to the entry's directory. The file contains the reason in\n plain text. For example, if a linked working tree's `.git` file points\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 849a87c..35542f3 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -33,11 +33,13 @@ static int gc_auto_threshold = 6700;\n static int gc_auto_pack_limit = 50;\n static int detach_auto = 1;\n static const char *prune_expire = \"2.weeks.ago\";\n+static const char *prune_worktrees_expire = \"3.months.ago\";\n \n static struct argv_array pack_refs_cmd = ARGV_ARRAY_INIT;\n static struct argv_array reflog = ARGV_ARRAY_INIT;\n static struct argv_array repack = ARGV_ARRAY_INIT;\n static struct argv_array prune = ARGV_ARRAY_INIT;\n+static struct argv_array prune_worktrees = ARGV_ARRAY_INIT;\n static struct argv_array rerere = ARGV_ARRAY_INIT;\n \n static char *pidfile;\n@@ -83,6 +85,7 @@ static void gc_config(void)\n \tgit_config_get_int(\"gc.autopacklimit\", &gc_auto_pack_limit);\n \tgit_config_get_bool(\"gc.autodetach\", &detach_auto);\n \tgit_config_date_string(\"gc.pruneexpire\", &prune_expire);\n+\tgit_config_date_string(\"gc.pruneworktreesexpire\", &prune_worktrees_expire);\n \tgit_config(git_default_config, NULL);\n }\n \n@@ -290,6 +293,7 @@ int cmd_gc(int argc, const char **argv, const char *prefix)\n \targv_array_pushl(&reflog, \"reflog\", \"expire\", \"--all\", NULL);\n \targv_array_pushl(&repack, \"repack\", \"-d\", \"-l\", NULL);\n \targv_array_pushl(&prune, \"prune\", \"--expire\", NULL);\n+\targv_array_pushl(&prune_worktrees, \"prune\", \"--worktrees\", \"--expire\", NULL);\n \targv_array_pushl(&rerere, \"rerere\", \"gc\", NULL);\n \n \tgc_config();\n@@ -359,6 +363,12 @@ int cmd_gc(int argc, const char **argv, const char *prefix)\n \t\t\treturn error(FAILED_RUN, prune.argv[0]);\n \t}\n \n+\tif (prune_worktrees_expire) {\n+\t\targv_array_push(&prune_worktrees, prune_worktrees_expire);\n+\t\tif (run_command_v_opt(prune_worktrees.argv, RUN_GIT_CMD))\n+\t\t\treturn error(FAILED_RUN, prune_worktrees.argv[0]);\n+\t}\n+\n \tif (run_command_v_opt(rerere.argv, RUN_GIT_CMD))\n \t\treturn error(FAILED_RUN, rerere.argv[0]);\n \n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249957","messageId":"1411867366-3821-30-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1411867366-3821-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v3 29/32] count-objects: report unused files in $GIT_DIR/worktrees/...","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-28T01:22:43Z","receivedAt":"2014-09-28T01:22:43Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"In linked checkouts, borrowed parts like config is taken from\n$GIT_COMMON_DIR. $GIT_DIR/config is never used. Report them as\ngarbage.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n builtin/count-objects.c |  4 +++-\n cache.h                 |  1 +\n path.c                  | 29 +++++++++++++++++++++++++++--\n 3 files changed, 31 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/count-objects.c b/builtin/count-objects.c\nindex a7f70cb..d3a1620 100644\n--- a/builtin/count-objects.c\n+++ b/builtin/count-objects.c\n@@ -102,8 +102,10 @@ int cmd_count_objects(int argc, const char **argv, const char *prefix)\n \t/* we do not take arguments other than flags for now */\n \tif (argc)\n \t\tusage_with_options(count_objects_usage, opts);\n-\tif (verbose)\n+\tif (verbose) {\n \t\treport_garbage = real_report_garbage;\n+\t\treport_linked_checkout_garbage();\n+\t}\n \tmemcpy(path, objdir, len);\n \tif (len && objdir[len-1] != '/')\n \t\tpath[len++] = '/';\ndiff --git a/cache.h b/cache.h\nindex aedfd05..ab3adb6 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -713,6 +713,7 @@ extern const char *mkpath(const char *fmt, ...) __attribute__((format (printf, 1\n extern const char *git_path(const char *fmt, ...) __attribute__((format (printf, 1, 2)));\n extern const char *git_path_submodule(const char *path, const char *fmt, ...)\n \t__attribute__((format (printf, 2, 3)));\n+extern void report_linked_checkout_garbage(void);\n \n /*\n  * Return the name of the file in the local object database that would\ndiff --git a/path.c b/path.c\nindex 72eca6d..cd8e2d6 100644\n--- a/path.c\n+++ b/path.c\n@@ -4,6 +4,7 @@\n #include \"cache.h\"\n #include \"strbuf.h\"\n #include \"string-list.h\"\n+#include \"dir.h\"\n \n static int get_st_mode_bits(const char *path, int *mode)\n {\n@@ -91,9 +92,9 @@ static void replace_dir(struct strbuf *buf, int len, const char *newdir)\n }\n \n static const char *common_list[] = {\n-\t\"/branches\", \"/hooks\", \"/info\", \"/logs\", \"/lost-found\", \"/modules\",\n+\t\"/branches\", \"/hooks\", \"/info\", \"!/logs\", \"/lost-found\", \"/modules\",\n \t\"/objects\", \"/refs\", \"/remotes\", \"/worktrees\", \"/rr-cache\", \"/svn\",\n-\t\"config\", \"gc.pid\", \"packed-refs\", \"shallow\",\n+\t\"config\", \"!gc.pid\", \"packed-refs\", \"shallow\",\n \tNULL\n };\n \n@@ -107,6 +108,8 @@ static void update_common_dir(struct strbuf *buf, int git_dir_len)\n \tfor (p = common_list; *p; p++) {\n \t\tconst char *path = *p;\n \t\tint is_dir = 0;\n+\t\tif (*path == '!')\n+\t\t\tpath++;\n \t\tif (*path == '/') {\n \t\t\tpath++;\n \t\t\tis_dir = 1;\n@@ -122,6 +125,28 @@ static void update_common_dir(struct strbuf *buf, int git_dir_len)\n \t}\n }\n \n+void report_linked_checkout_garbage(void)\n+{\n+\tstruct strbuf sb = STRBUF_INIT;\n+\tconst char **p;\n+\tint len;\n+\n+\tif (!git_common_dir_env)\n+\t\treturn;\n+\tstrbuf_addf(&sb, \"%s/\", get_git_dir());\n+\tlen = sb.len;\n+\tfor (p = common_list; *p; p++) {\n+\t\tconst char *path = *p;\n+\t\tif (*path == '!')\n+\t\t\tcontinue;\n+\t\tstrbuf_setlen(&sb, len);\n+\t\tstrbuf_addstr(&sb, path);\n+\t\tif (file_exists(sb.buf))\n+\t\t\treport_garbage(\"unused in linked checkout\", sb.buf);\n+\t}\n+\tstrbuf_release(&sb);\n+}\n+\n static void adjust_git_path(struct strbuf *buf, int git_dir_len)\n {\n \tconst char *base = buf->buf + git_dir_len;\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249956","messageId":"1411867366-3821-31-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1411867366-3821-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v3 30/32] git_path(): keep \"info/sparse-checkout\" per work-tree","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-28T01:22:44Z","receivedAt":"2014-09-28T01:22:44Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Currently git_path(\"info/sparse-checkout\") resolves to\n$GIT_COMMON_DIR/info/sparse-checkout in multiple worktree mode. It\nmakes more sense for the sparse checkout patterns to be per worktree,\nso you can have multiple checkouts with different parts of the tree.\n\nWith this, \"git checkout --to <new>\" on a sparse checkout will create\n<new> as a full checkout. Which is expected, it's how a new checkout\nis made. The user can reshape the worktree afterwards.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n path.c                | 3 ++-\n t/t0060-path-utils.sh | 1 +\n 2 files changed, 3 insertions(+), 1 deletion(-)\n\ndiff --git a/path.c b/path.c\nindex cd8e2d6..35d498e 100644\n--- a/path.c\n+++ b/path.c\n@@ -103,7 +103,8 @@ static void update_common_dir(struct strbuf *buf, int git_dir_len)\n \tchar *base = buf->buf + git_dir_len;\n \tconst char **p;\n \n-\tif (is_dir_file(base, \"logs\", \"HEAD\"))\n+\tif (is_dir_file(base, \"logs\", \"HEAD\") ||\n+\t    is_dir_file(base, \"info\", \"sparse-checkout\"))\n \t\treturn;\t/* keep this in $GIT_DIR */\n \tfor (p = common_list; *p; p++) {\n \t\tconst char *path = *p;\ndiff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh\nindex f5d6f80..93605f4 100755\n--- a/t/t0060-path-utils.sh\n+++ b/t/t0060-path-utils.sh\n@@ -270,6 +270,7 @@ test_git_path GIT_COMMON_DIR=bar objects                  bar/objects\n test_git_path GIT_COMMON_DIR=bar objects/bar              bar/objects/bar\n test_git_path GIT_COMMON_DIR=bar info/exclude             bar/info/exclude\n test_git_path GIT_COMMON_DIR=bar info/grafts              bar/info/grafts\n+test_git_path GIT_COMMON_DIR=bar info/sparse-checkout     .git/info/sparse-checkout\n test_git_path GIT_COMMON_DIR=bar remotes/bar              bar/remotes/bar\n test_git_path GIT_COMMON_DIR=bar branches/bar             bar/branches/bar\n test_git_path GIT_COMMON_DIR=bar logs/refs/heads/master   bar/logs/refs/heads/master\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249958","messageId":"1411867366-3821-32-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1411867366-3821-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v3 31/32] checkout: don't require a work tree when checking out into a new one","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-28T01:22:45Z","receivedAt":"2014-09-28T01:22:45Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"From: Dennis Kaarsemaker <dennis@kaarsemaker.net>\n\nFor normal use cases, it does not make sense for 'checkout' to work on\na bare repository, without a worktree. But \"checkout --to\" is an\nexception because it _creates_ a new worktree. Allow this option to\nrun on bare repositories.\n\nPeople who check out from a bare repository should remember that\ncore.logallrefupdates is off by default and it should be turned back\non. `--to` cannot do this automatically behind the user's back because\nsome user may deliberately want no reflog.\n\nFor people interested in repository setup/discovery code,\nis_bare_repository_cfg (aka \"core.bare\") is unchanged by this patch,\nwhich means 'true' by default for bare repos. Fortunately when we get\nthe repo through a linked checkout, is_bare_repository_cfg is never\nused. So all is still good.\n\n[nd: commit message]\n\nSigned-off-by: Dennis Kaarsemaker <dennis@kaarsemaker.net>\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n builtin/checkout.c     |  3 +++\n git.c                  |  2 +-\n t/t2025-checkout-to.sh | 15 +++++++++++++++\n 3 files changed, 19 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex bd0e2d5..01d0f2f 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -1364,6 +1364,9 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)\n \tif (opts.new_worktree_mode)\n \t\topts.new_worktree = NULL;\n \n+\tif (!opts.new_worktree)\n+\t\tsetup_work_tree();\n+\n \tif (conflict_style) {\n \t\topts.merge = 1; /* implied */\n \t\tgit_xmerge_config(\"merge.conflictstyle\", conflict_style, NULL);\ndiff --git a/git.c b/git.c\nindex 4076b01..6c1ac0c 100644\n--- a/git.c\n+++ b/git.c\n@@ -383,7 +383,7 @@ static struct cmd_struct commands[] = {\n \t{ \"check-ignore\", cmd_check_ignore, RUN_SETUP | NEED_WORK_TREE },\n \t{ \"check-mailmap\", cmd_check_mailmap, RUN_SETUP },\n \t{ \"check-ref-format\", cmd_check_ref_format },\n-\t{ \"checkout\", cmd_checkout, RUN_SETUP | NEED_WORK_TREE },\n+\t{ \"checkout\", cmd_checkout, RUN_SETUP },\n \t{ \"checkout-index\", cmd_checkout_index,\n \t\tRUN_SETUP | NEED_WORK_TREE},\n \t{ \"cherry\", cmd_cherry, RUN_SETUP },\ndiff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh\nindex e2db078..4bd1df4 100755\n--- a/t/t2025-checkout-to.sh\n+++ b/t/t2025-checkout-to.sh\n@@ -81,4 +81,19 @@ test_expect_success 'not die on re-checking out current branch' '\n \t)\n '\n \n+test_expect_success 'checkout --to from a bare repo' '\n+\t(\n+\t\tgit clone --bare . bare &&\n+\t\tcd bare &&\n+\t\tgit checkout --to ../there2 -b bare-master master\n+\t)\n+'\n+\n+test_expect_success 'checkout from a bare repo without --to' '\n+\t(\n+\t\tcd bare &&\n+\t\ttest_must_fail git checkout master\n+\t)\n+'\n+\n test_done\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249959","messageId":"1411867366-3821-33-git-send-email-pclouds@gmail.com","threadId":"37450","inReplyTo":"1411867366-3821-1-git-send-email-pclouds@gmail.com","subject":"[PATCH v3 32/32] t2025: add a test to make sure grafts is working from a linked checkout","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2014-09-28T01:22:46Z","receivedAt":"2014-09-28T01:22:46Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n t/t2025-checkout-to.sh | 18 ++++++++++++++++++\n 1 file changed, 18 insertions(+)\n\ndiff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh\nindex 4bd1df4..eddd325 100755\n--- a/t/t2025-checkout-to.sh\n+++ b/t/t2025-checkout-to.sh\n@@ -96,4 +96,22 @@ test_expect_success 'checkout from a bare repo without --to' '\n \t)\n '\n \n+test_expect_success 'checkout with grafts' '\n+\ttest_when_finished rm .git/info/grafts &&\n+\ttest_commit abc &&\n+\tSHA1=`git rev-parse HEAD` &&\n+\ttest_commit def &&\n+\ttest_commit xyz &&\n+\techo \"`git rev-parse HEAD` $SHA1\" >.git/info/grafts &&\n+\tcat >expected <<-\\EOF &&\n+\txyz\n+\tabc\n+\tEOF\n+\tgit log --format=%s -2 >actual &&\n+\ttest_cmp expected actual &&\n+\tgit checkout --detach --to grafted master &&\n+\tgit --git-dir=grafted/.git log --format=%s -2 >actual &&\n+\ttest_cmp expected actual\n+'\n+\n test_done\n-- \n2.1.0.rc0.78.gc0d8480\n"},{"id":"249960","messageId":"xmqqr3yw6ym6.fsf@gitster.dls.corp.google.com","threadId":"37450","inReplyTo":"1411867366-3821-1-git-send-email-pclouds@gmail.com","subject":"Re: [PATCH v3 00/32] nd/multiple-work-trees","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2014-09-28T06:35:45Z","receivedAt":"2014-09-28T06:35:45Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Nguyễn Thái Ngọc Duy  <pclouds@gmail.com> writes:\n\n> On Fri, Sep 26, 2014 at 4:20 AM, Junio C Hamano <gitster@pobox.com> wrote:\n>> It has been a while since the last review exchanges were seen.  Will\n>> it be time for v3 soon?\n>\n> Sorry I've been slow on picking up feedback from v2. v3 is rebased on\n> latest master. Other changes are mostly *.txt, and one broken &&\n> chain.\n\nAs there have been quite a lot of changes in overlapping areas\n(e.g. with Ronnie and Jonathan's refs.c clean-up series, and also\nwith Tanay's config API changes in builtin/gc.c), I agree that it no\nlonger makes sense to stick to the old fork point.\n\nThis applies cleanly to the 'master', the result merges cleanly to\nwhere the old series were merged to 'pu', and the difference between\nthe old/new \"pu^{/^Merge branch 'nd/multiple-work-trees' into pu}\"\ncommits matches what I remember from the review discussion you were\nhaving on the old series with others (attached below for others to\neyeball as a rough substitute for an interdiff).\n\nThanks.\n\ndiff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt\nindex 23f0c80..0c13825 100644\n--- a/Documentation/git-checkout.txt\n+++ b/Documentation/git-checkout.txt\n@@ -229,8 +229,8 @@ section of linkgit:git-add[1] to learn how to operate the `--patch` mode.\n \tCheck out a branch in a separate working directory at\n \t`<path>`. A new working directory is linked to the current\n \trepository, sharing everything except working directory\n-\tspecific files such as HEAD, index... See \"MULTIPLE CHECKOUT\n-\tMODE\" section for more information.\n+\tspecific files such as HEAD, index... See \"MULTIPLE WORKING\n+\tTREES\" section for more information.\n \n <branch>::\n \tBranch to checkout; if it refers to a branch (i.e., a name that,\n@@ -395,58 +395,66 @@ $ git reflog -2 HEAD # or\n $ git log -g -2 HEAD\n ------------\n \n-MULTIPLE CHECKOUT MODE\n+MULTIPLE WORKING TREES\n ----------------------\n-Normally a working directory is attached to repository. When \"git\n-checkout --to\" is used, a new working directory is attached to the\n-current repository. This new working directory is called \"linked\n-checkout\" as compared to the \"main checkout\" prepared by \"git init\" or\n-\"git clone\". A repository has one main checkout and zero or more\n-linked checkouts.\n-\n-Each linked checkout has a private directory in $GIT_DIR/worktrees in\n-the main checkout, usually named after the base name of the new\n-working directory, optionally with a number added to make it\n-unique. For example, the command `git checkout --to ../test-next next`\n-running with $GIT_DIR=/path/main may create the directory\n-`$GIT_DIR/worktrees/test-next` (or `$GIT_DIR/worktrees/test-next1` if\n-`test-next` is already taken).\n-\n-Within a linked checkout, $GIT_DIR is set to point to this private\n-directory (e.g. `/path/main/worktrees/test-next` in the example) and\n-$GIT_COMMON_DIR is set to point back to the main checkout's $GIT_DIR\n-(e.g. `/path/main`). Setting is done via a .git file located at the\n-top directory of the linked checkout.\n-\n-Path resolution via `git rev-parse --git-path` would use either\n-$GIT_DIR or $GIT_COMMON_DIR depending on the path. For example, the\n-linked checkout's `$GIT_DIR/HEAD` resolve to\n-`/path/main/worktrees/test-next/HEAD` (not `/path/main/HEAD` which is\n-the main checkout's HEAD) while `$GIT_DIR/refs/heads/master` would use\n-$GIT_COMMON_DIR and resolve to `/path/main/refs/heads/master`, which\n-is shared across checkouts.\n+\n+A git repository can support multiple working trees, allowing you to check\n+out more than one branch at a time.  With `git checkout --to` a new working\n+tree is associated with the repository.  This new working tree is called a\n+\"linked working tree\" as opposed to the \"main working tree\" prepared by \"git\n+init\" or \"git clone\".  A repository has one main working tree (if it's not a\n+bare repository) and zero or more linked working trees.\n+\n+Each linked working tree has a private sub-directory in the repository's\n+$GIT_DIR/worktrees directory.  The private sub-directory's name is usually\n+the base name of the linked working tree's path, possibly appended with a\n+number to make it unique.  For example, when `$GIT_DIR=/path/main/.git` the\n+command `git checkout --to /path/other/test-next next` creates the linked\n+working tree in `/path/other/test-next` and also creates a\n+`$GIT_DIR/worktrees/test-next` directory (or `$GIT_DIR/worktrees/test-next1`\n+if `test-next` is already taken).\n+\n+Within a linked working tree, $GIT_DIR is set to point to this private\n+directory (e.g. `/path/main/.git/worktrees/test-next` in the example) and\n+$GIT_COMMON_DIR is set to point back to the main working tree's $GIT_DIR\n+(e.g. `/path/main/.git`). These settings are made in a `.git` file located at\n+the top directory of the linked working tree.\n+\n+Path resolution via `git rev-parse --git-path` uses either\n+$GIT_DIR or $GIT_COMMON_DIR depending on the path. For example, in the\n+linked working tree `git rev-parse --git-path HEAD` returns\n+`/path/main/.git/worktrees/test-next/HEAD` (not\n+`/path/other/test-next/.git/HEAD` or `/path/main/.git/HEAD`) while `git\n+rev-parse --git-path refs/heads/master` uses\n+$GIT_COMMON_DIR and returns `/path/main/.git/refs/heads/master`,\n+since refs are shared across all working trees.\n \n See linkgit:gitrepository-layout[5] for more information. The rule of\n thumb is do not make any assumption about whether a path belongs to\n $GIT_DIR or $GIT_COMMON_DIR when you need to directly access something\n inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.\n \n-When you are done, simply deleting the linked working directory would\n-suffice. $GIT_DIR/worktrees can be cleaned up using `git prune\n---worktrees`, which is part of automated garbage collection.\n-\n-After you move a linked working directory to another file system, or\n-on a file system that does not support hard link, execute any git\n-command (e.g. `git status`) in the new working directory so that it\n-could update its location in $GIT_DIR/worktrees and not be\n-accidentally pruned.\n-\n-To stop `git prune --worktrees` from deleting a specific working\n-directory (e.g. because it's on a portable device), you could add the\n-file 'locked' to $GIT_DIR/worktrees. For example, if `.git` file of\n-the new working directory points to `/path/main/worktrees/test-next`,\n-the full path of the 'locked' file would be\n-`/path/main/worktrees/test-next/locked`. See\n+When you are done with a linked working tree you can simply delete it.\n+The working tree's entry in the repository's $GIT_DIR/worktrees\n+directory will eventually be removed automatically (see\n+`gc.pruneworktreesexpire` in linkgit::git-config[1]), or you can run\n+`git prune --worktrees` in the main or any linked working tree to\n+clean up any stale entries in $GIT_DIR/worktrees.\n+\n+If you move a linked working directory to another file system, or\n+within a file system that does not support hard links, you need to run\n+at least one git command inside the linked working directory\n+(e.g. `git status`) in order to update its entry in $GIT_DIR/worktrees\n+so that it does not get automatically removed.\n+\n+To prevent a $GIT_DIR/worktrees entry from from being pruned (which\n+can be useful in some situations, such as when the\n+entry's working tree is stored on a portable device), add a file named\n+'locked' to the entry's directory. The file contains the reason in\n+plain text. For example, if a linked working tree's `.git` file points\n+to `/path/main/.git/worktrees/test-next` then a file named\n+`/path/main/.git/worktrees/test-next/locked` will prevent the\n+`test-next` entry from being pruned.  See\n linkgit:gitrepository-layout[5] for details.\n \n EXAMPLES\ndiff --git a/Documentation/git-prune.txt b/Documentation/git-prune.txt\nindex a0ea381..1cf3bed 100644\n--- a/Documentation/git-prune.txt\n+++ b/Documentation/git-prune.txt\n@@ -49,7 +49,7 @@ OPTIONS\n \tOnly expire loose objects older than <time>.\n \n --worktrees::\n-\tPrune dead worktree information in $GIT_DIR/worktrees.\n+\tPrune dead working tree information in $GIT_DIR/worktrees.\n \n <head>...::\n \tIn addition to objects\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 125f0dd..35542f3 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -57,12 +57,14 @@ static void remove_pidfile_on_signal(int signo)\n \traise(signo);\n }\n \n-static void git_config_get_date_string(const char *var, const char **out)\n+static void git_config_date_string(const char *key, const char **output)\n {\n-\tif (!git_config_get_string_const(var, out) && strcmp(*out, \"now\")) {\n+\tif (git_config_get_string_const(key, output))\n+\t\treturn;\n+\tif (strcmp(*output, \"now\")) {\n \t\tunsigned long now = approxidate(\"now\");\n-\t\tif (approxidate(*out) >= now)\n-\t\t\tgit_die_config(var, _(\"Invalid %s: '%s'\"), var, *out);\n+\t\tif (approxidate(*output) >= now)\n+\t\t\tgit_die_config(key, _(\"Invalid %s: '%s'\"), key, *output);\n \t}\n }\n \n@@ -82,8 +84,8 @@ static void gc_config(void)\n \tgit_config_get_int(\"gc.auto\", &gc_auto_threshold);\n \tgit_config_get_int(\"gc.autopacklimit\", &gc_auto_pack_limit);\n \tgit_config_get_bool(\"gc.autodetach\", &detach_auto);\n-\tgit_config_get_date_string(\"gc.pruneexpire\", &prune_expire);\n-\tgit_config_get_date_string(\"gc.pruneworktreesexpire\", &prune_worktrees_expire);\n+\tgit_config_date_string(\"gc.pruneexpire\", &prune_expire);\n+\tgit_config_date_string(\"gc.pruneworktreesexpire\", &prune_worktrees_expire);\n \tgit_config(git_default_config, NULL);\n }\n \ndiff --git a/refs.c b/refs.c\nindex c4850f5..33ac01c 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -2935,12 +2935,14 @@ static int log_ref_write(const char *refname, const unsigned char *old_sha1,\n \t\terror(\"Unable to append to %s\", log_file);\n \t\terrno = save_errno;\n \t\tresult = -1;\n+\t\tgoto done;\n \t}\n \tif (close(logfd)) {\n \t\tint save_errno = errno;\n \t\terror(\"Unable to append to %s\", log_file);\n \t\terrno = save_errno;\n \t\tresult = -1;\n+\t\tgoto done;\n \t}\n done:\n \tstrbuf_release(&sb_log_file);\ndiff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh\nindex 27384a1..eddd325 100755\n--- a/t/t2025-checkout-to.sh\n+++ b/t/t2025-checkout-to.sh\n@@ -49,7 +49,7 @@ test_expect_success 'checkout --to a new worktree from a subdir' '\n test_expect_success 'checkout --to from a linked checkout' '\n \t(\n \t\tcd here &&\n-\t\tgit checkout --detach --to nested-here master\n+\t\tgit checkout --detach --to nested-here master &&\n \t\tcd nested-here &&\n \t\tgit fsck\n \t)\ndiff --git a/wrapper.c b/wrapper.c\nindex 6f40341..e821d05 100644\n--- a/wrapper.c\n+++ b/wrapper.c\n@@ -531,6 +531,14 @@ struct passwd *xgetpwuid_self(void)\n \treturn pw;\n }\n \n+char *xgetcwd(void)\n+{\n+\tstruct strbuf sb = STRBUF_INIT;\n+\tif (strbuf_getcwd(&sb))\n+\t\tdie_errno(_(\"unable to get current working directory\"));\n+\treturn strbuf_detach(&sb, NULL);\n+}\n+\n int write_file(const char *path, int fatal, const char *fmt, ...)\n {\n \tstruct strbuf sb = STRBUF_INIT;\n@@ -561,11 +569,3 @@ int write_file(const char *path, int fatal, const char *fmt, ...)\n \t}\n \treturn 0;\n }\n-\n-char *xgetcwd(void)\n-{\n-\tstruct strbuf sb = STRBUF_INIT;\n-\tif (strbuf_getcwd(&sb))\n-\t\tdie_errno(_(\"unable to get current working directory\"));\n-\treturn strbuf_detach(&sb, NULL);\n-}\n"},{"id":"250121","messageId":"542C2988.6060306@kdbg.org","threadId":"37450","inReplyTo":"1411867366-3821-20-git-send-email-pclouds@gmail.com","subject":"Re: [PATCH v3 19/32] setup.c: support multi-checkout repo setup","fromName":"Johannes Sixt","fromEmail":"j6t@kdbg.org","sentAt":"2014-10-01T16:19:20Z","receivedAt":"2014-10-01T16:19:20Z","isPatch":true,"sender":{"key":"j6t@kdbg.org","avatar":"https://avatars.githubusercontent.com/u/14810926?v=4"},"body":"Am 28.09.2014 um 03:22 schrieb Nguyễn Thái Ngọc Duy:\n> +test_expect_success 'GIT_DIR set (2)' '\n> +\techo \"gitdir: repo.git/repos/foo\" >gitfile &&\n> +\techo \"$TRASH_DIRECTORY/repo.git\" >repo.git/repos/foo/commondir &&\n> +\t(\n> +\t\tcd work &&\n> +\t\tGIT_DIR=../gitfile git rev-parse --git-common-dir >actual &&\n> +\t\ttest-path-utils real_path \"$TRASH_DIRECTORY/repo.git\" >expect &&\n> +\t\ttest_cmp expect actual\n> +\t)\n> +'\n\nThis requires the following fixup because MinGW git will understand\nonly DOS style absolute paths, but $TRASH_DIRECTORY is in\nPOSIX-MSYS-style /c/foo/bar.\n\n--- 8< ---\nSubject: [PATCH] fixup! setup.c: support multi-checkout repo setup\n\nSigned-off-by: Johannes Sixt <j6t@kdbg.org>\n---\n t/t1501-worktree.sh | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/t/t1501-worktree.sh b/t/t1501-worktree.sh\nindex e6ac7a4..4df7a2f 100755\n--- a/t/t1501-worktree.sh\n+++ b/t/t1501-worktree.sh\n@@ -366,7 +366,7 @@ test_expect_success 'GIT_DIR set (1)' '\n \n test_expect_success 'GIT_DIR set (2)' '\n \techo \"gitdir: repo.git/repos/foo\" >gitfile &&\n-\techo \"$TRASH_DIRECTORY/repo.git\" >repo.git/repos/foo/commondir &&\n+\techo \"$(pwd)/repo.git\" >repo.git/repos/foo/commondir &&\n \t(\n \t\tcd work &&\n \t\tGIT_DIR=../gitfile git rev-parse --git-common-dir >actual &&\n-- \n2.0.0.12.gbcf935e\n"},{"id":"250122","messageId":"542C29D3.7020205@kdbg.org","threadId":"37450","inReplyTo":"1411867366-3821-24-git-send-email-pclouds@gmail.com","subject":"Re: [PATCH v3 23/32] prune: strategies for linked checkouts","fromName":"Johannes Sixt","fromEmail":"j6t@kdbg.org","sentAt":"2014-10-01T16:20:35Z","receivedAt":"2014-10-01T16:20:35Z","isPatch":true,"sender":{"key":"j6t@kdbg.org","avatar":"https://avatars.githubusercontent.com/u/14810926?v=4"},"body":"Am 28.09.2014 um 03:22 schrieb Nguyễn Thái Ngọc Duy:\n> +test_expect_success 'prune directories with gitdir pointing to nowhere' '\n> +\tmkdir -p .git/worktrees/def/abc &&\n> +\t: >.git/worktrees/def/def &&\n> +\techo \"$TRASH_DIRECTORY\"/nowhere >.git/worktrees/def/gitdir &&\n> +\tgit prune --worktrees --verbose >actual &&\n> +\ttest_i18ngrep \"Removing worktrees/def: gitdir file points to non-existent location\" actual &&\n> +\t! test -d .git/worktrees/def &&\n> +\t! test -d .git/worktrees\n> +'\n> ...\n> +test_expect_success 'not prune recent checkouts' '\n> +\ttest_when_finished rm -r .git/worktrees\n> +\tmkdir zz &&\n> +\tmkdir -p .git/worktrees/jlm &&\n> +\techo \"$TRASH_DIRECTORY\"/zz >.git/worktrees/jlm/gitdir &&\n> +\tgit prune --worktrees --verbose --expire=2.days.ago &&\n> +\ttest -d .git/worktrees/jlm\n> +'\n\nThese require the following fixups because MinGW git will understand\nonly DOS style absolute paths, but $TRASH_DIRECTORY is in\nPOSIX-MSYS-style /c/foo/bar.\n\n--- 8< ---\nSubject: [PATCH] fixup! prune: strategies for linked checkouts\n\nSigned-off-by: Johannes Sixt <j6t@kdbg.org>\n---\n t/t2026-prune-linked-checkouts.sh | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/t/t2026-prune-linked-checkouts.sh b/t/t2026-prune-linked-checkouts.sh\nindex 3622800..170aefe 100755\n--- a/t/t2026-prune-linked-checkouts.sh\n+++ b/t/t2026-prune-linked-checkouts.sh\n@@ -57,7 +57,7 @@ test_expect_success 'prune directories with invalid gitdir' '\n test_expect_success 'prune directories with gitdir pointing to nowhere' '\n \tmkdir -p .git/worktrees/def/abc &&\n \t: >.git/worktrees/def/def &&\n-\techo \"$TRASH_DIRECTORY\"/nowhere >.git/worktrees/def/gitdir &&\n+\techo \"$(pwd)\"/nowhere >.git/worktrees/def/gitdir &&\n \tgit prune --worktrees --verbose >actual &&\n \ttest_i18ngrep \"Removing worktrees/def: gitdir file points to non-existent location\" actual &&\n \t! test -d .git/worktrees/def &&\n@@ -76,7 +76,7 @@ test_expect_success 'not prune recent checkouts' '\n \ttest_when_finished rm -r .git/worktrees\n \tmkdir zz &&\n \tmkdir -p .git/worktrees/jlm &&\n-\techo \"$TRASH_DIRECTORY\"/zz >.git/worktrees/jlm/gitdir &&\n+\techo \"$(pwd)\"/zz >.git/worktrees/jlm/gitdir &&\n \tgit prune --worktrees --verbose --expire=2.days.ago &&\n \ttest -d .git/worktrees/jlm\n '\n-- \n2.0.0.12.gbcf935e\n"}]}