git@vger.kernel.org mailing list mirror (one of many)
 help / color / mirror / code / Atom feed
* [PATCH 00/32] nd/multiple-work-trees cleanup
@ 2014-08-30  8:33 Nguyễn Thái Ngọc Duy
  2014-08-30  8:33 ` [PATCH 01/32] path.c: make get_pathname() return strbuf instead of static buffer Nguyễn Thái Ngọc Duy
                   ` (34 more replies)
  0 siblings, 35 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-08-30  8:33 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

This collapes some bug fix patches into the main ones, adds a few more
tests to cover recent changes, and removes advice.checkoutLocked (when
things are controversal, probably best to go without them until they
are settled). Diff against current version in 'pu'

diff --git a/Documentation/config.txt b/Documentation/config.txt
index b2c3388..57999fa 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -201,10 +201,6 @@ advice.*::
 	rmHints::
 		In case of failure in the output of linkgit:git-rm[1],
 		show directions on how to proceed from the current state.
-	checkoutLocked::
-		In multiple checkout setup, attempting to checkout a
-		branch already checked out elsewhere will fail. Show
-		some useful options to proceed.
 --
 
 core.fileMode::
diff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt
index 6bd82af..c9a7d1a 100644
--- a/Documentation/gitrepository-layout.txt
+++ b/Documentation/gitrepository-layout.txt
@@ -139,8 +139,8 @@ is often called 'detached HEAD.'  See linkgit:git-checkout[1]
 for details.
 
 config::
-	Repository specific configuration file. This file is ignored if
-	$GIT_COMMON_DIR is set and "$GIT_COMMON_DIR/config" will be
+	Repository specific configuration file. This file is ignored
+	if $GIT_COMMON_DIR is set and "$GIT_COMMON_DIR/config" will be
 	used instead.
 
 branches::
@@ -218,11 +218,10 @@ remotes::
 	"$GIT_COMMON_DIR/remotes" will be used instead.
 
 logs::
-	Records of changes made to refs are stored in this
-	directory.  See linkgit:git-update-ref[1]
-	for more information. This directory is ignored if
-	$GIT_COMMON_DIR is set and "$GIT_COMMON_DIR/logs" will be used
-	instead.
+	Records of changes made to refs are stored in this directory.
+	See linkgit:git-update-ref[1] for more information. This
+	directory is ignored $GIT_COMMON_DIR is set and
+	"$GIT_COMMON_DIR/logs" will be used instead.
 
 logs/refs/heads/`name`::
 	Records all changes made to the branch tip named `name`.
diff --git a/advice.c b/advice.c
index cf3b0f7..9b42033 100644
--- a/advice.c
+++ b/advice.c
@@ -15,7 +15,6 @@ int advice_detached_head = 1;
 int advice_set_upstream_failure = 1;
 int advice_object_name_warning = 1;
 int advice_rm_hints = 1;
-int advice_checkout_locked = 1;
 
 static struct {
 	const char *name;
@@ -36,7 +35,6 @@ static struct {
 	{ "setupstreamfailure", &advice_set_upstream_failure },
 	{ "objectnamewarning", &advice_object_name_warning },
 	{ "rmhints", &advice_rm_hints },
-	{ "checkoutlocked", &advice_checkout_locked },
 
 	/* make this an alias for backward compatibility */
 	{ "pushnonfastforward", &advice_push_update_rejected }
diff --git a/advice.h b/advice.h
index 935631d..5ecc6c1 100644
--- a/advice.h
+++ b/advice.h
@@ -18,7 +18,6 @@ extern int advice_detached_head;
 extern int advice_set_upstream_failure;
 extern int advice_object_name_warning;
 extern int advice_rm_hints;
-extern int advice_checkout_locked;
 
 int git_default_advice_config(const char *var, const char *value);
 __attribute__((format (printf, 1, 2)))
diff --git a/builtin/checkout.c b/builtin/checkout.c
index 3dc416c..b88c646 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -862,7 +862,7 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
 	if (!new->commit)
 		die(_("no branch specified"));
 	if (file_exists(path))
-		die(_("%s already exists"), path);
+		die(_("'%s' already exists"), path);
 
 	len = strlen(path);
 	while (len && is_dir_sep(path[len - 1]))
@@ -1038,16 +1038,7 @@ static void check_linked_checkout(struct branch_info *new, const char *id)
 		strbuf_rtrim(&gitdir);
 	} else
 		strbuf_addstr(&gitdir, get_git_common_dir());
-	if (advice_checkout_locked)
-		die(_("'%s' is already checked out at %s\n"
-		      "Either go there and continue working, or detach HEAD using\n"
-		      "    git checkout --detach [more options] %s\n"
-		      "or create a new branch based off '%s' using\n"
-		      "    git checkout -b <branch name> [more options] %s\n"
-		      "or switch to another branch at the other checkout and retry."),
-		    new->name, gitdir.buf, new->name, new->name, new->name);
-	else
-		die(_("'%s' is already checked out at %s"), new->name, gitdir.buf);
+	die(_("'%s' is already checked out at '%s'"), new->name, gitdir.buf);
 done:
 	strbuf_release(&path);
 	strbuf_release(&sb);
diff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh
index 508993f..aa24ea8 100755
--- a/t/t2025-checkout-to.sh
+++ b/t/t2025-checkout-to.sh
@@ -12,6 +12,11 @@ test_expect_success 'checkout --to not updating paths' '
 	test_must_fail git checkout --to -- init.t
 '
 
+test_expect_success 'checkout --to an existing worktree' '
+	mkdir existing &&
+	test_must_fail git checkout --detach --to existing master
+'
+
 test_expect_success 'checkout --to refuses to checkout locked branch' '
 	test_must_fail git checkout --to zere master &&
 	! test -d zere &&
@@ -31,6 +36,16 @@ test_expect_success 'checkout --to a new worktree' '
 	)
 '
 
+test_expect_success 'checkout --to a new worktree from a subdir' '
+	(
+		mkdir sub &&
+		cd sub &&
+		git checkout --detach --to here master &&
+		cd here &&
+		test_cmp ../../init.t init.t
+	)
+'
+
 test_expect_success 'checkout --to from a linked checkout' '
 	(
 		cd here &&
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 01/32] path.c: make get_pathname() return strbuf instead of static buffer
  2014-08-30  8:33 [PATCH 00/32] nd/multiple-work-trees cleanup Nguyễn Thái Ngọc Duy
@ 2014-08-30  8:33 ` Nguyễn Thái Ngọc Duy
  2014-08-30  8:33 ` [PATCH 02/32] path.c: make get_pathname() call sites return const char * Nguyễn Thái Ngọc Duy
                   ` (33 subsequent siblings)
  34 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-08-30  8:33 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

We've been avoiding PATH_MAX whenever possible. This patch makes
get_pathname() return a strbuf and updates the callers to take
advantage of this. The code is simplified as we no longer need to
worry about buffer overflow.

vsnpath() behavior is changed slightly: previously it always clears
the buffer before writing, now it just appends. Fortunately this is a
static function and all of its callers prepare the buffer properly:
git_path() gets the buffer from get_pathname() which resets the
buffer, the remaining call sites start with STRBUF_INIT'd buffer.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 path.c | 120 ++++++++++++++++++++++++++++-------------------------------------
 1 file changed, 51 insertions(+), 69 deletions(-)

diff --git a/path.c b/path.c
index bc804a3..42ef3af 100644
--- a/path.c
+++ b/path.c
@@ -16,11 +16,15 @@ static int get_st_mode_bits(const char *path, int *mode)
 
 static char bad_path[] = "/bad-path/";
 
-static char *get_pathname(void)
+static struct strbuf *get_pathname(void)
 {
-	static char pathname_array[4][PATH_MAX];
+	static struct strbuf pathname_array[4] = {
+		STRBUF_INIT, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT
+	};
 	static int index;
-	return pathname_array[3 & ++index];
+	struct strbuf *sb = &pathname_array[3 & ++index];
+	strbuf_reset(sb);
+	return sb;
 }
 
 static char *cleanup_path(char *path)
@@ -34,6 +38,13 @@ static char *cleanup_path(char *path)
 	return path;
 }
 
+static void strbuf_cleanup_path(struct strbuf *sb)
+{
+	char *path = cleanup_path(sb->buf);
+	if (path > sb->buf)
+		strbuf_remove(sb, 0, path - sb->buf);
+}
+
 char *mksnpath(char *buf, size_t n, const char *fmt, ...)
 {
 	va_list args;
@@ -49,85 +60,70 @@ char *mksnpath(char *buf, size_t n, const char *fmt, ...)
 	return cleanup_path(buf);
 }
 
-static char *vsnpath(char *buf, size_t n, const char *fmt, va_list args)
+static void vsnpath(struct strbuf *buf, const char *fmt, va_list args)
 {
 	const char *git_dir = get_git_dir();
-	size_t len;
-
-	len = strlen(git_dir);
-	if (n < len + 1)
-		goto bad;
-	memcpy(buf, git_dir, len);
-	if (len && !is_dir_sep(git_dir[len-1]))
-		buf[len++] = '/';
-	len += vsnprintf(buf + len, n - len, fmt, args);
-	if (len >= n)
-		goto bad;
-	return cleanup_path(buf);
-bad:
-	strlcpy(buf, bad_path, n);
-	return buf;
+	strbuf_addstr(buf, git_dir);
+	if (buf->len && !is_dir_sep(buf->buf[buf->len - 1]))
+		strbuf_addch(buf, '/');
+	strbuf_vaddf(buf, fmt, args);
+	strbuf_cleanup_path(buf);
 }
 
 char *git_snpath(char *buf, size_t n, const char *fmt, ...)
 {
-	char *ret;
+	struct strbuf sb = STRBUF_INIT;
 	va_list args;
 	va_start(args, fmt);
-	ret = vsnpath(buf, n, fmt, args);
+	vsnpath(&sb, fmt, args);
 	va_end(args);
-	return ret;
+	if (sb.len >= n)
+		strlcpy(buf, bad_path, n);
+	else
+		memcpy(buf, sb.buf, sb.len + 1);
+	strbuf_release(&sb);
+	return buf;
 }
 
 char *git_pathdup(const char *fmt, ...)
 {
-	char path[PATH_MAX], *ret;
+	struct strbuf path = STRBUF_INIT;
 	va_list args;
 	va_start(args, fmt);
-	ret = vsnpath(path, sizeof(path), fmt, args);
+	vsnpath(&path, fmt, args);
 	va_end(args);
-	return xstrdup(ret);
+	return strbuf_detach(&path, NULL);
 }
 
 char *mkpathdup(const char *fmt, ...)
 {
-	char *path;
 	struct strbuf sb = STRBUF_INIT;
 	va_list args;
-
 	va_start(args, fmt);
 	strbuf_vaddf(&sb, fmt, args);
 	va_end(args);
-	path = xstrdup(cleanup_path(sb.buf));
-
-	strbuf_release(&sb);
-	return path;
+	strbuf_cleanup_path(&sb);
+	return strbuf_detach(&sb, NULL);
 }
 
 char *mkpath(const char *fmt, ...)
 {
 	va_list args;
-	unsigned len;
-	char *pathname = get_pathname();
-
+	struct strbuf *pathname = get_pathname();
 	va_start(args, fmt);
-	len = vsnprintf(pathname, PATH_MAX, fmt, args);
+	strbuf_vaddf(pathname, fmt, args);
 	va_end(args);
-	if (len >= PATH_MAX)
-		return bad_path;
-	return cleanup_path(pathname);
+	return cleanup_path(pathname->buf);
 }
 
 char *git_path(const char *fmt, ...)
 {
-	char *pathname = get_pathname();
+	struct strbuf *pathname = get_pathname();
 	va_list args;
-	char *ret;
-
 	va_start(args, fmt);
-	ret = vsnpath(pathname, PATH_MAX, fmt, args);
+	vsnpath(pathname, fmt, args);
 	va_end(args);
-	return ret;
+	return pathname->buf;
 }
 
 void home_config_paths(char **global, char **xdg, char *file)
@@ -158,41 +154,27 @@ void home_config_paths(char **global, char **xdg, char *file)
 
 char *git_path_submodule(const char *path, const char *fmt, ...)
 {
-	char *pathname = get_pathname();
-	struct strbuf buf = STRBUF_INIT;
+	struct strbuf *buf = get_pathname();
 	const char *git_dir;
 	va_list args;
-	unsigned len;
-
-	len = strlen(path);
-	if (len > PATH_MAX-100)
-		return bad_path;
 
-	strbuf_addstr(&buf, path);
-	if (len && path[len-1] != '/')
-		strbuf_addch(&buf, '/');
-	strbuf_addstr(&buf, ".git");
+	strbuf_addstr(buf, path);
+	if (buf->len && buf->buf[buf->len - 1] != '/')
+		strbuf_addch(buf, '/');
+	strbuf_addstr(buf, ".git");
 
-	git_dir = read_gitfile(buf.buf);
+	git_dir = read_gitfile(buf->buf);
 	if (git_dir) {
-		strbuf_reset(&buf);
-		strbuf_addstr(&buf, git_dir);
+		strbuf_reset(buf);
+		strbuf_addstr(buf, git_dir);
 	}
-	strbuf_addch(&buf, '/');
-
-	if (buf.len >= PATH_MAX)
-		return bad_path;
-	memcpy(pathname, buf.buf, buf.len + 1);
-
-	strbuf_release(&buf);
-	len = strlen(pathname);
+	strbuf_addch(buf, '/');
 
 	va_start(args, fmt);
-	len += vsnprintf(pathname + len, PATH_MAX - len, fmt, args);
+	strbuf_vaddf(buf, fmt, args);
 	va_end(args);
-	if (len >= PATH_MAX)
-		return bad_path;
-	return cleanup_path(pathname);
+	strbuf_cleanup_path(buf);
+	return buf->buf;
 }
 
 int validate_headref(const char *path)
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 02/32] path.c: make get_pathname() call sites return const char *
  2014-08-30  8:33 [PATCH 00/32] nd/multiple-work-trees cleanup Nguyễn Thái Ngọc Duy
  2014-08-30  8:33 ` [PATCH 01/32] path.c: make get_pathname() return strbuf instead of static buffer Nguyễn Thái Ngọc Duy
@ 2014-08-30  8:33 ` Nguyễn Thái Ngọc Duy
  2014-08-30  8:33 ` [PATCH 03/32] git_snpath(): retire and replace with strbuf_git_path() Nguyễn Thái Ngọc Duy
                   ` (32 subsequent siblings)
  34 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-08-30  8:33 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Before the previous commit, get_pathname returns an array of PATH_MAX
length. Even if git_path() and similar functions does not use the
whole array, git_path() caller can, in theory.

After the commit, get_pathname() may return a buffer that has just
enough room for the returned string and git_path() caller should never
write beyond that.

Make git_path(), mkpath() and git_path_submodule() return a const
buffer to make sure callers do not write in it at all.

This could have been part of the previous commit, but the "const"
conversion is too much distraction from the core changes in path.c.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 builtin/checkout.c     | 2 +-
 builtin/clone.c        | 9 +++++----
 builtin/fetch.c        | 5 +++--
 builtin/fsck.c         | 4 ++--
 builtin/receive-pack.c | 2 +-
 builtin/remote.c       | 2 +-
 builtin/repack.c       | 8 +++++---
 cache.h                | 6 +++---
 fast-import.c          | 2 +-
 notes-merge.c          | 6 +++---
 path.c                 | 6 +++---
 refs.c                 | 8 ++++----
 run-command.c          | 4 ++--
 run-command.h          | 2 +-
 sha1_file.c            | 2 +-
 15 files changed, 36 insertions(+), 32 deletions(-)

diff --git a/builtin/checkout.c b/builtin/checkout.c
index 463cfee..3abcef1 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -585,7 +585,7 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
 			if (opts->new_branch_log && !log_all_ref_updates) {
 				int temp;
 				char log_file[PATH_MAX];
-				char *ref_name = mkpath("refs/heads/%s", opts->new_orphan_branch);
+				const char *ref_name = mkpath("refs/heads/%s", opts->new_orphan_branch);
 
 				temp = log_all_ref_updates;
 				log_all_ref_updates = 1;
diff --git a/builtin/clone.c b/builtin/clone.c
index e15ca33..91fac9d 100644
--- a/builtin/clone.c
+++ b/builtin/clone.c
@@ -289,16 +289,17 @@ static void copy_alternates(struct strbuf *src, struct strbuf *dst,
 	struct strbuf line = STRBUF_INIT;
 
 	while (strbuf_getline(&line, in, '\n') != EOF) {
-		char *abs_path, abs_buf[PATH_MAX];
+		char *abs_path;
 		if (!line.len || line.buf[0] == '#')
 			continue;
 		if (is_absolute_path(line.buf)) {
 			add_to_alternates_file(line.buf);
 			continue;
 		}
-		abs_path = mkpath("%s/objects/%s", src_repo, line.buf);
-		normalize_path_copy(abs_buf, abs_path);
-		add_to_alternates_file(abs_buf);
+		abs_path = mkpathdup("%s/objects/%s", src_repo, line.buf);
+		normalize_path_copy(abs_path, abs_path);
+		add_to_alternates_file(abs_path);
+		free(abs_path);
 	}
 	strbuf_release(&line);
 	fclose(in);
diff --git a/builtin/fetch.c b/builtin/fetch.c
index e8d0cca..9522b1b 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -573,7 +573,8 @@ static int store_updated_refs(const char *raw_url, const char *remote_name,
 	struct strbuf note = STRBUF_INIT;
 	const char *what, *kind;
 	struct ref *rm;
-	char *url, *filename = dry_run ? "/dev/null" : git_path("FETCH_HEAD");
+	char *url;
+	const char *filename = dry_run ? "/dev/null" : git_path("FETCH_HEAD");
 	int want_status;
 
 	fp = fopen(filename, "a");
@@ -807,7 +808,7 @@ static void check_not_current_branch(struct ref *ref_map)
 
 static int truncate_fetch_head(void)
 {
-	char *filename = git_path("FETCH_HEAD");
+	const char *filename = git_path("FETCH_HEAD");
 	FILE *fp = fopen(filename, "w");
 
 	if (!fp)
diff --git a/builtin/fsck.c b/builtin/fsck.c
index 8aadca1..d414962 100644
--- a/builtin/fsck.c
+++ b/builtin/fsck.c
@@ -225,12 +225,12 @@ static void check_unreachable_object(struct object *obj)
 			printf("dangling %s %s\n", typename(obj->type),
 			       sha1_to_hex(obj->sha1));
 		if (write_lost_and_found) {
-			char *filename = git_path("lost-found/%s/%s",
+			const char *filename = git_path("lost-found/%s/%s",
 				obj->type == OBJ_COMMIT ? "commit" : "other",
 				sha1_to_hex(obj->sha1));
 			FILE *f;
 
-			if (safe_create_leading_directories(filename)) {
+			if (safe_create_leading_directories_const(filename)) {
 				error("Could not create lost-found");
 				return;
 			}
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 18458e8..ed11e7e 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -599,7 +599,7 @@ static void run_update_post_hook(struct command *commands)
 	int argc;
 	const char **argv;
 	struct child_process proc;
-	char *hook;
+	const char *hook;
 
 	hook = find_hook("post-update");
 	for (argc = 0, cmd = commands; cmd; cmd = cmd->next) {
diff --git a/builtin/remote.c b/builtin/remote.c
index a8efe3d..3d99c9c 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -580,7 +580,7 @@ static int migrate_file(struct remote *remote)
 {
 	struct strbuf buf = STRBUF_INIT;
 	int i;
-	char *path = NULL;
+	const char *path = NULL;
 
 	strbuf_addf(&buf, "remote.%s.url", remote->name);
 	for (i = 0; i < remote->url_nr; i++)
diff --git a/builtin/repack.c b/builtin/repack.c
index ff2216a..a64a4a9 100644
--- a/builtin/repack.c
+++ b/builtin/repack.c
@@ -285,7 +285,8 @@ int cmd_repack(int argc, const char **argv, const char *prefix)
 	failed = 0;
 	for_each_string_list_item(item, &names) {
 		for (ext = 0; ext < ARRAY_SIZE(exts); ext++) {
-			char *fname, *fname_old;
+			const char *fname_old;
+			char *fname;
 			fname = mkpathdup("%s/pack-%s%s", packdir,
 						item->string, exts[ext].name);
 			if (!file_exists(fname)) {
@@ -313,7 +314,8 @@ int cmd_repack(int argc, const char **argv, const char *prefix)
 	if (failed) {
 		struct string_list rollback_failure = STRING_LIST_INIT_DUP;
 		for_each_string_list_item(item, &rollback) {
-			char *fname, *fname_old;
+			const char *fname_old;
+			char *fname;
 			fname = mkpathdup("%s/%s", packdir, item->string);
 			fname_old = mkpath("%s/old-%s", packdir, item->string);
 			if (rename(fname_old, fname))
@@ -366,7 +368,7 @@ int cmd_repack(int argc, const char **argv, const char *prefix)
 	/* Remove the "old-" files */
 	for_each_string_list_item(item, &names) {
 		for (ext = 0; ext < ARRAY_SIZE(exts); ext++) {
-			char *fname;
+			const char *fname;
 			fname = mkpath("%s/old-%s%s",
 					packdir,
 					item->string,
diff --git a/cache.h b/cache.h
index 44aa439..707408b 100644
--- a/cache.h
+++ b/cache.h
@@ -682,9 +682,9 @@ extern char *mkpathdup(const char *fmt, ...)
 	__attribute__((format (printf, 1, 2)));
 
 /* Return a statically allocated filename matching the sha1 signature */
-extern char *mkpath(const char *fmt, ...) __attribute__((format (printf, 1, 2)));
-extern char *git_path(const char *fmt, ...) __attribute__((format (printf, 1, 2)));
-extern char *git_path_submodule(const char *path, const char *fmt, ...)
+extern const char *mkpath(const char *fmt, ...) __attribute__((format (printf, 1, 2)));
+extern const char *git_path(const char *fmt, ...) __attribute__((format (printf, 1, 2)));
+extern const char *git_path_submodule(const char *path, const char *fmt, ...)
 	__attribute__((format (printf, 2, 3)));
 
 /*
diff --git a/fast-import.c b/fast-import.c
index fa635f7..d9c068b 100644
--- a/fast-import.c
+++ b/fast-import.c
@@ -404,7 +404,7 @@ static void dump_marks_helper(FILE *, uintmax_t, struct mark_set *);
 
 static void write_crash_report(const char *err)
 {
-	char *loc = git_path("fast_import_crash_%"PRIuMAX, (uintmax_t) getpid());
+	const char *loc = git_path("fast_import_crash_%"PRIuMAX, (uintmax_t) getpid());
 	FILE *rpt = fopen(loc, "w");
 	struct branch *b;
 	unsigned long lu;
diff --git a/notes-merge.c b/notes-merge.c
index fd5fae2..a9e6b15 100644
--- a/notes-merge.c
+++ b/notes-merge.c
@@ -280,7 +280,7 @@ static void check_notes_merge_worktree(struct notes_merge_options *o)
 				    "(%s exists).", git_path("NOTES_MERGE_*"));
 		}
 
-		if (safe_create_leading_directories(git_path(
+		if (safe_create_leading_directories_const(git_path(
 				NOTES_MERGE_WORKTREE "/.test")))
 			die_errno("unable to create directory %s",
 				  git_path(NOTES_MERGE_WORKTREE));
@@ -295,8 +295,8 @@ static void write_buf_to_worktree(const unsigned char *obj,
 				  const char *buf, unsigned long size)
 {
 	int fd;
-	char *path = git_path(NOTES_MERGE_WORKTREE "/%s", sha1_to_hex(obj));
-	if (safe_create_leading_directories(path))
+	const char *path = git_path(NOTES_MERGE_WORKTREE "/%s", sha1_to_hex(obj));
+	if (safe_create_leading_directories_const(path))
 		die_errno("unable to create directory for '%s'", path);
 	if (file_exists(path))
 		die("found existing file at '%s'", path);
diff --git a/path.c b/path.c
index 42ef3af..a3f8826 100644
--- a/path.c
+++ b/path.c
@@ -106,7 +106,7 @@ char *mkpathdup(const char *fmt, ...)
 	return strbuf_detach(&sb, NULL);
 }
 
-char *mkpath(const char *fmt, ...)
+const char *mkpath(const char *fmt, ...)
 {
 	va_list args;
 	struct strbuf *pathname = get_pathname();
@@ -116,7 +116,7 @@ char *mkpath(const char *fmt, ...)
 	return cleanup_path(pathname->buf);
 }
 
-char *git_path(const char *fmt, ...)
+const char *git_path(const char *fmt, ...)
 {
 	struct strbuf *pathname = get_pathname();
 	va_list args;
@@ -152,7 +152,7 @@ void home_config_paths(char **global, char **xdg, char *file)
 	free(to_free);
 }
 
-char *git_path_submodule(const char *path, const char *fmt, ...)
+const char *git_path_submodule(const char *path, const char *fmt, ...)
 {
 	struct strbuf *buf = get_pathname();
 	const char *git_dir;
diff --git a/refs.c b/refs.c
index 82e4842..dbd131e 100644
--- a/refs.c
+++ b/refs.c
@@ -1442,7 +1442,7 @@ static int resolve_gitlink_ref_recursive(struct ref_cache *refs,
 {
 	int fd, len;
 	char buffer[128], *p;
-	char *path;
+	const char *path;
 
 	if (recursion > MAXDEPTH || strlen(refname) > MAXREFLEN)
 		return -1;
@@ -2255,7 +2255,7 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname,
 					    const unsigned char *old_sha1,
 					    int flags, int *type_p)
 {
-	char *ref_file;
+	const char *ref_file;
 	const char *orig_refname = refname;
 	struct ref_lock *lock;
 	int last_errno = 0;
@@ -2318,7 +2318,7 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname,
 		lock->force_write = 1;
 
  retry:
-	switch (safe_create_leading_directories(ref_file)) {
+	switch (safe_create_leading_directories_const(ref_file)) {
 	case SCLD_OK:
 		break; /* success */
 	case SCLD_VANISHED:
@@ -2756,7 +2756,7 @@ static int rename_tmp_log(const char *newrefname)
 	int attempts_remaining = 4;
 
  retry:
-	switch (safe_create_leading_directories(git_path("logs/%s", newrefname))) {
+	switch (safe_create_leading_directories_const(git_path("logs/%s", newrefname))) {
 	case SCLD_OK:
 		break; /* success */
 	case SCLD_VANISHED:
diff --git a/run-command.c b/run-command.c
index be07d4a..614b8ac 100644
--- a/run-command.c
+++ b/run-command.c
@@ -758,9 +758,9 @@ int finish_async(struct async *async)
 #endif
 }
 
-char *find_hook(const char *name)
+const char *find_hook(const char *name)
 {
-	char *path = git_path("hooks/%s", name);
+	const char *path = git_path("hooks/%s", name);
 	if (access(path, X_OK) < 0)
 		path = NULL;
 
diff --git a/run-command.h b/run-command.h
index ea73de3..890cc95 100644
--- a/run-command.h
+++ b/run-command.h
@@ -48,7 +48,7 @@ int start_command(struct child_process *);
 int finish_command(struct child_process *);
 int run_command(struct child_process *);
 
-extern char *find_hook(const char *name);
+extern const char *find_hook(const char *name);
 LAST_ARG_MUST_BE_NULL
 extern int run_hook_le(const char *const *env, const char *name, ...);
 extern int run_hook_ve(const char *const *env, const char *name, va_list args);
diff --git a/sha1_file.c b/sha1_file.c
index a38854c..9700108 100644
--- a/sha1_file.c
+++ b/sha1_file.c
@@ -402,7 +402,7 @@ void add_to_alternates_file(const char *reference)
 {
 	struct lock_file *lock = xcalloc(1, sizeof(struct lock_file));
 	int fd = hold_lock_file_for_append(lock, git_path("objects/info/alternates"), LOCK_DIE_ON_ERROR);
-	char *alt = mkpath("%s\n", reference);
+	const char *alt = mkpath("%s\n", reference);
 	write_or_die(fd, alt, strlen(alt));
 	if (commit_lock_file(lock))
 		die("could not close alternates file");
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 03/32] git_snpath(): retire and replace with strbuf_git_path()
  2014-08-30  8:33 [PATCH 00/32] nd/multiple-work-trees cleanup Nguyễn Thái Ngọc Duy
  2014-08-30  8:33 ` [PATCH 01/32] path.c: make get_pathname() return strbuf instead of static buffer Nguyễn Thái Ngọc Duy
  2014-08-30  8:33 ` [PATCH 02/32] path.c: make get_pathname() call sites return const char * Nguyễn Thái Ngọc Duy
@ 2014-08-30  8:33 ` Nguyễn Thái Ngọc Duy
  2014-08-30  8:33 ` [PATCH 04/32] path.c: rename vsnpath() to do_git_path() Nguyễn Thái Ngọc Duy
                   ` (31 subsequent siblings)
  34 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-08-30  8:33 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

In the previous patch, git_snpath() is modified to allocate a new
strbuf buffer because vsnpath() needs that. But that makes it
awkward because git_snpath() receives a pre-allocated buffer from
outside and has to copy data back. Rename it to strbuf_git_path()
and make it receive strbuf directly.

Using git_path() in update_refs_for_switch() which used to call
git_snpath() is safe because that function and all of its callers do
not keep any pointer to the round-robin buffer pool allocated by
get_pathname().

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 builtin/checkout.c | 13 +++++----
 cache.h            |  4 +--
 path.c             | 11 ++------
 refs.c             | 78 +++++++++++++++++++++++++++++++++---------------------
 refs.h             |  2 +-
 5 files changed, 61 insertions(+), 47 deletions(-)

diff --git a/builtin/checkout.c b/builtin/checkout.c
index 3abcef1..8023987 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -584,18 +584,21 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
 		if (opts->new_orphan_branch) {
 			if (opts->new_branch_log && !log_all_ref_updates) {
 				int temp;
-				char log_file[PATH_MAX];
-				const char *ref_name = mkpath("refs/heads/%s", opts->new_orphan_branch);
+				struct strbuf log_file = STRBUF_INIT;
+				int ret;
+				const char *ref_name;
 
+				ref_name = mkpath("refs/heads/%s", opts->new_orphan_branch);
 				temp = log_all_ref_updates;
 				log_all_ref_updates = 1;
-				if (log_ref_setup(ref_name, log_file, sizeof(log_file))) {
+				ret = log_ref_setup(ref_name, &log_file);
+				log_all_ref_updates = temp;
+				strbuf_release(&log_file);
+				if (ret) {
 					fprintf(stderr, _("Can not do reflog for '%s'\n"),
 					    opts->new_orphan_branch);
-					log_all_ref_updates = temp;
 					return;
 				}
-				log_all_ref_updates = temp;
 			}
 		}
 		else
diff --git a/cache.h b/cache.h
index 707408b..279b581 100644
--- a/cache.h
+++ b/cache.h
@@ -674,8 +674,8 @@ extern int check_repository_format(void);
 
 extern char *mksnpath(char *buf, size_t n, const char *fmt, ...)
 	__attribute__((format (printf, 3, 4)));
-extern char *git_snpath(char *buf, size_t n, const char *fmt, ...)
-	__attribute__((format (printf, 3, 4)));
+extern void strbuf_git_path(struct strbuf *sb, const char *fmt, ...)
+	__attribute__((format (printf, 2, 3)));
 extern char *git_pathdup(const char *fmt, ...)
 	__attribute__((format (printf, 1, 2)));
 extern char *mkpathdup(const char *fmt, ...)
diff --git a/path.c b/path.c
index a3f8826..e545064 100644
--- a/path.c
+++ b/path.c
@@ -70,19 +70,12 @@ static void vsnpath(struct strbuf *buf, const char *fmt, va_list args)
 	strbuf_cleanup_path(buf);
 }
 
-char *git_snpath(char *buf, size_t n, const char *fmt, ...)
+void strbuf_git_path(struct strbuf *sb, const char *fmt, ...)
 {
-	struct strbuf sb = STRBUF_INIT;
 	va_list args;
 	va_start(args, fmt);
-	vsnpath(&sb, fmt, args);
+	vsnpath(sb, fmt, args);
 	va_end(args);
-	if (sb.len >= n)
-		strlcpy(buf, bad_path, n);
-	else
-		memcpy(buf, sb.buf, sb.len + 1);
-	strbuf_release(&sb);
-	return buf;
 }
 
 char *git_pathdup(const char *fmt, ...)
diff --git a/refs.c b/refs.c
index dbd131e..00b2312 100644
--- a/refs.c
+++ b/refs.c
@@ -1535,10 +1535,12 @@ static const char *handle_missing_loose_ref(const char *refname,
 
 const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int reading, int *flag)
 {
+	struct strbuf sb_path = STRBUF_INIT;
 	int depth = MAXDEPTH;
 	ssize_t len;
 	char buffer[256];
 	static char refname_buffer[256];
+	const char *ret;
 
 	if (flag)
 		*flag = 0;
@@ -1547,15 +1549,17 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea
 		return NULL;
 
 	for (;;) {
-		char path[PATH_MAX];
+		const char *path;
 		struct stat st;
 		char *buf;
 		int fd;
 
 		if (--depth < 0)
-			return NULL;
+			goto fail;
 
-		git_snpath(path, sizeof(path), "%s", refname);
+		strbuf_reset(&sb_path);
+		strbuf_git_path(&sb_path, "%s", refname);
+		path = sb_path.buf;
 
 		/*
 		 * We might have to loop back here to avoid a race
@@ -1569,10 +1573,11 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea
 	stat_ref:
 		if (lstat(path, &st) < 0) {
 			if (errno == ENOENT)
-				return handle_missing_loose_ref(refname, sha1,
-								reading, flag);
+				ret = handle_missing_loose_ref(refname, sha1,
+							       reading, flag);
 			else
-				return NULL;
+				ret = NULL;
+			goto done;
 		}
 
 		/* Follow "normalized" - ie "refs/.." symlinks by hand */
@@ -1583,7 +1588,7 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea
 					/* inconsistent with lstat; retry */
 					goto stat_ref;
 				else
-					return NULL;
+					goto fail;
 			}
 			buffer[len] = 0;
 			if (starts_with(buffer, "refs/") &&
@@ -1599,7 +1604,7 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea
 		/* Is it a directory? */
 		if (S_ISDIR(st.st_mode)) {
 			errno = EISDIR;
-			return NULL;
+			goto fail;
 		}
 
 		/*
@@ -1612,12 +1617,13 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea
 				/* inconsistent with lstat; retry */
 				goto stat_ref;
 			else
-				return NULL;
+				goto fail;
 		}
+
 		len = read_in_full(fd, buffer, sizeof(buffer)-1);
 		close(fd);
 		if (len < 0)
-			return NULL;
+			goto fail;
 		while (len && isspace(buffer[len-1]))
 			len--;
 		buffer[len] = '\0';
@@ -1634,9 +1640,10 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea
 			    (buffer[40] != '\0' && !isspace(buffer[40]))) {
 				if (flag)
 					*flag |= REF_ISBROKEN;
-				return NULL;
+				goto fail;
 			}
-			return refname;
+			ret = refname;
+			goto done;
 		}
 		if (flag)
 			*flag |= REF_ISSYMREF;
@@ -1646,10 +1653,15 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea
 		if (check_refname_format(buf, REFNAME_ALLOW_ONELEVEL)) {
 			if (flag)
 				*flag |= REF_ISBROKEN;
-			return NULL;
+			goto fail;
 		}
 		refname = strcpy(refname_buffer, buf);
 	}
+fail:
+	ret = NULL;
+done:
+	strbuf_release(&sb_path);
+	return ret;
 }
 
 char *resolve_refdup(const char *ref, unsigned char *sha1, int reading, int *flag)
@@ -2940,41 +2952,41 @@ static int copy_msg(char *buf, const char *msg)
 	return cp - buf;
 }
 
-int log_ref_setup(const char *refname, char *logfile, int bufsize)
+int log_ref_setup(const char *refname, struct strbuf *logfile)
 {
 	int logfd, oflags = O_APPEND | O_WRONLY;
 
-	git_snpath(logfile, bufsize, "logs/%s", refname);
+	strbuf_git_path(logfile, "logs/%s", refname);
 	if (log_all_ref_updates &&
 	    (starts_with(refname, "refs/heads/") ||
 	     starts_with(refname, "refs/remotes/") ||
 	     starts_with(refname, "refs/notes/") ||
 	     !strcmp(refname, "HEAD"))) {
-		if (safe_create_leading_directories(logfile) < 0)
+		if (safe_create_leading_directories(logfile->buf) < 0)
 			return error("unable to create directory for %s",
-				     logfile);
+				     logfile->buf);
 		oflags |= O_CREAT;
 	}
 
-	logfd = open(logfile, oflags, 0666);
+	logfd = open(logfile->buf, oflags, 0666);
 	if (logfd < 0) {
 		if (!(oflags & O_CREAT) && errno == ENOENT)
 			return 0;
 
 		if ((oflags & O_CREAT) && errno == EISDIR) {
-			if (remove_empty_directories(logfile)) {
+			if (remove_empty_directories(logfile->buf)) {
 				return error("There are still logs under '%s'",
-					     logfile);
+					     logfile->buf);
 			}
-			logfd = open(logfile, oflags, 0666);
+			logfd = open(logfile->buf, oflags, 0666);
 		}
 
 		if (logfd < 0)
 			return error("Unable to append to %s: %s",
-				     logfile, strerror(errno));
+				     logfile->buf, strerror(errno));
 	}
 
-	adjust_shared_perm(logfile);
+	adjust_shared_perm(logfile->buf);
 	close(logfd);
 	return 0;
 }
@@ -2985,20 +2997,22 @@ static int log_ref_write(const char *refname, const unsigned char *old_sha1,
 	int logfd, result, written, oflags = O_APPEND | O_WRONLY;
 	unsigned maxlen, len;
 	int msglen;
-	char log_file[PATH_MAX];
+	struct strbuf sb_log_file = STRBUF_INIT;
+	const char *log_file;
 	char *logrec;
 	const char *committer;
 
 	if (log_all_ref_updates < 0)
 		log_all_ref_updates = !is_bare_repository();
 
-	result = log_ref_setup(refname, log_file, sizeof(log_file));
+	result = log_ref_setup(refname, &sb_log_file);
 	if (result)
-		return result;
+		goto done;
+	log_file = sb_log_file.buf;
 
 	logfd = open(log_file, oflags);
 	if (logfd < 0)
-		return 0;
+		goto done;
 	msglen = msg ? strlen(msg) : 0;
 	committer = git_committer_info(0);
 	maxlen = strlen(committer) + msglen + 100;
@@ -3011,9 +3025,13 @@ static int log_ref_write(const char *refname, const unsigned char *old_sha1,
 		len += copy_msg(logrec + len - 1, msg) - 1;
 	written = len <= maxlen ? write_in_full(logfd, logrec, len) : -1;
 	free(logrec);
-	if (close(logfd) != 0 || written != len)
-		return error("Unable to append to %s", log_file);
-	return 0;
+	if (close(logfd) != 0 || written != len) {
+		error("Unable to append to %s", log_file);
+		result = -1;
+	}
+done:
+	strbuf_release(&sb_log_file);
+	return result;
 }
 
 static int is_branch(const char *refname)
diff --git a/refs.h b/refs.h
index 4e3050d..99d88a7 100644
--- a/refs.h
+++ b/refs.h
@@ -157,7 +157,7 @@ extern void unlock_ref(struct ref_lock *lock);
 extern int write_ref_sha1(struct ref_lock *lock, const unsigned char *sha1, const char *msg);
 
 /** Setup reflog before using. **/
-int log_ref_setup(const char *refname, char *logfile, int bufsize);
+int log_ref_setup(const char *refname, struct strbuf *logfile);
 
 /** Reads log for the value of ref during at_time. **/
 extern int read_ref_at(const char *refname, unsigned long at_time, int cnt,
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 04/32] path.c: rename vsnpath() to do_git_path()
  2014-08-30  8:33 [PATCH 00/32] nd/multiple-work-trees cleanup Nguyễn Thái Ngọc Duy
                   ` (2 preceding siblings ...)
  2014-08-30  8:33 ` [PATCH 03/32] git_snpath(): retire and replace with strbuf_git_path() Nguyễn Thái Ngọc Duy
@ 2014-08-30  8:33 ` Nguyễn Thái Ngọc Duy
  2014-08-30  8:33 ` [PATCH 05/32] path.c: group git_path(), git_pathdup() and strbuf_git_path() together Nguyễn Thái Ngọc Duy
                   ` (30 subsequent siblings)
  34 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-08-30  8:33 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

The name vsnpath() gives an impression that this is general path
handling function. It's not. This is the underlying implementation of
git_path(), git_pathdup() and strbuf_git_path() which will prefix
$GIT_DIR in the result string.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 path.c | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/path.c b/path.c
index e545064..2cb2e61 100644
--- a/path.c
+++ b/path.c
@@ -60,7 +60,7 @@ char *mksnpath(char *buf, size_t n, const char *fmt, ...)
 	return cleanup_path(buf);
 }
 
-static void vsnpath(struct strbuf *buf, const char *fmt, va_list args)
+static void do_git_path(struct strbuf *buf, const char *fmt, va_list args)
 {
 	const char *git_dir = get_git_dir();
 	strbuf_addstr(buf, git_dir);
@@ -74,7 +74,7 @@ void strbuf_git_path(struct strbuf *sb, const char *fmt, ...)
 {
 	va_list args;
 	va_start(args, fmt);
-	vsnpath(sb, fmt, args);
+	do_git_path(sb, fmt, args);
 	va_end(args);
 }
 
@@ -83,7 +83,7 @@ char *git_pathdup(const char *fmt, ...)
 	struct strbuf path = STRBUF_INIT;
 	va_list args;
 	va_start(args, fmt);
-	vsnpath(&path, fmt, args);
+	do_git_path(&path, fmt, args);
 	va_end(args);
 	return strbuf_detach(&path, NULL);
 }
@@ -114,7 +114,7 @@ const char *git_path(const char *fmt, ...)
 	struct strbuf *pathname = get_pathname();
 	va_list args;
 	va_start(args, fmt);
-	vsnpath(pathname, fmt, args);
+	do_git_path(pathname, fmt, args);
 	va_end(args);
 	return pathname->buf;
 }
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 05/32] path.c: group git_path(), git_pathdup() and strbuf_git_path() together
  2014-08-30  8:33 [PATCH 00/32] nd/multiple-work-trees cleanup Nguyễn Thái Ngọc Duy
                   ` (3 preceding siblings ...)
  2014-08-30  8:33 ` [PATCH 04/32] path.c: rename vsnpath() to do_git_path() Nguyễn Thái Ngọc Duy
@ 2014-08-30  8:33 ` Nguyễn Thái Ngọc Duy
  2014-08-30  8:33 ` [PATCH 06/32] git_path(): be aware of file relocation in $GIT_DIR Nguyễn Thái Ngọc Duy
                   ` (29 subsequent siblings)
  34 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-08-30  8:33 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 path.c | 20 ++++++++++----------
 1 file changed, 10 insertions(+), 10 deletions(-)

diff --git a/path.c b/path.c
index 2cb2e61..65881aa 100644
--- a/path.c
+++ b/path.c
@@ -78,6 +78,16 @@ void strbuf_git_path(struct strbuf *sb, const char *fmt, ...)
 	va_end(args);
 }
 
+const char *git_path(const char *fmt, ...)
+{
+	struct strbuf *pathname = get_pathname();
+	va_list args;
+	va_start(args, fmt);
+	do_git_path(pathname, fmt, args);
+	va_end(args);
+	return pathname->buf;
+}
+
 char *git_pathdup(const char *fmt, ...)
 {
 	struct strbuf path = STRBUF_INIT;
@@ -109,16 +119,6 @@ const char *mkpath(const char *fmt, ...)
 	return cleanup_path(pathname->buf);
 }
 
-const char *git_path(const char *fmt, ...)
-{
-	struct strbuf *pathname = get_pathname();
-	va_list args;
-	va_start(args, fmt);
-	do_git_path(pathname, fmt, args);
-	va_end(args);
-	return pathname->buf;
-}
-
 void home_config_paths(char **global, char **xdg, char *file)
 {
 	char *xdg_home = getenv("XDG_CONFIG_HOME");
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 06/32] git_path(): be aware of file relocation in $GIT_DIR
  2014-08-30  8:33 [PATCH 00/32] nd/multiple-work-trees cleanup Nguyễn Thái Ngọc Duy
                   ` (4 preceding siblings ...)
  2014-08-30  8:33 ` [PATCH 05/32] path.c: group git_path(), git_pathdup() and strbuf_git_path() together Nguyễn Thái Ngọc Duy
@ 2014-08-30  8:33 ` Nguyễn Thái Ngọc Duy
  2014-08-30  8:33 ` [PATCH 07/32] *.sh: respect $GIT_INDEX_FILE Nguyễn Thái Ngọc Duy
                   ` (28 subsequent siblings)
  34 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-08-30  8:33 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

We allow the user to relocate certain paths out of $GIT_DIR via
environment variables, e.g. GIT_OBJECT_DIRECTORY, GIT_INDEX_FILE and
GIT_GRAFT_FILE. Callers are not supposed to use git_path() or
git_pathdup() to get those paths. Instead they must use
get_object_directory(), get_index_file() and get_graft_file()
respectively. This is inconvenient and could be missed in review (for
example, there's git_path("objects/info/alternates") somewhere in
sha1_file.c).

This patch makes git_path() and git_pathdup() understand those
environment variables. So if you set GIT_OBJECT_DIRECTORY to /foo/bar,
git_path("objects/abc") should return /foo/bar/abc. The same is done
for the two remaining env variables.

"git rev-parse --git-path" is the wrapper for script use.

This patch kinda reverts a0279e1 (setup_git_env: use git_pathdup
instead of xmalloc + sprintf - 2014-06-19) because using git_pathdup
here would result in infinite recursion:

  setup_git_env() -> git_pathdup("objects") -> .. -> adjust_git_path()
  -> get_object_directory() -> oops, git_object_directory is NOT set
  yet -> setup_git_env()

I wanted to make git_pathdup_literal() that skips adjust_git_path().
But that won't work because later on when $GIT_COMMON_DIR is
introduced, git_pathdup_literal("objects") needs adjust_git_path() to
replace $GIT_DIR with $GIT_COMMON_DIR.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 Documentation/git-rev-parse.txt |  7 ++++++
 builtin/rev-parse.c             |  7 ++++++
 cache.h                         |  1 +
 environment.c                   | 19 +++++++++++-----
 path.c                          | 49 +++++++++++++++++++++++++++++++++++++++--
 t/t0060-path-utils.sh           | 19 ++++++++++++++++
 6 files changed, 95 insertions(+), 7 deletions(-)

diff --git a/Documentation/git-rev-parse.txt b/Documentation/git-rev-parse.txt
index 987395d..9465399 100644
--- a/Documentation/git-rev-parse.txt
+++ b/Documentation/git-rev-parse.txt
@@ -232,6 +232,13 @@ print a message to stderr and exit with nonzero status.
 	repository.  If <path> is a gitfile then the resolved path
 	to the real repository is printed.
 
+--git-path <path>::
+	Resolve "$GIT_DIR/<path>" and takes other path relocation
+	variables such as $GIT_OBJECT_DIRECTORY,
+	$GIT_INDEX_FILE... into account. For example, if
+	$GIT_OBJECT_DIRECTORY is set to /foo/bar then "git rev-parse
+	--git-path objects/abc" returns /foo/bar/abc.
+
 --show-cdup::
 	When the command is invoked from a subdirectory, show the
 	path of the top-level directory relative to the current
diff --git a/builtin/rev-parse.c b/builtin/rev-parse.c
index 1a6122d..7606d43 100644
--- a/builtin/rev-parse.c
+++ b/builtin/rev-parse.c
@@ -529,6 +529,13 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)
 	for (i = 1; i < argc; i++) {
 		const char *arg = argv[i];
 
+		if (!strcmp(arg, "--git-path")) {
+			if (!argv[i + 1])
+				die("--git-path requires an argument");
+			puts(git_path("%s", argv[i + 1]));
+			i++;
+			continue;
+		}
 		if (as_is) {
 			if (show_file(arg, output_prefix) && as_is < 2)
 				verify_filename(prefix, arg, 0);
diff --git a/cache.h b/cache.h
index 279b581..0128b9a 100644
--- a/cache.h
+++ b/cache.h
@@ -612,6 +612,7 @@ extern int fsync_object_files;
 extern int core_preload_index;
 extern int core_apply_sparse_checkout;
 extern int precomposed_unicode;
+extern int git_db_env, git_index_env, git_graft_env;
 
 /*
  * The character that begins a commented line in user-editable file
diff --git a/environment.c b/environment.c
index 565f652..fee12a6 100644
--- a/environment.c
+++ b/environment.c
@@ -83,6 +83,7 @@ static size_t namespace_len;
 
 static const char *git_dir;
 static char *git_object_dir, *git_index_file, *git_graft_file;
+int git_db_env, git_index_env, git_graft_env;
 
 /*
  * Repository-local GIT_* environment variables; see cache.h for details.
@@ -124,10 +125,18 @@ static char *expand_namespace(const char *raw_namespace)
 	return strbuf_detach(&buf, NULL);
 }
 
-static char *git_path_from_env(const char *envvar, const char *path)
+static char *git_path_from_env(const char *envvar, const char *path,
+			       int *fromenv)
 {
 	const char *value = getenv(envvar);
-	return value ? xstrdup(value) : git_pathdup("%s", path);
+	if (!value) {
+		char *buf = xmalloc(strlen(git_dir) + strlen(path) + 2);
+		sprintf(buf, "%s/%s", git_dir, path);
+		return buf;
+	}
+	if (fromenv)
+		*fromenv = 1;
+	return xstrdup(value);
 }
 
 static void setup_git_env(void)
@@ -140,9 +149,9 @@ static void setup_git_env(void)
 		git_dir = DEFAULT_GIT_DIR_ENVIRONMENT;
 	gitfile = read_gitfile(git_dir);
 	git_dir = xstrdup(gitfile ? gitfile : git_dir);
-	git_object_dir = git_path_from_env(DB_ENVIRONMENT, "objects");
-	git_index_file = git_path_from_env(INDEX_ENVIRONMENT, "index");
-	git_graft_file = git_path_from_env(GRAFT_ENVIRONMENT, "info/grafts");
+	git_object_dir = git_path_from_env(DB_ENVIRONMENT, "objects", &git_db_env);
+	git_index_file = git_path_from_env(INDEX_ENVIRONMENT, "index", &git_index_env);
+	git_graft_file = git_path_from_env(GRAFT_ENVIRONMENT, "info/grafts", &git_graft_env);
 	if (getenv(NO_REPLACE_OBJECTS_ENVIRONMENT))
 		check_replace_refs = 0;
 	namespace = expand_namespace(getenv(GIT_NAMESPACE_ENVIRONMENT));
diff --git a/path.c b/path.c
index 65881aa..3deb80c 100644
--- a/path.c
+++ b/path.c
@@ -60,13 +60,58 @@ char *mksnpath(char *buf, size_t n, const char *fmt, ...)
 	return cleanup_path(buf);
 }
 
+static int dir_prefix(const char *buf, const char *dir)
+{
+	int len = strlen(dir);
+	return !strncmp(buf, dir, len) &&
+		(is_dir_sep(buf[len]) || buf[len] == '\0');
+}
+
+/* $buf =~ m|$dir/+$file| but without regex */
+static int is_dir_file(const char *buf, const char *dir, const char *file)
+{
+	int len = strlen(dir);
+	if (strncmp(buf, dir, len) || !is_dir_sep(buf[len]))
+		return 0;
+	while (is_dir_sep(buf[len]))
+		len++;
+	return !strcmp(buf + len, file);
+}
+
+static void replace_dir(struct strbuf *buf, int len, const char *newdir)
+{
+	int newlen = strlen(newdir);
+	int need_sep = (buf->buf[len] && !is_dir_sep(buf->buf[len])) &&
+		!is_dir_sep(newdir[newlen - 1]);
+	if (need_sep)
+		len--;	 /* keep one char, to be replaced with '/'  */
+	strbuf_splice(buf, 0, len, newdir, newlen);
+	if (need_sep)
+		buf->buf[newlen] = '/';
+}
+
+static void adjust_git_path(struct strbuf *buf, int git_dir_len)
+{
+	const char *base = buf->buf + git_dir_len;
+	if (git_graft_env && is_dir_file(base, "info", "grafts"))
+		strbuf_splice(buf, 0, buf->len,
+			      get_graft_file(), strlen(get_graft_file()));
+	else if (git_index_env && !strcmp(base, "index"))
+		strbuf_splice(buf, 0, buf->len,
+			      get_index_file(), strlen(get_index_file()));
+	else if (git_db_env && dir_prefix(base, "objects"))
+		replace_dir(buf, git_dir_len + 7, get_object_directory());
+}
+
 static void do_git_path(struct strbuf *buf, const char *fmt, va_list args)
 {
-	const char *git_dir = get_git_dir();
-	strbuf_addstr(buf, git_dir);
+	int gitdir_len;
+	strbuf_addstr(buf, get_git_dir());
 	if (buf->len && !is_dir_sep(buf->buf[buf->len - 1]))
 		strbuf_addch(buf, '/');
+	gitdir_len = buf->len;
 	strbuf_vaddf(buf, fmt, args);
+	adjust_git_path(buf, gitdir_len);
 	strbuf_cleanup_path(buf);
 }
 
diff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh
index c0143a0..33d2818 100755
--- a/t/t0060-path-utils.sh
+++ b/t/t0060-path-utils.sh
@@ -19,6 +19,14 @@ relative_path() {
 	"test \"\$(test-path-utils relative_path '$1' '$2')\" = '$expected'"
 }
 
+test_git_path() {
+	test_expect_success "git-path $1 $2 => $3" "
+		$1 git rev-parse --git-path $2 >actual &&
+		echo $3 >expect &&
+		test_cmp expect actual
+	"
+}
+
 # On Windows, we are using MSYS's bash, which mangles the paths.
 # Absolute paths are anchored at the MSYS installation directory,
 # which means that the path / accounts for this many characters:
@@ -244,4 +252,15 @@ relative_path "<null>"		"<empty>"	./
 relative_path "<null>"		"<null>"	./
 relative_path "<null>"		/foo/a/b	./
 
+test_git_path A=B                info/grafts .git/info/grafts
+test_git_path GIT_GRAFT_FILE=foo info/grafts foo
+test_git_path GIT_GRAFT_FILE=foo info/////grafts foo
+test_git_path GIT_INDEX_FILE=foo index foo
+test_git_path GIT_INDEX_FILE=foo index/foo .git/index/foo
+test_git_path GIT_INDEX_FILE=foo index2 .git/index2
+test_expect_success 'setup fake objects directory foo' 'mkdir foo'
+test_git_path GIT_OBJECT_DIRECTORY=foo objects foo
+test_git_path GIT_OBJECT_DIRECTORY=foo objects/foo foo/foo
+test_git_path GIT_OBJECT_DIRECTORY=foo objects2 .git/objects2
+
 test_done
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 07/32] *.sh: respect $GIT_INDEX_FILE
  2014-08-30  8:33 [PATCH 00/32] nd/multiple-work-trees cleanup Nguyễn Thái Ngọc Duy
                   ` (5 preceding siblings ...)
  2014-08-30  8:33 ` [PATCH 06/32] git_path(): be aware of file relocation in $GIT_DIR Nguyễn Thái Ngọc Duy
@ 2014-08-30  8:33 ` Nguyễn Thái Ngọc Duy
  2014-08-30  8:33 ` [PATCH 08/32] reflog: avoid constructing .lock path with git_path Nguyễn Thái Ngọc Duy
                   ` (27 subsequent siblings)
  34 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-08-30  8:33 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 git-pull.sh  | 2 +-
 git-stash.sh | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/git-pull.sh b/git-pull.sh
index 18a394f..6ab0c31 100755
--- a/git-pull.sh
+++ b/git-pull.sh
@@ -240,7 +240,7 @@ test true = "$rebase" && {
 	if ! git rev-parse -q --verify HEAD >/dev/null
 	then
 		# On an unborn branch
-		if test -f "$GIT_DIR/index"
+		if test -f "$(git rev-parse --git-path index)"
 		then
 			die "$(gettext "updating an unborn branch with changes added to the index")"
 		fi
diff --git a/git-stash.sh b/git-stash.sh
index bcc757b..393e1ec 100755
--- a/git-stash.sh
+++ b/git-stash.sh
@@ -20,7 +20,7 @@ require_work_tree
 cd_to_toplevel
 
 TMP="$GIT_DIR/.git-stash.$$"
-TMPindex=${GIT_INDEX_FILE-"$GIT_DIR/index"}.stash.$$
+TMPindex=${GIT_INDEX_FILE-"$(git rev-parse --git-path index)"}.stash.$$
 trap 'rm -f "$TMP-"* "$TMPindex"' 0
 
 ref_stash=refs/stash
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 08/32] reflog: avoid constructing .lock path with git_path
  2014-08-30  8:33 [PATCH 00/32] nd/multiple-work-trees cleanup Nguyễn Thái Ngọc Duy
                   ` (6 preceding siblings ...)
  2014-08-30  8:33 ` [PATCH 07/32] *.sh: respect $GIT_INDEX_FILE Nguyễn Thái Ngọc Duy
@ 2014-08-30  8:33 ` Nguyễn Thái Ngọc Duy
  2014-08-30  8:33 ` [PATCH 09/32] fast-import: use git_path() for accessing .git dir instead of get_git_dir() Nguyễn Thái Ngọc Duy
                   ` (26 subsequent siblings)
  34 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-08-30  8:33 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Among pathnames in $GIT_DIR, e.g. "index" or "packed-refs", we want to
automatically and silently map some of them to the $GIT_DIR of the
repository we are borrowing from via $GIT_COMMON_DIR mechanism.  When
we formulate the pathname for its lockfile, we want it to be in the
same location as its final destination.  "index" is not shared and
needs to remain in the borrowing repository, while "packed-refs" is
shared and needs to go to the borrowed repository.

git_path() could be taught about the ".lock" suffix and map
"index.lock" and "packed-refs.lock" the same way their basenames are
mapped, but instead the caller can help by asking where the basename
(e.g. "index") is mapped to git_path() and then appending ".lock"
after the mapping is done.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 builtin/reflog.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/builtin/reflog.c b/builtin/reflog.c
index e8a8fb1..9bd874d 100644
--- a/builtin/reflog.c
+++ b/builtin/reflog.c
@@ -372,7 +372,7 @@ static int expire_reflog(const char *ref, const unsigned char *sha1, int unused,
 	if (!reflog_exists(ref))
 		goto finish;
 	if (!cmd->dry_run) {
-		newlog_path = git_pathdup("logs/%s.lock", ref);
+		newlog_path = mkpathdup("%s.lock", log_file);
 		cb.newlog = fopen(newlog_path, "w");
 	}
 
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 09/32] fast-import: use git_path() for accessing .git dir instead of get_git_dir()
  2014-08-30  8:33 [PATCH 00/32] nd/multiple-work-trees cleanup Nguyễn Thái Ngọc Duy
                   ` (7 preceding siblings ...)
  2014-08-30  8:33 ` [PATCH 08/32] reflog: avoid constructing .lock path with git_path Nguyễn Thái Ngọc Duy
@ 2014-08-30  8:33 ` Nguyễn Thái Ngọc Duy
  2014-08-30  8:33 ` [PATCH 10/32] commit: use SEQ_DIR instead of hardcoding "sequencer" Nguyễn Thái Ngọc Duy
                   ` (25 subsequent siblings)
  34 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-08-30  8:33 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

This allows git_path() to redirect info/fast-import to another place
if needed

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 fast-import.c | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/fast-import.c b/fast-import.c
index d9c068b..ea426c4 100644
--- a/fast-import.c
+++ b/fast-import.c
@@ -3103,12 +3103,9 @@ static void parse_progress(void)
 
 static char* make_fast_import_path(const char *path)
 {
-	struct strbuf abs_path = STRBUF_INIT;
-
 	if (!relative_marks_paths || is_absolute_path(path))
 		return xstrdup(path);
-	strbuf_addf(&abs_path, "%s/info/fast-import/%s", get_git_dir(), path);
-	return strbuf_detach(&abs_path, NULL);
+	return xstrdup(git_path("info/fast-import/%s", path));
 }
 
 static void option_import_marks(const char *marks,
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 10/32] commit: use SEQ_DIR instead of hardcoding "sequencer"
  2014-08-30  8:33 [PATCH 00/32] nd/multiple-work-trees cleanup Nguyễn Thái Ngọc Duy
                   ` (8 preceding siblings ...)
  2014-08-30  8:33 ` [PATCH 09/32] fast-import: use git_path() for accessing .git dir instead of get_git_dir() Nguyễn Thái Ngọc Duy
@ 2014-08-30  8:33 ` Nguyễn Thái Ngọc Duy
  2014-08-30  8:33 ` [PATCH 11/32] $GIT_COMMON_DIR: a new environment variable Nguyễn Thái Ngọc Duy
                   ` (24 subsequent siblings)
  34 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-08-30  8:33 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 builtin/commit.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/builtin/commit.c b/builtin/commit.c
index 461c3b1..4b9f012 100644
--- a/builtin/commit.c
+++ b/builtin/commit.c
@@ -156,7 +156,7 @@ static void determine_whence(struct wt_status *s)
 		whence = FROM_MERGE;
 	else if (file_exists(git_path("CHERRY_PICK_HEAD"))) {
 		whence = FROM_CHERRY_PICK;
-		if (file_exists(git_path("sequencer")))
+		if (file_exists(git_path(SEQ_DIR)))
 			sequencer_in_use = 1;
 	}
 	else
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 11/32] $GIT_COMMON_DIR: a new environment variable
  2014-08-30  8:33 [PATCH 00/32] nd/multiple-work-trees cleanup Nguyễn Thái Ngọc Duy
                   ` (9 preceding siblings ...)
  2014-08-30  8:33 ` [PATCH 10/32] commit: use SEQ_DIR instead of hardcoding "sequencer" Nguyễn Thái Ngọc Duy
@ 2014-08-30  8:33 ` Nguyễn Thái Ngọc Duy
  2014-08-30  8:33 ` [PATCH 12/32] git-sh-setup.sh: use rev-parse --git-path to get $GIT_DIR/objects Nguyễn Thái Ngọc Duy
                   ` (23 subsequent siblings)
  34 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-08-30  8:33 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

This variable is intended to support multiple working directories
attached to a repository. Such a repository may have a main working
directory, created by either "git init" or "git clone" and one or more
linked working directories. These working directories and the main
repository share the same repository directory.

In linked working directories, $GIT_COMMON_DIR must be defined to point
to the real repository directory and $GIT_DIR points to an unused
subdirectory inside $GIT_COMMON_DIR. File locations inside the
repository are reorganized from the linked worktree view point:

 - worktree-specific such as HEAD, logs/HEAD, index, other top-level
   refs and unrecognized files are from $GIT_DIR.

 - the rest like objects, refs, info, hooks, packed-refs, shallow...
   are from $GIT_COMMON_DIR (except info/sparse-checkout, but that's
   a separate patch)

Scripts are supposed to retrieve paths in $GIT_DIR with "git rev-parse
--git-path", which will take care of "$GIT_DIR vs $GIT_COMMON_DIR"
business.

The redirection is done by git_path(), git_pathdup() and
strbuf_git_path(). The selected list of paths goes to $GIT_COMMON_DIR,
not the other way around in case a developer adds a new
worktree-specific file and it's accidentally promoted to be shared
across repositories (this includes unknown files added by third party
commands)

The list of known files that belong to $GIT_DIR are:

ADD_EDIT.patch BISECT_ANCESTORS_OK BISECT_EXPECTED_REV BISECT_LOG
BISECT_NAMES CHERRY_PICK_HEAD COMMIT_MSG FETCH_HEAD HEAD MERGE_HEAD
MERGE_MODE MERGE_RR NOTES_EDITMSG NOTES_MERGE_WORKTREE ORIG_HEAD
REVERT_HEAD SQUASH_MSG TAG_EDITMSG fast_import_crash_* logs/HEAD
next-index-* rebase-apply rebase-merge rsync-refs-* sequencer/*
shallow_*

Path mapping is NOT done for git_path_submodule(). Multi-checkouts are
not supported as submodules.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
---
 Documentation/git.txt                  |  8 ++++++
 Documentation/gitrepository-layout.txt | 45 +++++++++++++++++++++++++---------
 cache.h                                |  4 ++-
 environment.c                          | 28 +++++++++++++++------
 path.c                                 | 34 +++++++++++++++++++++++++
 t/t0060-path-utils.sh                  | 16 ++++++++++++
 6 files changed, 116 insertions(+), 19 deletions(-)

diff --git a/Documentation/git.txt b/Documentation/git.txt
index 7924209..749052f 100644
--- a/Documentation/git.txt
+++ b/Documentation/git.txt
@@ -788,6 +788,14 @@ Git so take care if using Cogito etc.
 	an explicit repository directory set via 'GIT_DIR' or on the
 	command line.
 
+'GIT_COMMON_DIR'::
+	If this variable is set to a path, non-worktree files that are
+	normally in $GIT_DIR will be taken from this path
+	instead. Worktree-specific files such as HEAD or index are
+	taken from $GIT_DIR. See linkgit:gitrepository-layout[5] for
+	details. This variable has lower precedence than other path
+	variables such as GIT_INDEX_FILE, GIT_OBJECT_DIRECTORY...
+
 Git Commits
 ~~~~~~~~~~~
 'GIT_AUTHOR_NAME'::
diff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt
index 17d2ea6..b88ac65 100644
--- a/Documentation/gitrepository-layout.txt
+++ b/Documentation/gitrepository-layout.txt
@@ -46,6 +46,9 @@ of incomplete object store is not suitable to be published for
 use with dumb transports but otherwise is OK as long as
 `objects/info/alternates` points at the object stores it
 borrows from.
++
+This directory is ignored if $GIT_COMMON_DIR is set and
+"$GIT_COMMON_DIR/objects" will be used instead.
 
 objects/[0-9a-f][0-9a-f]::
 	A newly created object is stored in its own file.
@@ -92,7 +95,8 @@ refs::
 	References are stored in subdirectories of this
 	directory.  The 'git prune' command knows to preserve
 	objects reachable from refs found in this directory and
-	its subdirectories.
+	its subdirectories. This directory is ignored if $GIT_COMMON_DIR
+	is set and "$GIT_COMMON_DIR/refs" will be used instead.
 
 refs/heads/`name`::
 	records tip-of-the-tree commit objects of branch `name`
@@ -114,7 +118,8 @@ refs/replace/`<obj-sha1>`::
 packed-refs::
 	records the same information as refs/heads/, refs/tags/,
 	and friends record in a more efficient way.  See
-	linkgit:git-pack-refs[1].
+	linkgit:git-pack-refs[1]. This file is ignored if $GIT_COMMON_DIR
+	is set and "$GIT_COMMON_DIR/packed-refs" will be used instead.
 
 HEAD::
 	A symref (see glossary) to the `refs/heads/` namespace
@@ -133,6 +138,11 @@ being a symref to point at the current branch.  Such a state
 is often called 'detached HEAD.'  See linkgit:git-checkout[1]
 for details.
 
+config::
+	Repository specific configuration file. This file is ignored
+	if $GIT_COMMON_DIR is set and "$GIT_COMMON_DIR/config" will be
+	used instead.
+
 branches::
 	A slightly deprecated way to store shorthands to be used
 	to specify a URL to 'git fetch', 'git pull' and 'git push'.
@@ -140,7 +150,10 @@ branches::
 	'name' can be given to these commands in place of
 	'repository' argument.  See the REMOTES section in
 	linkgit:git-fetch[1] for details.  This mechanism is legacy
-	and not likely to be found in modern repositories.
+	and not likely to be found in modern repositories. This
+	directory is ignored if $GIT_COMMON_DIR is set and
+	"$GIT_COMMON_DIR/branches" will be used instead.
+
 
 hooks::
 	Hooks are customization scripts used by various Git
@@ -149,7 +162,9 @@ hooks::
 	default.  To enable, the `.sample` suffix has to be
 	removed from the filename by renaming.
 	Read linkgit:githooks[5] for more details about
-	each hook.
+	each hook. This directory is ignored if $GIT_COMMON_DIR is set
+	and "$GIT_COMMON_DIR/hooks" will be used instead.
+
 
 index::
 	The current index file for the repository.  It is
@@ -157,7 +172,8 @@ index::
 
 info::
 	Additional information about the repository is recorded
-	in this directory.
+	in this directory. This directory is ignored if $GIT_COMMON_DIR
+	is set and "$GIT_COMMON_DIR/index" will be used instead.
 
 info/refs::
 	This file helps dumb transports discover what refs are
@@ -197,12 +213,15 @@ remotes::
 	when interacting with remote repositories via 'git fetch',
 	'git pull' and 'git push' commands.  See the REMOTES section
 	in linkgit:git-fetch[1] for details.  This mechanism is legacy
-	and not likely to be found in modern repositories.
+	and not likely to be found in modern repositories. This
+	directory is ignored if $GIT_COMMON_DIR is set and
+	"$GIT_COMMON_DIR/remotes" will be used instead.
 
 logs::
-	Records of changes made to refs are stored in this
-	directory.  See linkgit:git-update-ref[1]
-	for more information.
+	Records of changes made to refs are stored in this directory.
+	See linkgit:git-update-ref[1] for more information. This
+	directory is ignored $GIT_COMMON_DIR is set and
+	"$GIT_COMMON_DIR/logs" will be used instead.
 
 logs/refs/heads/`name`::
 	Records all changes made to the branch tip named `name`.
@@ -213,10 +232,14 @@ logs/refs/tags/`name`::
 shallow::
 	This is similar to `info/grafts` but is internally used
 	and maintained by shallow clone mechanism.  See `--depth`
-	option to linkgit:git-clone[1] and linkgit:git-fetch[1].
+	option to linkgit:git-clone[1] and linkgit:git-fetch[1]. This
+	file is ignored if $GIT_COMMON_DIR is set and
+	"$GIT_COMMON_DIR/shallow" will be used instead.
 
 modules::
-	Contains the git-repositories of the submodules.
+	Contains the git-repositories of the submodules. This
+	directory is ignored if $GIT_COMMON_DIR is set and
+	"$GIT_COMMON_DIR/modules" will be used instead.
 
 SEE ALSO
 --------
diff --git a/cache.h b/cache.h
index 0128b9a..b606ee4 100644
--- a/cache.h
+++ b/cache.h
@@ -362,6 +362,7 @@ static inline enum object_type object_type(unsigned int mode)
 
 /* Double-check local_repo_env below if you add to this list. */
 #define GIT_DIR_ENVIRONMENT "GIT_DIR"
+#define GIT_COMMON_DIR_ENVIRONMENT "GIT_COMMON_DIR"
 #define GIT_NAMESPACE_ENVIRONMENT "GIT_NAMESPACE"
 #define GIT_WORK_TREE_ENVIRONMENT "GIT_WORK_TREE"
 #define GIT_PREFIX_ENVIRONMENT "GIT_PREFIX"
@@ -415,6 +416,7 @@ extern int is_inside_git_dir(void);
 extern char *git_work_tree_cfg;
 extern int is_inside_work_tree(void);
 extern const char *get_git_dir(void);
+extern const char *get_git_common_dir(void);
 extern int is_git_directory(const char *path);
 extern char *get_object_directory(void);
 extern char *get_index_file(void);
@@ -612,7 +614,7 @@ extern int fsync_object_files;
 extern int core_preload_index;
 extern int core_apply_sparse_checkout;
 extern int precomposed_unicode;
-extern int git_db_env, git_index_env, git_graft_env;
+extern int git_db_env, git_index_env, git_graft_env, git_common_dir_env;
 
 /*
  * The character that begins a commented line in user-editable file
diff --git a/environment.c b/environment.c
index fee12a6..78a07e4 100644
--- a/environment.c
+++ b/environment.c
@@ -81,9 +81,9 @@ static char *work_tree;
 static const char *namespace;
 static size_t namespace_len;
 
-static const char *git_dir;
+static const char *git_dir, *git_common_dir;
 static char *git_object_dir, *git_index_file, *git_graft_file;
-int git_db_env, git_index_env, git_graft_env;
+int git_db_env, git_index_env, git_graft_env, git_common_dir_env;
 
 /*
  * Repository-local GIT_* environment variables; see cache.h for details.
@@ -125,8 +125,8 @@ static char *expand_namespace(const char *raw_namespace)
 	return strbuf_detach(&buf, NULL);
 }
 
-static char *git_path_from_env(const char *envvar, const char *path,
-			       int *fromenv)
+static char *git_path_from_env(const char *envvar, const char *git_dir,
+			       const char *path, int *fromenv)
 {
 	const char *value = getenv(envvar);
 	if (!value) {
@@ -149,9 +149,18 @@ static void setup_git_env(void)
 		git_dir = DEFAULT_GIT_DIR_ENVIRONMENT;
 	gitfile = read_gitfile(git_dir);
 	git_dir = xstrdup(gitfile ? gitfile : git_dir);
-	git_object_dir = git_path_from_env(DB_ENVIRONMENT, "objects", &git_db_env);
-	git_index_file = git_path_from_env(INDEX_ENVIRONMENT, "index", &git_index_env);
-	git_graft_file = git_path_from_env(GRAFT_ENVIRONMENT, "info/grafts", &git_graft_env);
+	git_common_dir = getenv(GIT_COMMON_DIR_ENVIRONMENT);
+	if (git_common_dir) {
+		git_common_dir_env = 1;
+		git_common_dir = xstrdup(git_common_dir);
+	} else
+		git_common_dir = git_dir;
+	git_object_dir = git_path_from_env(DB_ENVIRONMENT, git_common_dir,
+					   "objects", &git_db_env);
+	git_index_file = git_path_from_env(INDEX_ENVIRONMENT, git_dir,
+					   "index", &git_index_env);
+	git_graft_file = git_path_from_env(GRAFT_ENVIRONMENT, git_common_dir,
+					   "info/grafts", &git_graft_env);
 	if (getenv(NO_REPLACE_OBJECTS_ENVIRONMENT))
 		check_replace_refs = 0;
 	namespace = expand_namespace(getenv(GIT_NAMESPACE_ENVIRONMENT));
@@ -174,6 +183,11 @@ const char *get_git_dir(void)
 	return git_dir;
 }
 
+const char *get_git_common_dir(void)
+{
+	return git_common_dir;
+}
+
 const char *get_git_namespace(void)
 {
 	if (!namespace)
diff --git a/path.c b/path.c
index 3deb80c..8a6586c 100644
--- a/path.c
+++ b/path.c
@@ -90,6 +90,38 @@ static void replace_dir(struct strbuf *buf, int len, const char *newdir)
 		buf->buf[newlen] = '/';
 }
 
+static const char *common_list[] = {
+	"/branches", "/hooks", "/info", "/logs", "/lost-found", "/modules",
+	"/objects", "/refs", "/remotes", "/rr-cache", "/svn",
+	"config", "gc.pid", "packed-refs", "shallow",
+	NULL
+};
+
+static void update_common_dir(struct strbuf *buf, int git_dir_len)
+{
+	char *base = buf->buf + git_dir_len;
+	const char **p;
+
+	if (is_dir_file(base, "logs", "HEAD"))
+		return;	/* keep this in $GIT_DIR */
+	for (p = common_list; *p; p++) {
+		const char *path = *p;
+		int is_dir = 0;
+		if (*path == '/') {
+			path++;
+			is_dir = 1;
+		}
+		if (is_dir && dir_prefix(base, path)) {
+			replace_dir(buf, git_dir_len, get_git_common_dir());
+			return;
+		}
+		if (!is_dir && !strcmp(base, path)) {
+			replace_dir(buf, git_dir_len, get_git_common_dir());
+			return;
+		}
+	}
+}
+
 static void adjust_git_path(struct strbuf *buf, int git_dir_len)
 {
 	const char *base = buf->buf + git_dir_len;
@@ -101,6 +133,8 @@ static void adjust_git_path(struct strbuf *buf, int git_dir_len)
 			      get_index_file(), strlen(get_index_file()));
 	else if (git_db_env && dir_prefix(base, "objects"))
 		replace_dir(buf, git_dir_len + 7, get_object_directory());
+	else if (git_common_dir_env)
+		update_common_dir(buf, git_dir_len);
 }
 
 static void do_git_path(struct strbuf *buf, const char *fmt, va_list args)
diff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh
index 33d2818..f5d6f80 100755
--- a/t/t0060-path-utils.sh
+++ b/t/t0060-path-utils.sh
@@ -262,5 +262,21 @@ test_expect_success 'setup fake objects directory foo' 'mkdir foo'
 test_git_path GIT_OBJECT_DIRECTORY=foo objects foo
 test_git_path GIT_OBJECT_DIRECTORY=foo objects/foo foo/foo
 test_git_path GIT_OBJECT_DIRECTORY=foo objects2 .git/objects2
+test_expect_success 'setup common repository' 'git --git-dir=bar init'
+test_git_path GIT_COMMON_DIR=bar index                    .git/index
+test_git_path GIT_COMMON_DIR=bar HEAD                     .git/HEAD
+test_git_path GIT_COMMON_DIR=bar logs/HEAD                .git/logs/HEAD
+test_git_path GIT_COMMON_DIR=bar objects                  bar/objects
+test_git_path GIT_COMMON_DIR=bar objects/bar              bar/objects/bar
+test_git_path GIT_COMMON_DIR=bar info/exclude             bar/info/exclude
+test_git_path GIT_COMMON_DIR=bar info/grafts              bar/info/grafts
+test_git_path GIT_COMMON_DIR=bar remotes/bar              bar/remotes/bar
+test_git_path GIT_COMMON_DIR=bar branches/bar             bar/branches/bar
+test_git_path GIT_COMMON_DIR=bar logs/refs/heads/master   bar/logs/refs/heads/master
+test_git_path GIT_COMMON_DIR=bar refs/heads/master        bar/refs/heads/master
+test_git_path GIT_COMMON_DIR=bar hooks/me                 bar/hooks/me
+test_git_path GIT_COMMON_DIR=bar config                   bar/config
+test_git_path GIT_COMMON_DIR=bar packed-refs              bar/packed-refs
+test_git_path GIT_COMMON_DIR=bar shallow                  bar/shallow
 
 test_done
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 12/32] git-sh-setup.sh: use rev-parse --git-path to get $GIT_DIR/objects
  2014-08-30  8:33 [PATCH 00/32] nd/multiple-work-trees cleanup Nguyễn Thái Ngọc Duy
                   ` (10 preceding siblings ...)
  2014-08-30  8:33 ` [PATCH 11/32] $GIT_COMMON_DIR: a new environment variable Nguyễn Thái Ngọc Duy
@ 2014-08-30  8:33 ` Nguyễn Thái Ngọc Duy
  2014-08-30  8:33 ` [PATCH 13/32] *.sh: avoid hardcoding $GIT_DIR/hooks/ Nguyễn Thái Ngọc Duy
                   ` (22 subsequent siblings)
  34 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-08-30  8:33 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

If $GIT_COMMON_DIR is set, $GIT_OBJECT_DIRECTORY should be
$GIT_COMMON_DIR/objects, not $GIT_DIR/objects. Just let rev-parse
--git-path handle it.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 git-sh-setup.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git-sh-setup.sh b/git-sh-setup.sh
index 9447980..d3dbb2f 100644
--- a/git-sh-setup.sh
+++ b/git-sh-setup.sh
@@ -345,7 +345,7 @@ then
 		echo >&2 "Unable to determine absolute path of git directory"
 		exit 1
 	}
-	: ${GIT_OBJECT_DIRECTORY="$GIT_DIR/objects"}
+	: ${GIT_OBJECT_DIRECTORY="$(git rev-parse --git-path objects)"}
 fi
 
 peel_committish () {
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 13/32] *.sh: avoid hardcoding $GIT_DIR/hooks/...
  2014-08-30  8:33 [PATCH 00/32] nd/multiple-work-trees cleanup Nguyễn Thái Ngọc Duy
                   ` (11 preceding siblings ...)
  2014-08-30  8:33 ` [PATCH 12/32] git-sh-setup.sh: use rev-parse --git-path to get $GIT_DIR/objects Nguyễn Thái Ngọc Duy
@ 2014-08-30  8:33 ` Nguyễn Thái Ngọc Duy
  2014-08-30  8:33 ` [PATCH 14/32] git-stash: avoid hardcoding $GIT_DIR/logs/ Nguyễn Thái Ngọc Duy
                   ` (21 subsequent siblings)
  34 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-08-30  8:33 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

If $GIT_COMMON_DIR is set, it should be $GIT_COMMON_DIR/hooks/, not
$GIT_DIR/hooks/. Just let rev-parse --git-path handle it.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 git-am.sh                              | 22 +++++++++++-----------
 git-rebase--interactive.sh             |  6 +++---
 git-rebase--merge.sh                   |  6 ++----
 git-rebase.sh                          |  4 ++--
 templates/hooks--applypatch-msg.sample |  4 ++--
 templates/hooks--pre-applypatch.sample |  4 ++--
 6 files changed, 22 insertions(+), 24 deletions(-)

diff --git a/git-am.sh b/git-am.sh
index ee61a77..66803d1 100755
--- a/git-am.sh
+++ b/git-am.sh
@@ -810,10 +810,10 @@ To restore the original branch and stop patching run \"\$cmdline --abort\"."
 		continue
 	fi
 
-	if test -x "$GIT_DIR"/hooks/applypatch-msg
+	hook="$(git rev-parse --git-path hooks/applypatch-msg)"
+	if test -x "$hook"
 	then
-		"$GIT_DIR"/hooks/applypatch-msg "$dotest/final-commit" ||
-		stop_here $this
+		"$hook" "$dotest/final-commit" || stop_here $this
 	fi
 
 	if test -f "$dotest/final-commit"
@@ -887,9 +887,10 @@ did you forget to use 'git add'?"
 		stop_here_user_resolve $this
 	fi
 
-	if test -x "$GIT_DIR"/hooks/pre-applypatch
+	hook="$(git rev-parse --git-path hooks/pre-applypatch)"
+	if test -x "$hook"
 	then
-		"$GIT_DIR"/hooks/pre-applypatch || stop_here $this
+		"$hook" || stop_here $this
 	fi
 
 	tree=$(git write-tree) &&
@@ -916,18 +917,17 @@ did you forget to use 'git add'?"
 		echo "$(cat "$dotest/original-commit") $commit" >> "$dotest/rewritten"
 	fi
 
-	if test -x "$GIT_DIR"/hooks/post-applypatch
-	then
-		"$GIT_DIR"/hooks/post-applypatch
-	fi
+	hook="$(git rev-parse --git-path hooks/post-applypatch)"
+	test -x "$hook" && "$hook"
 
 	go_next
 done
 
 if test -s "$dotest"/rewritten; then
     git notes copy --for-rewrite=rebase < "$dotest"/rewritten
-    if test -x "$GIT_DIR"/hooks/post-rewrite; then
-	"$GIT_DIR"/hooks/post-rewrite rebase < "$dotest"/rewritten
+    hook="$(git rev-parse --git-path hooks/post-rewrite)"
+    if test -x "$hook"; then
+	"$hook" rebase < "$dotest"/rewritten
     fi
 fi
 
diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
index 7e1eda0..e8995f9 100644
--- a/git-rebase--interactive.sh
+++ b/git-rebase--interactive.sh
@@ -642,9 +642,9 @@ do_next () {
 		git notes copy --for-rewrite=rebase < "$rewritten_list" ||
 		true # we don't care if this copying failed
 	} &&
-	if test -x "$GIT_DIR"/hooks/post-rewrite &&
-		test -s "$rewritten_list"; then
-		"$GIT_DIR"/hooks/post-rewrite rebase < "$rewritten_list"
+	hook="$(git rev-parse --git-path hooks/post-rewrite)"
+	if test -x "$hook" && test -s "$rewritten_list"; then
+		"$hook" rebase < "$rewritten_list"
 		true # we don't care if this hook failed
 	fi &&
 	warn "Successfully rebased and updated $head_name."
diff --git a/git-rebase--merge.sh b/git-rebase--merge.sh
index d3fb67d..2cc2a6d 100644
--- a/git-rebase--merge.sh
+++ b/git-rebase--merge.sh
@@ -94,10 +94,8 @@ finish_rb_merge () {
 	if test -s "$state_dir"/rewritten
 	then
 		git notes copy --for-rewrite=rebase <"$state_dir"/rewritten
-		if test -x "$GIT_DIR"/hooks/post-rewrite
-		then
-			"$GIT_DIR"/hooks/post-rewrite rebase <"$state_dir"/rewritten
-		fi
+		hook="$(git rev-parse --git-path hooks/post-rewrite)"
+		test -x "$hook" && "$hook" rebase <"$state_dir"/rewritten
 	fi
 	say All done.
 }
diff --git a/git-rebase.sh b/git-rebase.sh
index 06c810b..d60e710 100755
--- a/git-rebase.sh
+++ b/git-rebase.sh
@@ -201,9 +201,9 @@ run_specific_rebase () {
 
 run_pre_rebase_hook () {
 	if test -z "$ok_to_skip_pre_rebase" &&
-	   test -x "$GIT_DIR/hooks/pre-rebase"
+	   test -x "$(git rev-parse --git-path hooks/pre-rebase)"
 	then
-		"$GIT_DIR/hooks/pre-rebase" ${1+"$@"} ||
+		"$(git rev-parse --git-path hooks/pre-rebase)" ${1+"$@"} ||
 		die "$(gettext "The pre-rebase hook refused to rebase.")"
 	fi
 }
diff --git a/templates/hooks--applypatch-msg.sample b/templates/hooks--applypatch-msg.sample
index 8b2a2fe..a5d7b84 100755
--- a/templates/hooks--applypatch-msg.sample
+++ b/templates/hooks--applypatch-msg.sample
@@ -10,6 +10,6 @@
 # To enable this hook, rename this file to "applypatch-msg".
 
 . git-sh-setup
-test -x "$GIT_DIR/hooks/commit-msg" &&
-	exec "$GIT_DIR/hooks/commit-msg" ${1+"$@"}
+commitmsg="$(git rev-parse --git-path hooks/commit-msg)"
+test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"}
 :
diff --git a/templates/hooks--pre-applypatch.sample b/templates/hooks--pre-applypatch.sample
index b1f187c..4142082 100755
--- a/templates/hooks--pre-applypatch.sample
+++ b/templates/hooks--pre-applypatch.sample
@@ -9,6 +9,6 @@
 # To enable this hook, rename this file to "pre-applypatch".
 
 . git-sh-setup
-test -x "$GIT_DIR/hooks/pre-commit" &&
-	exec "$GIT_DIR/hooks/pre-commit" ${1+"$@"}
+precommit="$(git rev-parse --git-path hooks/pre-commit)"
+test -x "$precommit" && exec "$precommit" ${1+"$@"}
 :
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 14/32] git-stash: avoid hardcoding $GIT_DIR/logs/....
  2014-08-30  8:33 [PATCH 00/32] nd/multiple-work-trees cleanup Nguyễn Thái Ngọc Duy
                   ` (12 preceding siblings ...)
  2014-08-30  8:33 ` [PATCH 13/32] *.sh: avoid hardcoding $GIT_DIR/hooks/ Nguyễn Thái Ngọc Duy
@ 2014-08-30  8:33 ` Nguyễn Thái Ngọc Duy
  2014-08-30  8:33 ` [PATCH 15/32] setup.c: convert is_git_directory() to use strbuf Nguyễn Thái Ngọc Duy
                   ` (20 subsequent siblings)
  34 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-08-30  8:33 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 git-stash.sh | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/git-stash.sh b/git-stash.sh
index 393e1ec..41f8f6b 100755
--- a/git-stash.sh
+++ b/git-stash.sh
@@ -184,7 +184,7 @@ store_stash () {
 	fi
 
 	# Make sure the reflog for stash is kept.
-	: >>"$GIT_DIR/logs/$ref_stash"
+	: >>"$(git rev-parse --git-path logs/$ref_stash)"
 	git update-ref -m "$stash_msg" $ref_stash $w_commit
 	ret=$?
 	test $ret != 0 && test -z $quiet &&
@@ -259,7 +259,7 @@ save_stash () {
 		say "$(gettext "No local changes to save")"
 		exit 0
 	fi
-	test -f "$GIT_DIR/logs/$ref_stash" ||
+	test -f "$(git rev-parse --git-path logs/$ref_stash)" ||
 		clear_stash || die "$(gettext "Cannot initialize stash")"
 
 	create_stash "$stash_msg" $untracked
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 15/32] setup.c: convert is_git_directory() to use strbuf
  2014-08-30  8:33 [PATCH 00/32] nd/multiple-work-trees cleanup Nguyễn Thái Ngọc Duy
                   ` (13 preceding siblings ...)
  2014-08-30  8:33 ` [PATCH 14/32] git-stash: avoid hardcoding $GIT_DIR/logs/ Nguyễn Thái Ngọc Duy
@ 2014-08-30  8:33 ` Nguyễn Thái Ngọc Duy
  2014-08-30  8:33 ` [PATCH 16/32] setup.c: detect $GIT_COMMON_DIR in is_git_directory() Nguyễn Thái Ngọc Duy
                   ` (19 subsequent siblings)
  34 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-08-30  8:33 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 setup.c | 37 +++++++++++++++++++++----------------
 1 file changed, 21 insertions(+), 16 deletions(-)

diff --git a/setup.c b/setup.c
index 0a22f8b..425fd79 100644
--- a/setup.c
+++ b/setup.c
@@ -238,31 +238,36 @@ void verify_non_filename(const char *prefix, const char *arg)
  */
 int is_git_directory(const char *suspect)
 {
-	char path[PATH_MAX];
-	size_t len = strlen(suspect);
+	struct strbuf path = STRBUF_INIT;
+	int ret = 0;
+	size_t len;
 
-	if (PATH_MAX <= len + strlen("/objects"))
-		die("Too long path: %.*s", 60, suspect);
-	strcpy(path, suspect);
+	strbuf_addstr(&path, suspect);
+	len = path.len;
 	if (getenv(DB_ENVIRONMENT)) {
 		if (access(getenv(DB_ENVIRONMENT), X_OK))
-			return 0;
+			goto done;
 	}
 	else {
-		strcpy(path + len, "/objects");
-		if (access(path, X_OK))
-			return 0;
+		strbuf_addstr(&path, "/objects");
+		if (access(path.buf, X_OK))
+			goto done;
 	}
 
-	strcpy(path + len, "/refs");
-	if (access(path, X_OK))
-		return 0;
+	strbuf_setlen(&path, len);
+	strbuf_addstr(&path, "/refs");
+	if (access(path.buf, X_OK))
+		goto done;
 
-	strcpy(path + len, "/HEAD");
-	if (validate_headref(path))
-		return 0;
+	strbuf_setlen(&path, len);
+	strbuf_addstr(&path, "/HEAD");
+	if (validate_headref(path.buf))
+		goto done;
 
-	return 1;
+	ret = 1;
+done:
+	strbuf_release(&path);
+	return ret;
 }
 
 int is_inside_git_dir(void)
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 16/32] setup.c: detect $GIT_COMMON_DIR in is_git_directory()
  2014-08-30  8:33 [PATCH 00/32] nd/multiple-work-trees cleanup Nguyễn Thái Ngọc Duy
                   ` (14 preceding siblings ...)
  2014-08-30  8:33 ` [PATCH 15/32] setup.c: convert is_git_directory() to use strbuf Nguyễn Thái Ngọc Duy
@ 2014-08-30  8:33 ` Nguyễn Thái Ngọc Duy
  2014-08-30  8:33 ` [PATCH 17/32] setup.c: convert check_repository_format_gently to use strbuf Nguyễn Thái Ngọc Duy
                   ` (18 subsequent siblings)
  34 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-08-30  8:33 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

If the file "$GIT_DIR/commondir" exists, it contains the value of
$GIT_COMMON_DIR.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 Documentation/gitrepository-layout.txt |  7 ++++++
 setup.c                                | 43 +++++++++++++++++++++++++++++-----
 2 files changed, 44 insertions(+), 6 deletions(-)

diff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt
index b88ac65..58d1087 100644
--- a/Documentation/gitrepository-layout.txt
+++ b/Documentation/gitrepository-layout.txt
@@ -236,6 +236,13 @@ shallow::
 	file is ignored if $GIT_COMMON_DIR is set and
 	"$GIT_COMMON_DIR/shallow" will be used instead.
 
+commondir::
+	If this file exists, $GIT_COMMON_DIR (see linkgit:git[1]) will
+	be set to the path specified in this file if it is not
+	explicitly set. If the specified path is relative, it is
+	relative to $GIT_DIR. The repository with commondir is
+	incomplete without the repository pointed by "commondir".
+
 modules::
 	Contains the git-repositories of the submodules. This
 	directory is ignored if $GIT_COMMON_DIR is set and
diff --git a/setup.c b/setup.c
index 425fd79..176d505 100644
--- a/setup.c
+++ b/setup.c
@@ -224,6 +224,33 @@ void verify_non_filename(const char *prefix, const char *arg)
 	    "'git <command> [<revision>...] -- [<file>...]'", arg);
 }
 
+static void get_common_dir(struct strbuf *sb, const char *gitdir)
+{
+	struct strbuf data = STRBUF_INIT;
+	struct strbuf path = STRBUF_INIT;
+	const char *git_common_dir = getenv(GIT_COMMON_DIR_ENVIRONMENT);
+	if (git_common_dir) {
+		strbuf_addstr(sb, git_common_dir);
+		return;
+	}
+	strbuf_addf(&path, "%s/commondir", gitdir);
+	if (file_exists(path.buf)) {
+		if (strbuf_read_file(&data, path.buf, 0) <= 0)
+			die_errno(_("failed to read %s"), path.buf);
+		while (data.len && (data.buf[data.len - 1] == '\n' ||
+				    data.buf[data.len - 1] == '\r'))
+			data.len--;
+		data.buf[data.len] = '\0';
+		strbuf_reset(&path);
+		if (!is_absolute_path(data.buf))
+			strbuf_addf(&path, "%s/", gitdir);
+		strbuf_addbuf(&path, &data);
+		strbuf_addstr(sb, real_path(path.buf));
+	} else
+		strbuf_addstr(sb, gitdir);
+	strbuf_release(&data);
+	strbuf_release(&path);
+}
 
 /*
  * Test if it looks like we're at a git directory.
@@ -242,13 +269,22 @@ int is_git_directory(const char *suspect)
 	int ret = 0;
 	size_t len;
 
-	strbuf_addstr(&path, suspect);
+	/* Check worktree-related signatures */
+	strbuf_addf(&path, "%s/HEAD", suspect);
+	if (validate_headref(path.buf))
+		goto done;
+
+	strbuf_reset(&path);
+	get_common_dir(&path, suspect);
 	len = path.len;
+
+	/* Check non-worktree-related signatures */
 	if (getenv(DB_ENVIRONMENT)) {
 		if (access(getenv(DB_ENVIRONMENT), X_OK))
 			goto done;
 	}
 	else {
+		strbuf_setlen(&path, len);
 		strbuf_addstr(&path, "/objects");
 		if (access(path.buf, X_OK))
 			goto done;
@@ -259,11 +295,6 @@ int is_git_directory(const char *suspect)
 	if (access(path.buf, X_OK))
 		goto done;
 
-	strbuf_setlen(&path, len);
-	strbuf_addstr(&path, "/HEAD");
-	if (validate_headref(path.buf))
-		goto done;
-
 	ret = 1;
 done:
 	strbuf_release(&path);
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 17/32] setup.c: convert check_repository_format_gently to use strbuf
  2014-08-30  8:33 [PATCH 00/32] nd/multiple-work-trees cleanup Nguyễn Thái Ngọc Duy
                   ` (15 preceding siblings ...)
  2014-08-30  8:33 ` [PATCH 16/32] setup.c: detect $GIT_COMMON_DIR in is_git_directory() Nguyễn Thái Ngọc Duy
@ 2014-08-30  8:33 ` Nguyễn Thái Ngọc Duy
  2014-08-30  8:33 ` [PATCH 18/32] setup.c: detect $GIT_COMMON_DIR check_repository_format_gently() Nguyễn Thái Ngọc Duy
                   ` (17 subsequent siblings)
  34 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-08-30  8:33 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 setup.c | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)

diff --git a/setup.c b/setup.c
index 176d505..a17389f 100644
--- a/setup.c
+++ b/setup.c
@@ -342,7 +342,9 @@ void setup_work_tree(void)
 
 static int check_repository_format_gently(const char *gitdir, int *nongit_ok)
 {
-	char repo_config[PATH_MAX+1];
+	struct strbuf sb = STRBUF_INIT;
+	const char *repo_config;
+	int ret = 0;
 
 	/*
 	 * git_config() can't be used here because it calls git_pathdup()
@@ -353,7 +355,8 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)
 	 * Use a gentler version of git_config() to check if this repo
 	 * is a good one.
 	 */
-	snprintf(repo_config, PATH_MAX, "%s/config", gitdir);
+	strbuf_addf(&sb, "%s/config", gitdir);
+	repo_config = sb.buf;
 	git_config_early(check_repository_format_version, NULL, repo_config);
 	if (GIT_REPO_VERSION < repository_format_version) {
 		if (!nongit_ok)
@@ -363,9 +366,10 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)
 			GIT_REPO_VERSION, repository_format_version);
 		warning("Please upgrade Git");
 		*nongit_ok = -1;
-		return -1;
+		ret = -1;
 	}
-	return 0;
+	strbuf_release(&sb);
+	return ret;
 }
 
 /*
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 18/32] setup.c: detect $GIT_COMMON_DIR check_repository_format_gently()
  2014-08-30  8:33 [PATCH 00/32] nd/multiple-work-trees cleanup Nguyễn Thái Ngọc Duy
                   ` (16 preceding siblings ...)
  2014-08-30  8:33 ` [PATCH 17/32] setup.c: convert check_repository_format_gently to use strbuf Nguyễn Thái Ngọc Duy
@ 2014-08-30  8:33 ` Nguyễn Thái Ngọc Duy
  2014-08-30  8:33 ` [PATCH 19/32] setup.c: support multi-checkout repo setup Nguyễn Thái Ngọc Duy
                   ` (16 subsequent siblings)
  34 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-08-30  8:33 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 setup.c | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/setup.c b/setup.c
index a17389f..79f79f2 100644
--- a/setup.c
+++ b/setup.c
@@ -346,6 +346,10 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)
 	const char *repo_config;
 	int ret = 0;
 
+	get_common_dir(&sb, gitdir);
+	strbuf_addstr(&sb, "/config");
+	repo_config = sb.buf;
+
 	/*
 	 * git_config() can't be used here because it calls git_pathdup()
 	 * to get $GIT_CONFIG/config. That call will make setup_git_env()
@@ -355,8 +359,6 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)
 	 * Use a gentler version of git_config() to check if this repo
 	 * is a good one.
 	 */
-	strbuf_addf(&sb, "%s/config", gitdir);
-	repo_config = sb.buf;
 	git_config_early(check_repository_format_version, NULL, repo_config);
 	if (GIT_REPO_VERSION < repository_format_version) {
 		if (!nongit_ok)
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 19/32] setup.c: support multi-checkout repo setup
  2014-08-30  8:33 [PATCH 00/32] nd/multiple-work-trees cleanup Nguyễn Thái Ngọc Duy
                   ` (17 preceding siblings ...)
  2014-08-30  8:33 ` [PATCH 18/32] setup.c: detect $GIT_COMMON_DIR check_repository_format_gently() Nguyễn Thái Ngọc Duy
@ 2014-08-30  8:33 ` Nguyễn Thái Ngọc Duy
  2014-08-30  8:33 ` [PATCH 20/32] wrapper.c: wrapper to open a file, fprintf then close Nguyễn Thái Ngọc Duy
                   ` (15 subsequent siblings)
  34 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-08-30  8:33 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

The repo setup procedure is updated to detect $GIT_DIR/commondir and
set $GIT_COMMON_DIR properly.

The core.worktree is ignored when $GIT_COMMON_DIR is set. This is
because the config file is shared in multi-checkout setup, but
checkout directories _are_ different. Making core.worktree effective
in all checkouts mean it's back to a single checkout.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 Documentation/config.txt        |  2 ++
 Documentation/git-rev-parse.txt |  3 ++
 builtin/rev-parse.c             |  4 +++
 cache.h                         |  1 +
 environment.c                   |  8 ++---
 setup.c                         | 33 +++++++++++++-----
 t/t1501-worktree.sh             | 76 +++++++++++++++++++++++++++++++++++++++++
 t/t1510-repo-setup.sh           |  1 +
 trace.c                         |  1 +
 9 files changed, 115 insertions(+), 14 deletions(-)

diff --git a/Documentation/config.txt b/Documentation/config.txt
index 1d718bd..286e539 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -380,6 +380,8 @@ false), while all other repositories are assumed to be bare (bare
 
 core.worktree::
 	Set the path to the root of the working tree.
+	If GIT_COMMON_DIR environment variable is set, core.worktree
+	is ignored and not used for determining the root of working tree.
 	This can be overridden by the GIT_WORK_TREE environment
 	variable and the '--work-tree' command-line option.
 	The value can be an absolute path or relative to the path to
diff --git a/Documentation/git-rev-parse.txt b/Documentation/git-rev-parse.txt
index 9465399..f1867d3 100644
--- a/Documentation/git-rev-parse.txt
+++ b/Documentation/git-rev-parse.txt
@@ -215,6 +215,9 @@ If `$GIT_DIR` is not defined and the current directory
 is not detected to lie in a Git repository or work tree
 print a message to stderr and exit with nonzero status.
 
+--git-common-dir::
+	Show `$GIT_COMMON_DIR` if defined, else `$GIT_DIR`.
+
 --is-inside-git-dir::
 	When the current working directory is below the repository
 	directory print "true", otherwise "false".
diff --git a/builtin/rev-parse.c b/builtin/rev-parse.c
index 7606d43..29475c5 100644
--- a/builtin/rev-parse.c
+++ b/builtin/rev-parse.c
@@ -757,6 +757,10 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)
 				printf("%s%s.git\n", cwd, len && cwd[len-1] != '/' ? "/" : "");
 				continue;
 			}
+			if (!strcmp(arg, "--git-common-dir")) {
+				puts(get_git_common_dir());
+				continue;
+			}
 			if (!strcmp(arg, "--resolve-git-dir")) {
 				const char *gitdir = argv[++i];
 				if (!gitdir)
diff --git a/cache.h b/cache.h
index b606ee4..e3ff7dc 100644
--- a/cache.h
+++ b/cache.h
@@ -422,6 +422,7 @@ extern char *get_object_directory(void);
 extern char *get_index_file(void);
 extern char *get_graft_file(void);
 extern int set_git_dir(const char *path);
+extern int get_common_dir(struct strbuf *sb, const char *gitdir);
 extern const char *get_git_namespace(void);
 extern const char *strip_namespace(const char *namespaced_ref);
 extern const char *get_git_work_tree(void);
diff --git a/environment.c b/environment.c
index 78a07e4..d5b0788 100644
--- a/environment.c
+++ b/environment.c
@@ -141,6 +141,7 @@ static char *git_path_from_env(const char *envvar, const char *git_dir,
 
 static void setup_git_env(void)
 {
+	struct strbuf sb = STRBUF_INIT;
 	const char *gitfile;
 	const char *shallow_file;
 
@@ -149,12 +150,9 @@ static void setup_git_env(void)
 		git_dir = DEFAULT_GIT_DIR_ENVIRONMENT;
 	gitfile = read_gitfile(git_dir);
 	git_dir = xstrdup(gitfile ? gitfile : git_dir);
-	git_common_dir = getenv(GIT_COMMON_DIR_ENVIRONMENT);
-	if (git_common_dir) {
+	if (get_common_dir(&sb, git_dir))
 		git_common_dir_env = 1;
-		git_common_dir = xstrdup(git_common_dir);
-	} else
-		git_common_dir = git_dir;
+	git_common_dir = strbuf_detach(&sb, NULL);
 	git_object_dir = git_path_from_env(DB_ENVIRONMENT, git_common_dir,
 					   "objects", &git_db_env);
 	git_index_file = git_path_from_env(INDEX_ENVIRONMENT, git_dir,
diff --git a/setup.c b/setup.c
index 79f79f2..8f90bc3 100644
--- a/setup.c
+++ b/setup.c
@@ -224,14 +224,15 @@ void verify_non_filename(const char *prefix, const char *arg)
 	    "'git <command> [<revision>...] -- [<file>...]'", arg);
 }
 
-static void get_common_dir(struct strbuf *sb, const char *gitdir)
+int get_common_dir(struct strbuf *sb, const char *gitdir)
 {
 	struct strbuf data = STRBUF_INIT;
 	struct strbuf path = STRBUF_INIT;
 	const char *git_common_dir = getenv(GIT_COMMON_DIR_ENVIRONMENT);
+	int ret = 0;
 	if (git_common_dir) {
 		strbuf_addstr(sb, git_common_dir);
-		return;
+		return 1;
 	}
 	strbuf_addf(&path, "%s/commondir", gitdir);
 	if (file_exists(path.buf)) {
@@ -246,10 +247,12 @@ static void get_common_dir(struct strbuf *sb, const char *gitdir)
 			strbuf_addf(&path, "%s/", gitdir);
 		strbuf_addbuf(&path, &data);
 		strbuf_addstr(sb, real_path(path.buf));
+		ret = 1;
 	} else
 		strbuf_addstr(sb, gitdir);
 	strbuf_release(&data);
 	strbuf_release(&path);
+	return ret;
 }
 
 /*
@@ -340,13 +343,26 @@ void setup_work_tree(void)
 	initialized = 1;
 }
 
+static int check_repo_format(const char *var, const char *value, void *cb)
+{
+	if (strcmp(var, "core.repositoryformatversion") == 0)
+		repository_format_version = git_config_int(var, value);
+	else if (strcmp(var, "core.sharedrepository") == 0)
+		shared_repository = git_config_perm(var, value);
+	return 0;
+}
+
 static int check_repository_format_gently(const char *gitdir, int *nongit_ok)
 {
 	struct strbuf sb = STRBUF_INIT;
 	const char *repo_config;
+	config_fn_t fn;
 	int ret = 0;
 
-	get_common_dir(&sb, gitdir);
+	if (get_common_dir(&sb, gitdir))
+		fn = check_repo_format;
+	else
+		fn = check_repository_format_version;
 	strbuf_addstr(&sb, "/config");
 	repo_config = sb.buf;
 
@@ -359,7 +375,7 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)
 	 * Use a gentler version of git_config() to check if this repo
 	 * is a good one.
 	 */
-	git_config_early(check_repository_format_version, NULL, repo_config);
+	git_config_early(fn, NULL, repo_config);
 	if (GIT_REPO_VERSION < repository_format_version) {
 		if (!nongit_ok)
 			die ("Expected git repo version <= %d, found %d",
@@ -831,11 +847,10 @@ int git_config_perm(const char *var, const char *value)
 
 int check_repository_format_version(const char *var, const char *value, void *cb)
 {
-	if (strcmp(var, "core.repositoryformatversion") == 0)
-		repository_format_version = git_config_int(var, value);
-	else if (strcmp(var, "core.sharedrepository") == 0)
-		shared_repository = git_config_perm(var, value);
-	else if (strcmp(var, "core.bare") == 0) {
+	int ret = check_repo_format(var, value, cb);
+	if (ret)
+		return ret;
+	if (strcmp(var, "core.bare") == 0) {
 		is_bare_repository_cfg = git_config_bool(var, value);
 		if (is_bare_repository_cfg == 1)
 			inside_work_tree = -1;
diff --git a/t/t1501-worktree.sh b/t/t1501-worktree.sh
index 8f36aa9..e6ac7a4 100755
--- a/t/t1501-worktree.sh
+++ b/t/t1501-worktree.sh
@@ -346,4 +346,80 @@ test_expect_success 'relative $GIT_WORK_TREE and git subprocesses' '
 	test_cmp expected actual
 '
 
+test_expect_success 'Multi-worktree setup' '
+	mkdir work &&
+	mkdir -p repo.git/repos/foo &&
+	cp repo.git/HEAD repo.git/index repo.git/repos/foo &&
+	sane_unset GIT_DIR GIT_CONFIG GIT_WORK_TREE
+'
+
+test_expect_success 'GIT_DIR set (1)' '
+	echo "gitdir: repo.git/repos/foo" >gitfile &&
+	echo ../.. >repo.git/repos/foo/commondir &&
+	(
+		cd work &&
+		GIT_DIR=../gitfile git rev-parse --git-common-dir >actual &&
+		test-path-utils real_path "$TRASH_DIRECTORY/repo.git" >expect &&
+		test_cmp expect actual
+	)
+'
+
+test_expect_success 'GIT_DIR set (2)' '
+	echo "gitdir: repo.git/repos/foo" >gitfile &&
+	echo "$TRASH_DIRECTORY/repo.git" >repo.git/repos/foo/commondir &&
+	(
+		cd work &&
+		GIT_DIR=../gitfile git rev-parse --git-common-dir >actual &&
+		test-path-utils real_path "$TRASH_DIRECTORY/repo.git" >expect &&
+		test_cmp expect actual
+	)
+'
+
+test_expect_success 'Auto discovery' '
+	echo "gitdir: repo.git/repos/foo" >.git &&
+	echo ../.. >repo.git/repos/foo/commondir &&
+	(
+		cd work &&
+		git rev-parse --git-common-dir >actual &&
+		test-path-utils real_path "$TRASH_DIRECTORY/repo.git" >expect &&
+		test_cmp expect actual &&
+		echo haha >data1 &&
+		git add data1 &&
+		git ls-files --full-name :/ | grep data1 >actual &&
+		echo work/data1 >expect &&
+		test_cmp expect actual
+	)
+'
+
+test_expect_success '$GIT_DIR/common overrides core.worktree' '
+	mkdir elsewhere &&
+	git --git-dir=repo.git config core.worktree "$TRASH_DIRECTORY/elsewhere" &&
+	echo "gitdir: repo.git/repos/foo" >.git &&
+	echo ../.. >repo.git/repos/foo/commondir &&
+	(
+		cd work &&
+		git rev-parse --git-common-dir >actual &&
+		test-path-utils real_path "$TRASH_DIRECTORY/repo.git" >expect &&
+		test_cmp expect actual &&
+		echo haha >data2 &&
+		git add data2 &&
+		git ls-files --full-name :/ | grep data2 >actual &&
+		echo work/data2 >expect &&
+		test_cmp expect actual
+	)
+'
+
+test_expect_success '$GIT_WORK_TREE overrides $GIT_DIR/common' '
+	echo "gitdir: repo.git/repos/foo" >.git &&
+	echo ../.. >repo.git/repos/foo/commondir &&
+	(
+		cd work &&
+		echo haha >data3 &&
+		git --git-dir=../.git --work-tree=. add data3 &&
+		git ls-files --full-name -- :/ | grep data3 >actual &&
+		echo data3 >expect &&
+		test_cmp expect actual
+	)
+'
+
 test_done
diff --git a/t/t1510-repo-setup.sh b/t/t1510-repo-setup.sh
index e1b2a99..33c1a58 100755
--- a/t/t1510-repo-setup.sh
+++ b/t/t1510-repo-setup.sh
@@ -106,6 +106,7 @@ setup_env () {
 expect () {
 	cat >"$1/expected" <<-EOF
 	setup: git_dir: $2
+	setup: git_common_dir: $2
 	setup: worktree: $3
 	setup: cwd: $4
 	setup: prefix: $5
diff --git a/trace.c b/trace.c
index 08180a9..a594761 100644
--- a/trace.c
+++ b/trace.c
@@ -173,6 +173,7 @@ void trace_repo_setup(const char *prefix)
 		prefix = "(null)";
 
 	trace_printf_key(key, "setup: git_dir: %s\n", quote_crnl(get_git_dir()));
+	trace_printf_key(key, "setup: git_common_dir: %s\n", quote_crnl(get_git_common_dir()));
 	trace_printf_key(key, "setup: worktree: %s\n", quote_crnl(git_work_tree));
 	trace_printf_key(key, "setup: cwd: %s\n", quote_crnl(cwd));
 	trace_printf_key(key, "setup: prefix: %s\n", quote_crnl(prefix));
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 20/32] wrapper.c: wrapper to open a file, fprintf then close
  2014-08-30  8:33 [PATCH 00/32] nd/multiple-work-trees cleanup Nguyễn Thái Ngọc Duy
                   ` (18 preceding siblings ...)
  2014-08-30  8:33 ` [PATCH 19/32] setup.c: support multi-checkout repo setup Nguyễn Thái Ngọc Duy
@ 2014-08-30  8:33 ` Nguyễn Thái Ngọc Duy
  2014-08-30  8:33 ` [PATCH 21/32] use new wrapper write_file() for simple file writing Nguyễn Thái Ngọc Duy
                   ` (14 subsequent siblings)
  34 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-08-30  8:33 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 cache.h   |  2 ++
 wrapper.c | 31 +++++++++++++++++++++++++++++++
 2 files changed, 33 insertions(+)

diff --git a/cache.h b/cache.h
index e3ff7dc..bf4d15e 100644
--- a/cache.h
+++ b/cache.h
@@ -1359,6 +1359,8 @@ static inline ssize_t write_str_in_full(int fd, const char *str)
 {
 	return write_in_full(fd, str, strlen(str));
 }
+__attribute__((format (printf,3,4)))
+extern int write_file(const char *path, int fatal, const char *fmt, ...);
 
 /* pager.c */
 extern void setup_pager(void);
diff --git a/wrapper.c b/wrapper.c
index bc1bfb8..9d7b9ac 100644
--- a/wrapper.c
+++ b/wrapper.c
@@ -493,3 +493,34 @@ struct passwd *xgetpwuid_self(void)
 		    errno ? strerror(errno) : _("no such user"));
 	return pw;
 }
+
+int write_file(const char *path, int fatal, const char *fmt, ...)
+{
+	struct strbuf sb = STRBUF_INIT;
+	va_list params;
+	int fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0666);
+	if (fd < 0) {
+		if (fatal)
+			die_errno(_("could not open %s for writing"), path);
+		return -1;
+	}
+	va_start(params, fmt);
+	strbuf_vaddf(&sb, fmt, params);
+	va_end(params);
+	if (write_in_full(fd, sb.buf, sb.len) != sb.len) {
+		int err = errno;
+		close(fd);
+		strbuf_release(&sb);
+		errno = err;
+		if (fatal)
+			die_errno(_("could not write to %s"), path);
+		return -1;
+	}
+	strbuf_release(&sb);
+	if (close(fd)) {
+		if (fatal)
+			die_errno(_("could not close %s"), path);
+		return -1;
+	}
+	return 0;
+}
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 21/32] use new wrapper write_file() for simple file writing
  2014-08-30  8:33 [PATCH 00/32] nd/multiple-work-trees cleanup Nguyễn Thái Ngọc Duy
                   ` (19 preceding siblings ...)
  2014-08-30  8:33 ` [PATCH 20/32] wrapper.c: wrapper to open a file, fprintf then close Nguyễn Thái Ngọc Duy
@ 2014-08-30  8:33 ` Nguyễn Thái Ngọc Duy
  2014-08-30  8:33 ` [PATCH 22/32] checkout: support checking out into a new working directory Nguyễn Thái Ngọc Duy
                   ` (13 subsequent siblings)
  34 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-08-30  8:33 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

This fixes common problems in these code about error handling,
forgetting to close the file handle after fprintf() fails, or not
printing out the error string..

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 builtin/branch.c  |  4 +---
 builtin/init-db.c |  7 +------
 daemon.c          | 11 +----------
 submodule.c       |  9 ++-------
 transport.c       |  8 +++-----
 5 files changed, 8 insertions(+), 31 deletions(-)

diff --git a/builtin/branch.c b/builtin/branch.c
index 0591b22..e4265a1 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -754,7 +754,6 @@ static const char edit_description[] = "BRANCH_DESCRIPTION";
 
 static int edit_branch_description(const char *branch_name)
 {
-	FILE *fp;
 	int status;
 	struct strbuf buf = STRBUF_INIT;
 	struct strbuf name = STRBUF_INIT;
@@ -767,8 +766,7 @@ static int edit_branch_description(const char *branch_name)
 		    "  %s\n"
 		    "Lines starting with '%c' will be stripped.\n",
 		    branch_name, comment_line_char);
-	fp = fopen(git_path(edit_description), "w");
-	if ((fwrite(buf.buf, 1, buf.len, fp) < buf.len) || fclose(fp)) {
+	if (write_file(git_path(edit_description), 0, "%s", buf.buf)) {
 		strbuf_release(&buf);
 		return error(_("could not write branch description template: %s"),
 			     strerror(errno));
diff --git a/builtin/init-db.c b/builtin/init-db.c
index 56f85e2..ce8416a 100644
--- a/builtin/init-db.c
+++ b/builtin/init-db.c
@@ -342,7 +342,6 @@ int set_git_dir_init(const char *git_dir, const char *real_git_dir,
 static void separate_git_dir(const char *git_dir)
 {
 	struct stat st;
-	FILE *fp;
 
 	if (!stat(git_link, &st)) {
 		const char *src;
@@ -358,11 +357,7 @@ static void separate_git_dir(const char *git_dir)
 			die_errno(_("unable to move %s to %s"), src, git_dir);
 	}
 
-	fp = fopen(git_link, "w");
-	if (!fp)
-		die(_("Could not create git link %s"), git_link);
-	fprintf(fp, "gitdir: %s\n", git_dir);
-	fclose(fp);
+	write_file(git_link, 1, "gitdir: %s\n", git_dir);
 }
 
 int init_db(const char *template_dir, unsigned int flags)
diff --git a/daemon.c b/daemon.c
index 1eb6631..dd638a9 100644
--- a/daemon.c
+++ b/daemon.c
@@ -1094,15 +1094,6 @@ static struct credentials *prepare_credentials(const char *user_name,
 }
 #endif
 
-static void store_pid(const char *path)
-{
-	FILE *f = fopen(path, "w");
-	if (!f)
-		die_errno("cannot open pid file '%s'", path);
-	if (fprintf(f, "%"PRIuMAX"\n", (uintmax_t) getpid()) < 0 || fclose(f) != 0)
-		die_errno("failed to write pid file '%s'", path);
-}
-
 static int serve(struct string_list *listen_addr, int listen_port,
     struct credentials *cred)
 {
@@ -1313,7 +1304,7 @@ int main(int argc, char **argv)
 		sanitize_stdfds();
 
 	if (pid_file)
-		store_pid(pid_file);
+		write_file(pid_file, 1, "%"PRIuMAX"\n", (uintmax_t) getpid());
 
 	/* prepare argv for serving-processes */
 	cld_argv = xmalloc(sizeof (char *) * (argc + 2));
diff --git a/submodule.c b/submodule.c
index b80ecac..b7b6059 100644
--- a/submodule.c
+++ b/submodule.c
@@ -1112,16 +1112,11 @@ void connect_work_tree_and_git_dir(const char *work_tree, const char *git_dir)
 	struct strbuf file_name = STRBUF_INIT;
 	struct strbuf rel_path = STRBUF_INIT;
 	const char *real_work_tree = xstrdup(real_path(work_tree));
-	FILE *fp;
 
 	/* Update gitfile */
 	strbuf_addf(&file_name, "%s/.git", work_tree);
-	fp = fopen(file_name.buf, "w");
-	if (!fp)
-		die(_("Could not create git link %s"), file_name.buf);
-	fprintf(fp, "gitdir: %s\n", relative_path(git_dir, real_work_tree,
-						  &rel_path));
-	fclose(fp);
+	write_file(file_name.buf, 1, "gitdir: %s\n",
+		   relative_path(git_dir, real_work_tree, &rel_path));
 
 	/* Update core.worktree setting */
 	strbuf_reset(&file_name);
diff --git a/transport.c b/transport.c
index 59c9727..1b779bb 100644
--- a/transport.c
+++ b/transport.c
@@ -296,7 +296,6 @@ static int write_one_ref(const char *name, const unsigned char *sha1,
 {
 	struct strbuf *buf = data;
 	int len = buf->len;
-	FILE *f;
 
 	/* when called via for_each_ref(), flags is non-zero */
 	if (flags && !starts_with(name, "refs/heads/") &&
@@ -305,10 +304,9 @@ static int write_one_ref(const char *name, const unsigned char *sha1,
 
 	strbuf_addstr(buf, name);
 	if (safe_create_leading_directories(buf->buf) ||
-			!(f = fopen(buf->buf, "w")) ||
-			fprintf(f, "%s\n", sha1_to_hex(sha1)) < 0 ||
-			fclose(f))
-		return error("problems writing temporary file %s", buf->buf);
+	    write_file(buf->buf, 0, "%s\n", sha1_to_hex(sha1)))
+		return error("problems writing temporary file %s: %s",
+			     buf->buf, strerror(errno));
 	strbuf_setlen(buf, len);
 	return 0;
 }
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 22/32] checkout: support checking out into a new working directory
  2014-08-30  8:33 [PATCH 00/32] nd/multiple-work-trees cleanup Nguyễn Thái Ngọc Duy
                   ` (20 preceding siblings ...)
  2014-08-30  8:33 ` [PATCH 21/32] use new wrapper write_file() for simple file writing Nguyễn Thái Ngọc Duy
@ 2014-08-30  8:33 ` Nguyễn Thái Ngọc Duy
  2014-08-30 20:50   ` Philip Oakley
  2014-08-30  8:33 ` [PATCH 23/32] checkout: clean up half-prepared directories in --to mode Nguyễn Thái Ngọc Duy
                   ` (12 subsequent siblings)
  34 siblings, 1 reply; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-08-30  8:33 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

"git checkout --to" sets up a new working directory with a .git file
pointing to $GIT_DIR/repos/<id>. It then executes "git checkout" again
on the new worktree with the same arguments except "--to" is taken
out. The second checkout execution, which is not contaminated with any
info from the current repository, will actually check out and
everything that normal "git checkout" does.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
---
 Documentation/git-checkout.txt         | 34 ++++++++++++
 Documentation/git.txt                  |  3 +-
 Documentation/gitrepository-layout.txt |  7 +++
 builtin/checkout.c                     | 95 +++++++++++++++++++++++++++++++++-
 path.c                                 |  2 +-
 t/t2025-checkout-to.sh (new +x)        | 63 ++++++++++++++++++++++
 6 files changed, 200 insertions(+), 4 deletions(-)
 create mode 100755 t/t2025-checkout-to.sh

diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt
index 33ad2ad..38c70c5 100644
--- a/Documentation/git-checkout.txt
+++ b/Documentation/git-checkout.txt
@@ -225,6 +225,13 @@ This means that you can use `git checkout -p` to selectively discard
 edits from your current working tree. See the ``Interactive Mode''
 section of linkgit:git-add[1] to learn how to operate the `--patch` mode.
 
+--to=<path>::
+	Check out a new branch in a separate working directory at
+	`<path>`. A new working directory is linked to the current
+	repository, sharing everything except working directory
+	specific files such as HEAD, index... See "MULTIPLE CHECKOUT
+	MODE" section for more information.
+
 <branch>::
 	Branch to checkout; if it refers to a branch (i.e., a name that,
 	when prepended with "refs/heads/", is a valid ref), then that
@@ -388,6 +395,33 @@ $ git reflog -2 HEAD # or
 $ git log -g -2 HEAD
 ------------
 
+MULTIPLE CHECKOUT MODE
+----------------------
+Normally a working directory is attached to repository. When "git
+checkout --to" is used, a new working directory is attached to the
+current repository. This new working directory is called "linked
+checkout" as compared to the "main checkout" prepared by "git init" or
+"git clone". A repository has one main checkout and zero or more
+linked checkouts.
+
+All checkouts share the same repository. Linked checkouts see the
+repository a bit different from the main checkout. When the checkout
+"new" reads the path $GIT_DIR/HEAD for example, the actual path
+returned could be $GIT_DIR/repos/new/HEAD. This ensures checkouts
+won't step on each other.
+
+Each linked checkout has a private space in $GIT_DIR/repos, usually
+named after the base name of the working directory with a number added
+to make it unique. The linked checkout's $GIT_DIR points to this
+private space while $GIT_COMMON_DIR points to the main checkout's
+$GIT_DIR. These settings are done by "git checkout --to".
+
+Because in this mode $GIT_DIR becomes a lightweight virtual file
+system where a path could be rewritten to some place else, accessing
+$GIT_DIR from scripts should use `git rev-parse --git-path` to resolve
+a path instead of using it directly unless the path is known to be
+private to the working directory.
+
 EXAMPLES
 --------
 
diff --git a/Documentation/git.txt b/Documentation/git.txt
index 749052f..c0a4940 100644
--- a/Documentation/git.txt
+++ b/Documentation/git.txt
@@ -792,7 +792,8 @@ Git so take care if using Cogito etc.
 	If this variable is set to a path, non-worktree files that are
 	normally in $GIT_DIR will be taken from this path
 	instead. Worktree-specific files such as HEAD or index are
-	taken from $GIT_DIR. See linkgit:gitrepository-layout[5] for
+	taken from $GIT_DIR. See linkgit:gitrepository-layout[5] and
+	the section 'MULTIPLE CHECKOUT MODE' in linkgit:checkout[1]
 	details. This variable has lower precedence than other path
 	variables such as GIT_INDEX_FILE, GIT_OBJECT_DIRECTORY...
 
diff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt
index 58d1087..fab398a 100644
--- a/Documentation/gitrepository-layout.txt
+++ b/Documentation/gitrepository-layout.txt
@@ -248,6 +248,13 @@ modules::
 	directory is ignored if $GIT_COMMON_DIR is set and
 	"$GIT_COMMON_DIR/modules" will be used instead.
 
+repos::
+	Contains worktree specific information of linked
+	checkouts. Each subdirectory contains the worktree-related
+	part of a linked checkout. This directory is ignored if
+	$GIT_COMMON_DIR is set and "$GIT_COMMON_DIR/repos" will be
+	used instead.
+
 SEE ALSO
 --------
 linkgit:git-init[1],
diff --git a/builtin/checkout.c b/builtin/checkout.c
index 8023987..6373823 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -48,6 +48,10 @@ struct checkout_opts {
 	const char *prefix;
 	struct pathspec pathspec;
 	struct tree *source_tree;
+
+	const char *new_worktree;
+	const char **saved_argv;
+	int new_worktree_mode;
 };
 
 static int post_checkout_hook(struct commit *old, struct commit *new,
@@ -250,6 +254,9 @@ static int checkout_paths(const struct checkout_opts *opts,
 		die(_("Cannot update paths and switch to branch '%s' at the same time."),
 		    opts->new_branch);
 
+	if (opts->new_worktree)
+		die(_("'%s' cannot be used with updating paths"), "--to");
+
 	if (opts->patch_mode)
 		return run_add_interactive(revision, "--patch=checkout",
 					   &opts->pathspec);
@@ -485,7 +492,7 @@ static int merge_working_tree(const struct checkout_opts *opts,
 			topts.dir->flags |= DIR_SHOW_IGNORED;
 			setup_standard_excludes(topts.dir);
 		}
-		tree = parse_tree_indirect(old->commit ?
+		tree = parse_tree_indirect(old->commit && !opts->new_worktree_mode ?
 					   old->commit->object.sha1 :
 					   EMPTY_TREE_SHA1_BIN);
 		init_tree_desc(&trees[0], tree->buffer, tree->size);
@@ -796,7 +803,8 @@ static int switch_branches(const struct checkout_opts *opts,
 		return ret;
 	}
 
-	if (!opts->quiet && !old.path && old.commit && new->commit != old.commit)
+	if (!opts->quiet && !old.path && old.commit &&
+	    new->commit != old.commit && !opts->new_worktree_mode)
 		orphaned_commit_warning(old.commit, new->commit);
 
 	update_refs_for_switch(opts, &old, new);
@@ -806,6 +814,76 @@ static int switch_branches(const struct checkout_opts *opts,
 	return ret || writeout_error;
 }
 
+static int prepare_linked_checkout(const struct checkout_opts *opts,
+				   struct branch_info *new)
+{
+	struct strbuf sb_git = STRBUF_INIT, sb_repo = STRBUF_INIT;
+	struct strbuf sb = STRBUF_INIT;
+	const char *path = opts->new_worktree, *name;
+	struct stat st;
+	struct child_process cp;
+	int counter = 0, len;
+
+	if (!new->commit)
+		die(_("no branch specified"));
+	if (file_exists(path))
+		die(_("'%s' already exists"), path);
+
+	len = strlen(path);
+	while (len && is_dir_sep(path[len - 1]))
+		len--;
+
+	for (name = path + len - 1; name > path; name--)
+		if (is_dir_sep(*name)) {
+			name++;
+			break;
+		}
+	strbuf_addstr(&sb_repo,
+		      git_path("repos/%.*s", (int)(path + len - name), name));
+	len = sb_repo.len;
+	if (safe_create_leading_directories_const(sb_repo.buf))
+		die_errno(_("could not create leading directories of '%s'"),
+			  sb_repo.buf);
+	while (!stat(sb_repo.buf, &st)) {
+		counter++;
+		strbuf_setlen(&sb_repo, len);
+		strbuf_addf(&sb_repo, "%d", counter);
+	}
+	name = strrchr(sb_repo.buf, '/') + 1;
+	if (mkdir(sb_repo.buf, 0777))
+		die_errno(_("could not create directory of '%s'"), sb_repo.buf);
+
+	strbuf_addf(&sb_git, "%s/.git", path);
+	if (safe_create_leading_directories_const(sb_git.buf))
+		die_errno(_("could not create leading directories of '%s'"),
+			  sb_git.buf);
+
+	write_file(sb_git.buf, 1, "gitdir: %s/repos/%s\n",
+		   real_path(get_git_common_dir()), name);
+	/*
+	 * This is to keep resolve_ref() happy. We need a valid HEAD
+	 * or is_git_directory() will reject the directory. Any valid
+	 * value would do because this value will be ignored and
+	 * replaced at the next (real) checkout.
+	 */
+	strbuf_addf(&sb, "%s/HEAD", sb_repo.buf);
+	write_file(sb.buf, 1, "%s\n", sha1_to_hex(new->commit->object.sha1));
+	strbuf_reset(&sb);
+	strbuf_addf(&sb, "%s/commondir", sb_repo.buf);
+	write_file(sb.buf, 1, "../..\n");
+
+	if (!opts->quiet)
+		fprintf_ln(stderr, _("Enter %s (identifier %s)"), path, name);
+
+	setenv("GIT_CHECKOUT_NEW_WORKTREE", "1", 1);
+	setenv(GIT_DIR_ENVIRONMENT, sb_git.buf, 1);
+	setenv(GIT_WORK_TREE_ENVIRONMENT, path, 1);
+	memset(&cp, 0, sizeof(cp));
+	cp.git_cmd = 1;
+	cp.argv = opts->saved_argv;
+	return run_command(&cp);
+}
+
 static int git_checkout_config(const char *var, const char *value, void *cb)
 {
 	if (!strcmp(var, "diff.ignoresubmodules")) {
@@ -1067,6 +1145,9 @@ static int checkout_branch(struct checkout_opts *opts,
 		die(_("Cannot switch branch to a non-commit '%s'"),
 		    new->name);
 
+	if (opts->new_worktree)
+		return prepare_linked_checkout(opts, new);
+
 	if (!new->commit && opts->new_branch) {
 		unsigned char rev[20];
 		int flag;
@@ -1109,6 +1190,8 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
 			 N_("do not limit pathspecs to sparse entries only")),
 		OPT_HIDDEN_BOOL(0, "guess", &dwim_new_local_branch,
 				N_("second guess 'git checkout no-such-branch'")),
+		OPT_FILENAME(0, "to", &opts.new_worktree,
+			   N_("check a branch out in a separate working directory")),
 		OPT_END(),
 	};
 
@@ -1117,6 +1200,9 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
 	opts.overwrite_ignore = 1;
 	opts.prefix = prefix;
 
+	opts.saved_argv = xmalloc(sizeof(const char *) * (argc + 2));
+	memcpy(opts.saved_argv, argv, sizeof(const char *) * (argc + 1));
+
 	gitmodules_config();
 	git_config(git_checkout_config, &opts);
 
@@ -1125,6 +1211,11 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
 	argc = parse_options(argc, argv, prefix, options, checkout_usage,
 			     PARSE_OPT_KEEP_DASHDASH);
 
+	/* recursive execution from checkout_new_worktree() */
+	opts.new_worktree_mode = getenv("GIT_CHECKOUT_NEW_WORKTREE") != NULL;
+	if (opts.new_worktree_mode)
+		opts.new_worktree = NULL;
+
 	if (conflict_style) {
 		opts.merge = 1; /* implied */
 		git_xmerge_config("merge.conflictstyle", conflict_style, NULL);
diff --git a/path.c b/path.c
index 8a6586c..e41d6b3 100644
--- a/path.c
+++ b/path.c
@@ -92,7 +92,7 @@ static void replace_dir(struct strbuf *buf, int len, const char *newdir)
 
 static const char *common_list[] = {
 	"/branches", "/hooks", "/info", "/logs", "/lost-found", "/modules",
-	"/objects", "/refs", "/remotes", "/rr-cache", "/svn",
+	"/objects", "/refs", "/remotes", "/repos", "/rr-cache", "/svn",
 	"config", "gc.pid", "packed-refs", "shallow",
 	NULL
 };
diff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh
new file mode 100755
index 0000000..8c73b18
--- /dev/null
+++ b/t/t2025-checkout-to.sh
@@ -0,0 +1,63 @@
+#!/bin/sh
+
+test_description='test git checkout --to'
+
+. ./test-lib.sh
+
+test_expect_success 'setup' '
+	test_commit init
+'
+
+test_expect_success 'checkout --to not updating paths' '
+	test_must_fail git checkout --to -- init.t
+'
+
+test_expect_success 'checkout --to an existing worktree' '
+	mkdir existing &&
+	test_must_fail git checkout --detach --to existing master
+'
+
+test_expect_success 'checkout --to a new worktree' '
+	git checkout --to here master &&
+	(
+		cd here &&
+		test_cmp ../init.t init.t &&
+		git symbolic-ref HEAD >actual &&
+		echo refs/heads/master >expect &&
+		test_cmp expect actual &&
+		git fsck
+	)
+'
+
+test_expect_success 'checkout --to a new worktree from a subdir' '
+	(
+		mkdir sub &&
+		cd sub &&
+		git checkout --detach --to here master &&
+		cd here &&
+		test_cmp ../../init.t init.t
+	)
+'
+
+test_expect_success 'checkout --to from a linked checkout' '
+	(
+		cd here &&
+		git checkout --to nested-here master
+		cd nested-here &&
+		git fsck
+	)
+'
+
+test_expect_success 'checkout --to a new worktree creating new branch' '
+	git checkout --to there -b newmaster master &&
+	(
+		cd there &&
+		test_cmp ../init.t init.t &&
+		git symbolic-ref HEAD >actual &&
+		echo refs/heads/newmaster >expect &&
+		test_cmp expect actual &&
+		git fsck
+	)
+'
+
+test_done
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 23/32] checkout: clean up half-prepared directories in --to mode
  2014-08-30  8:33 [PATCH 00/32] nd/multiple-work-trees cleanup Nguyễn Thái Ngọc Duy
                   ` (21 preceding siblings ...)
  2014-08-30  8:33 ` [PATCH 22/32] checkout: support checking out into a new working directory Nguyễn Thái Ngọc Duy
@ 2014-08-30  8:33 ` Nguyễn Thái Ngọc Duy
  2014-08-30  8:33 ` [PATCH 24/32] checkout: reject if the branch is already checked out elsewhere Nguyễn Thái Ngọc Duy
                   ` (11 subsequent siblings)
  34 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-08-30  8:33 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
---
 builtin/checkout.c     | 54 ++++++++++++++++++++++++++++++++++++++++++++++++--
 t/t2025-checkout-to.sh |  6 ++++++
 2 files changed, 58 insertions(+), 2 deletions(-)

diff --git a/builtin/checkout.c b/builtin/checkout.c
index 6373823..4ae925a 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -20,6 +20,7 @@
 #include "resolve-undo.h"
 #include "submodule.h"
 #include "argv-array.h"
+#include "sigchain.h"
 
 static const char * const checkout_usage[] = {
 	N_("git checkout [options] <branch>"),
@@ -814,6 +815,35 @@ static int switch_branches(const struct checkout_opts *opts,
 	return ret || writeout_error;
 }
 
+static char *junk_work_tree;
+static char *junk_git_dir;
+static int is_junk;
+static pid_t junk_pid;
+
+static void remove_junk(void)
+{
+	struct strbuf sb = STRBUF_INIT;
+	if (!is_junk || getpid() != junk_pid)
+		return;
+	if (junk_git_dir) {
+		strbuf_addstr(&sb, junk_git_dir);
+		remove_dir_recursively(&sb, 0);
+		strbuf_reset(&sb);
+	}
+	if (junk_work_tree) {
+		strbuf_addstr(&sb, junk_work_tree);
+		remove_dir_recursively(&sb, 0);
+	}
+	strbuf_release(&sb);
+}
+
+static void remove_junk_on_signal(int signo)
+{
+	remove_junk();
+	sigchain_pop(signo);
+	raise(signo);
+}
+
 static int prepare_linked_checkout(const struct checkout_opts *opts,
 				   struct branch_info *new)
 {
@@ -822,7 +852,7 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
 	const char *path = opts->new_worktree, *name;
 	struct stat st;
 	struct child_process cp;
-	int counter = 0, len;
+	int counter = 0, len, ret;
 
 	if (!new->commit)
 		die(_("no branch specified"));
@@ -850,13 +880,21 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
 		strbuf_addf(&sb_repo, "%d", counter);
 	}
 	name = strrchr(sb_repo.buf, '/') + 1;
+
+	junk_pid = getpid();
+	atexit(remove_junk);
+	sigchain_push_common(remove_junk_on_signal);
+
 	if (mkdir(sb_repo.buf, 0777))
 		die_errno(_("could not create directory of '%s'"), sb_repo.buf);
+	junk_git_dir = xstrdup(sb_repo.buf);
+	is_junk = 1;
 
 	strbuf_addf(&sb_git, "%s/.git", path);
 	if (safe_create_leading_directories_const(sb_git.buf))
 		die_errno(_("could not create leading directories of '%s'"),
 			  sb_git.buf);
+	junk_work_tree = xstrdup(path);
 
 	write_file(sb_git.buf, 1, "gitdir: %s/repos/%s\n",
 		   real_path(get_git_common_dir()), name);
@@ -881,7 +919,19 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
 	memset(&cp, 0, sizeof(cp));
 	cp.git_cmd = 1;
 	cp.argv = opts->saved_argv;
-	return run_command(&cp);
+	ret = run_command(&cp);
+	if (!ret) {
+		is_junk = 0;
+		free(junk_work_tree);
+		free(junk_git_dir);
+		junk_work_tree = NULL;
+		junk_git_dir = NULL;
+	}
+	strbuf_release(&sb);
+	strbuf_release(&sb_repo);
+	strbuf_release(&sb_git);
+	return ret;
+
 }
 
 static int git_checkout_config(const char *var, const char *value, void *cb)
diff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh
index 8c73b18..75ac420 100755
--- a/t/t2025-checkout-to.sh
+++ b/t/t2025-checkout-to.sh
@@ -17,6 +17,12 @@ test_expect_success 'checkout --to an existing worktree' '
 	test_must_fail git checkout --detach --to existing master
 '
 
+test_expect_success 'checkout --to refuses to checkout locked branch' '
+	test_must_fail git checkout --to zere master &&
+	! test -d zere &&
+	! test -d .git/repos/zere
+'
+
 test_expect_success 'checkout --to a new worktree' '
 	git checkout --to here master &&
 	(
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 24/32] checkout: reject if the branch is already checked out elsewhere
  2014-08-30  8:33 [PATCH 00/32] nd/multiple-work-trees cleanup Nguyễn Thái Ngọc Duy
                   ` (22 preceding siblings ...)
  2014-08-30  8:33 ` [PATCH 23/32] checkout: clean up half-prepared directories in --to mode Nguyễn Thái Ngọc Duy
@ 2014-08-30  8:33 ` Nguyễn Thái Ngọc Duy
  2014-08-30  8:33 ` [PATCH 25/32] prune: strategies for linked checkouts Nguyễn Thái Ngọc Duy
                   ` (10 subsequent siblings)
  34 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-08-30  8:33 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

One branch obviously can't be checked out at two places (but detached
heads are ok). Give the user a choice in this case: --detach, -b
new-branch, switch branch in the other checkout first or simply 'cd'
and continue to work there.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
---
 builtin/checkout.c     | 86 ++++++++++++++++++++++++++++++++++++++++++++++++--
 t/t2025-checkout-to.sh | 25 ++++++++++++---
 2 files changed, 104 insertions(+), 7 deletions(-)

diff --git a/builtin/checkout.c b/builtin/checkout.c
index 4ae925a..614db34 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -432,6 +432,11 @@ struct branch_info {
 	const char *name; /* The short name used */
 	const char *path; /* The full name of a real branch */
 	struct commit *commit; /* The named commit */
+	/*
+	 * if not null the branch is detached because it's already
+	 * checked out in this checkout
+	 */
+	char *checkout;
 };
 
 static void setup_branch_path(struct branch_info *branch)
@@ -989,12 +994,78 @@ static const char *unique_tracking_name(const char *name, unsigned char *sha1)
 	return NULL;
 }
 
+static void check_linked_checkout(struct branch_info *new, const char *id)
+{
+	struct strbuf sb = STRBUF_INIT;
+	struct strbuf path = STRBUF_INIT;
+	struct strbuf gitdir = STRBUF_INIT;
+	const char *start, *end;
+
+	if (id)
+		strbuf_addf(&path, "%s/repos/%s/HEAD", get_git_common_dir(), id);
+	else
+		strbuf_addf(&path, "%s/HEAD", get_git_common_dir());
+
+	if (strbuf_read_file(&sb, path.buf, 0) < 0 ||
+	    !skip_prefix(sb.buf, "ref:", &start))
+		goto done;
+	while (isspace(*start))
+		start++;
+	end = start;
+	while (*end && !isspace(*end))
+		end++;
+	if (strncmp(start, new->path, end - start) || new->path[end - start] != '\0')
+		goto done;
+	if (id) {
+		strbuf_reset(&path);
+		strbuf_addf(&path, "%s/repos/%s/gitdir", get_git_common_dir(), id);
+		if (strbuf_read_file(&gitdir, path.buf, 0) <= 0)
+			goto done;
+		strbuf_rtrim(&gitdir);
+	} else
+		strbuf_addstr(&gitdir, get_git_common_dir());
+	die(_("'%s' is already checked out at '%s'"), new->name, gitdir.buf);
+done:
+	strbuf_release(&path);
+	strbuf_release(&sb);
+	strbuf_release(&gitdir);
+}
+
+static void check_linked_checkouts(struct branch_info *new)
+{
+	struct strbuf path = STRBUF_INIT;
+	DIR *dir;
+	struct dirent *d;
+
+	strbuf_addf(&path, "%s/repos", get_git_common_dir());
+	if ((dir = opendir(path.buf)) == NULL) {
+		strbuf_release(&path);
+		return;
+	}
+
+	/*
+	 * $GIT_COMMON_DIR/HEAD is practically outside
+	 * $GIT_DIR so resolve_ref_unsafe() won't work (it
+	 * uses git_path). Parse the ref ourselves.
+	 */
+	check_linked_checkout(new, NULL);
+
+	while ((d = readdir(dir)) != NULL) {
+		if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, ".."))
+			continue;
+		check_linked_checkout(new, d->d_name);
+	}
+	strbuf_release(&path);
+	closedir(dir);
+}
+
 static int parse_branchname_arg(int argc, const char **argv,
 				int dwim_new_local_branch_ok,
 				struct branch_info *new,
 				struct tree **source_tree,
 				unsigned char rev[20],
-				const char **new_branch)
+				const char **new_branch,
+				int force_detach)
 {
 	int argcount = 0;
 	unsigned char branch_rev[20];
@@ -1116,6 +1187,16 @@ static int parse_branchname_arg(int argc, const char **argv,
 	else
 		new->path = NULL; /* not an existing branch */
 
+	if (new->path && !force_detach && !*new_branch) {
+		unsigned char sha1[20];
+		int flag;
+		char *head_ref = resolve_refdup("HEAD", sha1, 0, &flag);
+		if (head_ref &&
+		    (!(flag & REF_ISSYMREF) || strcmp(head_ref, new->path)))
+			check_linked_checkouts(new);
+		free(head_ref);
+	}
+
 	new->commit = lookup_commit_reference_gently(rev, 1);
 	if (!new->commit) {
 		/* not a commit */
@@ -1322,7 +1403,8 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
 			!opts.new_branch;
 		int n = parse_branchname_arg(argc, argv, dwim_ok,
 					     &new, &opts.source_tree,
-					     rev, &opts.new_branch);
+					     rev, &opts.new_branch,
+					     opts.force_detach);
 		argv += n;
 		argc -= n;
 	}
diff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh
index 75ac420..796472b 100755
--- a/t/t2025-checkout-to.sh
+++ b/t/t2025-checkout-to.sh
@@ -24,13 +24,14 @@ test_expect_success 'checkout --to refuses to checkout locked branch' '
 '
 
 test_expect_success 'checkout --to a new worktree' '
-	git checkout --to here master &&
+	git rev-parse HEAD >expect &&
+	git checkout --detach --to here master &&
 	(
 		cd here &&
 		test_cmp ../init.t init.t &&
-		git symbolic-ref HEAD >actual &&
-		echo refs/heads/master >expect &&
-		test_cmp expect actual &&
+		test_must_fail git symbolic-ref HEAD &&
+		git rev-parse HEAD >actual &&
+		test_cmp ../expect actual &&
 		git fsck
 	)
 '
@@ -48,7 +49,7 @@ test_expect_success 'checkout --to a new worktree from a subdir' '
 test_expect_success 'checkout --to from a linked checkout' '
 	(
 		cd here &&
-		git checkout --to nested-here master
+		git checkout --detach --to nested-here master
 		cd nested-here &&
 		git fsck
 	)
@@ -66,4 +67,18 @@ test_expect_success 'checkout --to a new worktree creating new branch' '
 	)
 '
 
+test_expect_success 'die the same branch is already checked out' '
+	(
+		cd here &&
+		test_must_fail git checkout newmaster
+	)
+'
+
+test_expect_success 'not die on re-checking out current branch' '
+	(
+		cd there &&
+		git checkout newmaster
+	)
+'
+
 test_done
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 25/32] prune: strategies for linked checkouts
  2014-08-30  8:33 [PATCH 00/32] nd/multiple-work-trees cleanup Nguyễn Thái Ngọc Duy
                   ` (23 preceding siblings ...)
  2014-08-30  8:33 ` [PATCH 24/32] checkout: reject if the branch is already checked out elsewhere Nguyễn Thái Ngọc Duy
@ 2014-08-30  8:33 ` Nguyễn Thái Ngọc Duy
  2014-08-30  8:33 ` [PATCH 26/32] gc: style change -- no SP before closing parenthesis Nguyễn Thái Ngọc Duy
                   ` (9 subsequent siblings)
  34 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-08-30  8:33 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

(alias R=$GIT_COMMON_DIR/repos/<id>)

 - linked checkouts are supposed to keep its location in $R/gitdir up
   to date. The use case is auto fixup after a manual checkout move.

 - linked checkouts are supposed to update mtime of $R/gitdir. If
   $R/gitdir's mtime is older than a limit, and it points to nowhere,
   repos/<id> is to be pruned.

 - If $R/locked exists, repos/<id> is not supposed to be pruned. If
   $R/locked exists and $R/gitdir's mtime is older than a really long
   limit, warn about old unused repo.

 - "git checkout --to" is supposed to make a hard link named $R/link
   pointing to the .git file on supported file systems to help detect
   the user manually deleting the checkout. If $R/link exists and its
   link count is greated than 1, the repo is kept.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
---
 Documentation/git-prune.txt                |  3 +
 Documentation/gitrepository-layout.txt     | 19 ++++++
 builtin/checkout.c                         | 14 +++++
 builtin/prune.c                            | 95 ++++++++++++++++++++++++++++++
 setup.c                                    | 13 ++++
 t/t2026-prune-linked-checkouts.sh (new +x) | 84 ++++++++++++++++++++++++++
 6 files changed, 228 insertions(+)
 create mode 100755 t/t2026-prune-linked-checkouts.sh

diff --git a/Documentation/git-prune.txt b/Documentation/git-prune.txt
index 7a493c8..50e39ec 100644
--- a/Documentation/git-prune.txt
+++ b/Documentation/git-prune.txt
@@ -48,6 +48,9 @@ OPTIONS
 --expire <time>::
 	Only expire loose objects older than <time>.
 
+--repos::
+	Prune directories in $GIT_DIR/repos.
+
 <head>...::
 	In addition to objects
 	reachable from any of our references, keep objects
diff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt
index fab398a..c9a7d1a 100644
--- a/Documentation/gitrepository-layout.txt
+++ b/Documentation/gitrepository-layout.txt
@@ -255,6 +255,25 @@ repos::
 	$GIT_COMMON_DIR is set and "$GIT_COMMON_DIR/repos" will be
 	used instead.
 
+repos/<id>/gitdir::
+	A text file containing the absolute path back to the .git file
+	that points to here. This is used to check if the linked
+	repository has been manually removed and there is no need to
+	keep this directory any more. mtime of this file should be
+	updated every time the linked repository is accessed.
+
+repos/<id>/locked::
+	If this file exists, the linked repository may be on a
+	portable device and not available. It does not mean that the
+	linked repository is gone and `repos/<id>` could be
+	removed. The file's content contains a reason string on why
+	the repository is locked.
+
+repos/<id>/link::
+	If this file exists, it is a hard link to the linked .git
+	file. It is used to detect if the linked repository is
+	manually removed.
+
 SEE ALSO
 --------
 linkgit:git-init[1],
diff --git a/builtin/checkout.c b/builtin/checkout.c
index 614db34..0f28b4a 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -895,12 +895,22 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
 	junk_git_dir = xstrdup(sb_repo.buf);
 	is_junk = 1;
 
+	/*
+	 * lock the incomplete repo so prune won't delete it, unlock
+	 * after the preparation is over.
+	 */
+	strbuf_addf(&sb, "%s/locked", sb_repo.buf);
+	write_file(sb.buf, 1, "initializing\n");
+
 	strbuf_addf(&sb_git, "%s/.git", path);
 	if (safe_create_leading_directories_const(sb_git.buf))
 		die_errno(_("could not create leading directories of '%s'"),
 			  sb_git.buf);
 	junk_work_tree = xstrdup(path);
 
+	strbuf_reset(&sb);
+	strbuf_addf(&sb, "%s/gitdir", sb_repo.buf);
+	write_file(sb.buf, 1, "%s\n", real_path(sb_git.buf));
 	write_file(sb_git.buf, 1, "gitdir: %s/repos/%s\n",
 		   real_path(get_git_common_dir()), name);
 	/*
@@ -909,6 +919,7 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
 	 * value would do because this value will be ignored and
 	 * replaced at the next (real) checkout.
 	 */
+	strbuf_reset(&sb);
 	strbuf_addf(&sb, "%s/HEAD", sb_repo.buf);
 	write_file(sb.buf, 1, "%s\n", sha1_to_hex(new->commit->object.sha1));
 	strbuf_reset(&sb);
@@ -932,6 +943,9 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
 		junk_work_tree = NULL;
 		junk_git_dir = NULL;
 	}
+	strbuf_reset(&sb);
+	strbuf_addf(&sb, "%s/locked", sb_repo.buf);
+	unlink_or_warn(sb.buf);
 	strbuf_release(&sb);
 	strbuf_release(&sb_repo);
 	strbuf_release(&sb_git);
diff --git a/builtin/prune.c b/builtin/prune.c
index 144a3bd..e72c391 100644
--- a/builtin/prune.c
+++ b/builtin/prune.c
@@ -112,6 +112,91 @@ static void prune_object_dir(const char *path)
 	}
 }
 
+static int prune_repo_dir(const char *id, struct strbuf *reason)
+{
+	struct stat st;
+	char *path;
+	int fd, len;
+
+	if (!is_directory(git_path("repos/%s", id))) {
+		strbuf_addf(reason, _("Removing repos/%s: not a valid directory"), id);
+		return 1;
+	}
+	if (file_exists(git_path("repos/%s/locked", id)))
+		return 0;
+	if (stat(git_path("repos/%s/gitdir", id), &st)) {
+		strbuf_addf(reason, _("Removing repos/%s: gitdir file does not exist"), id);
+		return 1;
+	}
+	fd = open(git_path("repos/%s/gitdir", id), O_RDONLY);
+	if (fd < 0) {
+		strbuf_addf(reason, _("Removing repos/%s: unable to read gitdir file (%s)"),
+			    id, strerror(errno));
+		return 1;
+	}
+	len = st.st_size;
+	path = xmalloc(len + 1);
+	read_in_full(fd, path, len);
+	close(fd);
+	while (len && (path[len - 1] == '\n' || path[len - 1] == '\r'))
+		len--;
+	if (!len) {
+		strbuf_addf(reason, _("Removing repos/%s: invalid gitdir file"), id);
+		free(path);
+		return 1;
+	}
+	path[len] = '\0';
+	if (!file_exists(path)) {
+		struct stat st_link;
+		free(path);
+		/*
+		 * the repo is moved manually and has not been
+		 * accessed since?
+		 */
+		if (!stat(git_path("repos/%s/link", id), &st_link) &&
+		    st_link.st_nlink > 1)
+			return 0;
+		strbuf_addf(reason, _("Removing repos/%s: gitdir file points to non-existent location"), id);
+		return 1;
+	}
+	free(path);
+	return st.st_mtime <= expire;
+}
+
+static void prune_repos_dir(void)
+{
+	struct strbuf reason = STRBUF_INIT;
+	struct strbuf path = STRBUF_INIT;
+	DIR *dir = opendir(git_path("repos"));
+	struct dirent *d;
+	int ret;
+	if (!dir)
+		return;
+	while ((d = readdir(dir)) != NULL) {
+		if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, ".."))
+			continue;
+		strbuf_reset(&reason);
+		if (!prune_repo_dir(d->d_name, &reason))
+			continue;
+		if (show_only || verbose)
+			printf("%s\n", reason.buf);
+		if (show_only)
+			continue;
+		strbuf_reset(&path);
+		strbuf_addstr(&path, git_path("repos/%s", d->d_name));
+		ret = remove_dir_recursively(&path, 0);
+		if (ret < 0 && errno == ENOTDIR)
+			ret = unlink(path.buf);
+		if (ret)
+			error(_("failed to remove: %s"), strerror(errno));
+	}
+	closedir(dir);
+	if (!show_only)
+		rmdir(git_path("repos"));
+	strbuf_release(&reason);
+	strbuf_release(&path);
+}
+
 /*
  * Write errors (particularly out of space) can result in
  * failed temporary packs (and more rarely indexes and other
@@ -138,10 +223,12 @@ int cmd_prune(int argc, const char **argv, const char *prefix)
 {
 	struct rev_info revs;
 	struct progress *progress = NULL;
+	int prune_repos = 0;
 	const struct option options[] = {
 		OPT__DRY_RUN(&show_only, N_("do not remove, show only")),
 		OPT__VERBOSE(&verbose, N_("report pruned objects")),
 		OPT_BOOL(0, "progress", &show_progress, N_("show progress")),
+		OPT_BOOL(0, "repos", &prune_repos, N_("prune .git/repos/")),
 		OPT_EXPIRY_DATE(0, "expire", &expire,
 				N_("expire objects older than <time>")),
 		OPT_END()
@@ -154,6 +241,14 @@ int cmd_prune(int argc, const char **argv, const char *prefix)
 	init_revisions(&revs, prefix);
 
 	argc = parse_options(argc, argv, prefix, options, prune_usage, 0);
+
+	if (prune_repos) {
+		if (argc)
+			die(_("--repos does not take extra arguments"));
+		prune_repos_dir();
+		return 0;
+	}
+
 	while (argc--) {
 		unsigned char sha1[20];
 		const char *name = *argv++;
diff --git a/setup.c b/setup.c
index 8f90bc3..da2d669 100644
--- a/setup.c
+++ b/setup.c
@@ -390,6 +390,17 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)
 	return ret;
 }
 
+static void update_linked_gitdir(const char *gitfile, const char *gitdir)
+{
+	struct strbuf path = STRBUF_INIT;
+	struct stat st;
+
+	strbuf_addf(&path, "%s/gitfile", gitdir);
+	if (stat(path.buf, &st) || st.st_mtime + 24 * 3600 < time(NULL))
+		write_file(path.buf, 0, "%s\n", gitfile);
+	strbuf_release(&path);
+}
+
 /*
  * Try to read the location of the git directory from the .git file,
  * return path to git directory if found.
@@ -438,6 +449,8 @@ const char *read_gitfile(const char *path)
 
 	if (!is_git_directory(dir))
 		die("Not a git repository: %s", dir);
+
+	update_linked_gitdir(path, dir);
 	path = real_path(dir);
 
 	free(buf);
diff --git a/t/t2026-prune-linked-checkouts.sh b/t/t2026-prune-linked-checkouts.sh
new file mode 100755
index 0000000..79d84cb
--- /dev/null
+++ b/t/t2026-prune-linked-checkouts.sh
@@ -0,0 +1,84 @@
+#!/bin/sh
+
+test_description='prune $GIT_DIR/repos'
+
+. ./test-lib.sh
+
+test_expect_success 'prune --repos on normal repo' '
+	git prune --repos &&
+	test_must_fail git prune --repos abc
+'
+
+test_expect_success 'prune files inside $GIT_DIR/repos' '
+	mkdir .git/repos &&
+	: >.git/repos/abc &&
+	git prune --repos --verbose >actual &&
+	cat >expect <<EOF &&
+Removing repos/abc: not a valid directory
+EOF
+	test_i18ncmp expect actual &&
+	! test -f .git/repos/abc &&
+	! test -d .git/repos
+'
+
+test_expect_success 'prune directories without gitdir' '
+	mkdir -p .git/repos/def/abc &&
+	: >.git/repos/def/def &&
+	cat >expect <<EOF &&
+Removing repos/def: gitdir file does not exist
+EOF
+	git prune --repos --verbose >actual &&
+	test_i18ncmp expect actual &&
+	! test -d .git/repos/def &&
+	! test -d .git/repos
+'
+
+test_expect_success POSIXPERM 'prune directories with unreadable gitdir' '
+	mkdir -p .git/repos/def/abc &&
+	: >.git/repos/def/def &&
+	: >.git/repos/def/gitdir &&
+	chmod u-r .git/repos/def/gitdir &&
+	git prune --repos --verbose >actual &&
+	test_i18ngrep "Removing repos/def: unable to read gitdir file" actual &&
+	! test -d .git/repos/def &&
+	! test -d .git/repos
+'
+
+test_expect_success 'prune directories with invalid gitdir' '
+	mkdir -p .git/repos/def/abc &&
+	: >.git/repos/def/def &&
+	: >.git/repos/def/gitdir &&
+	git prune --repos --verbose >actual &&
+	test_i18ngrep "Removing repos/def: invalid gitdir file" actual &&
+	! test -d .git/repos/def &&
+	! test -d .git/repos
+'
+
+test_expect_success 'prune directories with gitdir pointing to nowhere' '
+	mkdir -p .git/repos/def/abc &&
+	: >.git/repos/def/def &&
+	echo "$TRASH_DIRECTORY"/nowhere >.git/repos/def/gitdir &&
+	git prune --repos --verbose >actual &&
+	test_i18ngrep "Removing repos/def: gitdir file points to non-existent location" actual &&
+	! test -d .git/repos/def &&
+	! test -d .git/repos
+'
+
+test_expect_success 'not prune locked checkout' '
+	test_when_finished rm -r .git/repos
+	mkdir -p .git/repos/ghi &&
+	: >.git/repos/ghi/locked &&
+	git prune --repos &&
+	test -d .git/repos/ghi
+'
+
+test_expect_success 'not prune recent checkouts' '
+	test_when_finished rm -r .git/repos
+	mkdir zz &&
+	mkdir -p .git/repos/jlm &&
+	echo "$TRASH_DIRECTORY"/zz >.git/repos/jlm/gitdir &&
+	git prune --repos --verbose --expire=2.days.ago &&
+	test -d .git/repos/jlm
+'
+
+test_done
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 26/32] gc: style change -- no SP before closing parenthesis
  2014-08-30  8:33 [PATCH 00/32] nd/multiple-work-trees cleanup Nguyễn Thái Ngọc Duy
                   ` (24 preceding siblings ...)
  2014-08-30  8:33 ` [PATCH 25/32] prune: strategies for linked checkouts Nguyễn Thái Ngọc Duy
@ 2014-08-30  8:33 ` Nguyễn Thái Ngọc Duy
  2014-08-30  8:33 ` [PATCH 27/32] gc: factor out gc.pruneexpire parsing code Nguyễn Thái Ngọc Duy
                   ` (8 subsequent siblings)
  34 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-08-30  8:33 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 builtin/gc.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/builtin/gc.c b/builtin/gc.c
index 8d219d8..3bfb990 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -298,7 +298,7 @@ int cmd_gc(int argc, const char **argv, const char *prefix)
 	argv_array_pushl(&pack_refs_cmd, "pack-refs", "--all", "--prune", NULL);
 	argv_array_pushl(&reflog, "reflog", "expire", "--all", NULL);
 	argv_array_pushl(&repack, "repack", "-d", "-l", NULL);
-	argv_array_pushl(&prune, "prune", "--expire", NULL );
+	argv_array_pushl(&prune, "prune", "--expire", NULL);
 	argv_array_pushl(&rerere, "rerere", "gc", NULL);
 
 	git_config(gc_config, NULL);
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 27/32] gc: factor out gc.pruneexpire parsing code
  2014-08-30  8:33 [PATCH 00/32] nd/multiple-work-trees cleanup Nguyễn Thái Ngọc Duy
                   ` (25 preceding siblings ...)
  2014-08-30  8:33 ` [PATCH 26/32] gc: style change -- no SP before closing parenthesis Nguyễn Thái Ngọc Duy
@ 2014-08-30  8:33 ` Nguyễn Thái Ngọc Duy
  2014-08-30  8:33 ` [PATCH 28/32] gc: support prune --repos Nguyễn Thái Ngọc Duy
                   ` (7 subsequent siblings)
  34 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-08-30  8:33 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 builtin/gc.c | 21 +++++++++++++--------
 1 file changed, 13 insertions(+), 8 deletions(-)

diff --git a/builtin/gc.c b/builtin/gc.c
index 3bfb990..e38c902 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -55,6 +55,17 @@ static void remove_pidfile_on_signal(int signo)
 	raise(signo);
 }
 
+static int git_config_date_string(const char **output,
+				  const char *var, const char *value)
+{
+	if (value && strcmp(value, "now")) {
+		unsigned long now = approxidate("now");
+		if (approxidate(value) >= now)
+			return error(_("Invalid %s: '%s'"), var, value);
+	}
+	return git_config_string(output, var, value);
+}
+
 static int gc_config(const char *var, const char *value, void *cb)
 {
 	if (!strcmp(var, "gc.packrefs")) {
@@ -84,14 +95,8 @@ static int gc_config(const char *var, const char *value, void *cb)
 		detach_auto = git_config_bool(var, value);
 		return 0;
 	}
-	if (!strcmp(var, "gc.pruneexpire")) {
-		if (value && strcmp(value, "now")) {
-			unsigned long now = approxidate("now");
-			if (approxidate(value) >= now)
-				return error(_("Invalid %s: '%s'"), var, value);
-		}
-		return git_config_string(&prune_expire, var, value);
-	}
+	if (!strcmp(var, "gc.pruneexpire"))
+		return git_config_date_string(&prune_expire, var, value);
 	return git_default_config(var, value, cb);
 }
 
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 28/32] gc: support prune --repos
  2014-08-30  8:33 [PATCH 00/32] nd/multiple-work-trees cleanup Nguyễn Thái Ngọc Duy
                   ` (26 preceding siblings ...)
  2014-08-30  8:33 ` [PATCH 27/32] gc: factor out gc.pruneexpire parsing code Nguyễn Thái Ngọc Duy
@ 2014-08-30  8:33 ` Nguyễn Thái Ngọc Duy
  2014-08-30  8:33 ` [PATCH 29/32] count-objects: report unused files in $GIT_DIR/repos/ Nguyễn Thái Ngọc Duy
                   ` (6 subsequent siblings)
  34 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-08-30  8:33 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 Documentation/config.txt |  7 +++++++
 builtin/gc.c             | 11 +++++++++++
 2 files changed, 18 insertions(+)

diff --git a/Documentation/config.txt b/Documentation/config.txt
index 286e539..57999fa 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -1211,6 +1211,13 @@ gc.pruneexpire::
 	"now" may be used to disable this  grace period and always prune
 	unreachable objects immediately.
 
+gc.prunereposexpire::
+	When 'git gc' is run, it will call
+	'prune --repos --expire 3.months.ago'.
+	Override the grace period with this config variable. The value
+	"now" may be used to disable the grace period and prune
+	$GIT_DIR/repos immediately.
+
 gc.reflogexpire::
 gc.<pattern>.reflogexpire::
 	'git reflog expire' removes reflog entries older than
diff --git a/builtin/gc.c b/builtin/gc.c
index e38c902..0c65808 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -33,11 +33,13 @@ static int gc_auto_threshold = 6700;
 static int gc_auto_pack_limit = 50;
 static int detach_auto = 1;
 static const char *prune_expire = "2.weeks.ago";
+static const char *prune_repos_expire = "3.months.ago";
 
 static struct argv_array pack_refs_cmd = ARGV_ARRAY_INIT;
 static struct argv_array reflog = ARGV_ARRAY_INIT;
 static struct argv_array repack = ARGV_ARRAY_INIT;
 static struct argv_array prune = ARGV_ARRAY_INIT;
+static struct argv_array prune_repos = ARGV_ARRAY_INIT;
 static struct argv_array rerere = ARGV_ARRAY_INIT;
 
 static char *pidfile;
@@ -97,6 +99,8 @@ static int gc_config(const char *var, const char *value, void *cb)
 	}
 	if (!strcmp(var, "gc.pruneexpire"))
 		return git_config_date_string(&prune_expire, var, value);
+	if (!strcmp(var, "gc.prunereposexpire"))
+		return git_config_date_string(&prune_repos_expire, var, value);
 	return git_default_config(var, value, cb);
 }
 
@@ -304,6 +308,7 @@ int cmd_gc(int argc, const char **argv, const char *prefix)
 	argv_array_pushl(&reflog, "reflog", "expire", "--all", NULL);
 	argv_array_pushl(&repack, "repack", "-d", "-l", NULL);
 	argv_array_pushl(&prune, "prune", "--expire", NULL);
+	argv_array_pushl(&prune_repos, "prune", "--repos", "--expire", NULL);
 	argv_array_pushl(&rerere, "rerere", "gc", NULL);
 
 	git_config(gc_config, NULL);
@@ -373,6 +378,12 @@ int cmd_gc(int argc, const char **argv, const char *prefix)
 			return error(FAILED_RUN, prune.argv[0]);
 	}
 
+	if (prune_repos_expire) {
+		argv_array_push(&prune_repos, prune_repos_expire);
+		if (run_command_v_opt(prune_repos.argv, RUN_GIT_CMD))
+			return error(FAILED_RUN, prune_repos.argv[0]);
+	}
+
 	if (run_command_v_opt(rerere.argv, RUN_GIT_CMD))
 		return error(FAILED_RUN, rerere.argv[0]);
 
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 29/32] count-objects: report unused files in $GIT_DIR/repos/...
  2014-08-30  8:33 [PATCH 00/32] nd/multiple-work-trees cleanup Nguyễn Thái Ngọc Duy
                   ` (27 preceding siblings ...)
  2014-08-30  8:33 ` [PATCH 28/32] gc: support prune --repos Nguyễn Thái Ngọc Duy
@ 2014-08-30  8:33 ` Nguyễn Thái Ngọc Duy
  2014-08-30  8:34 ` [PATCH 30/32] git_path(): keep "info/sparse-checkout" per work-tree Nguyễn Thái Ngọc Duy
                   ` (5 subsequent siblings)
  34 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-08-30  8:33 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

In linked checkouts, borrowed parts like config is taken from
$GIT_COMMON_DIR. $GIT_DIR/config is never used. Report them as
garbage.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 builtin/count-objects.c |  4 +++-
 cache.h                 |  1 +
 path.c                  | 29 +++++++++++++++++++++++++++--
 3 files changed, 31 insertions(+), 3 deletions(-)

diff --git a/builtin/count-objects.c b/builtin/count-objects.c
index a7f70cb..d3a1620 100644
--- a/builtin/count-objects.c
+++ b/builtin/count-objects.c
@@ -102,8 +102,10 @@ int cmd_count_objects(int argc, const char **argv, const char *prefix)
 	/* we do not take arguments other than flags for now */
 	if (argc)
 		usage_with_options(count_objects_usage, opts);
-	if (verbose)
+	if (verbose) {
 		report_garbage = real_report_garbage;
+		report_linked_checkout_garbage();
+	}
 	memcpy(path, objdir, len);
 	if (len && objdir[len-1] != '/')
 		path[len++] = '/';
diff --git a/cache.h b/cache.h
index bf4d15e..12f0fc0 100644
--- a/cache.h
+++ b/cache.h
@@ -690,6 +690,7 @@ extern const char *mkpath(const char *fmt, ...) __attribute__((format (printf, 1
 extern const char *git_path(const char *fmt, ...) __attribute__((format (printf, 1, 2)));
 extern const char *git_path_submodule(const char *path, const char *fmt, ...)
 	__attribute__((format (printf, 2, 3)));
+extern void report_linked_checkout_garbage(void);
 
 /*
  * Return the name of the file in the local object database that would
diff --git a/path.c b/path.c
index e41d6b3..b5af137 100644
--- a/path.c
+++ b/path.c
@@ -4,6 +4,7 @@
 #include "cache.h"
 #include "strbuf.h"
 #include "string-list.h"
+#include "dir.h"
 
 static int get_st_mode_bits(const char *path, int *mode)
 {
@@ -91,9 +92,9 @@ static void replace_dir(struct strbuf *buf, int len, const char *newdir)
 }
 
 static const char *common_list[] = {
-	"/branches", "/hooks", "/info", "/logs", "/lost-found", "/modules",
+	"/branches", "/hooks", "/info", "!/logs", "/lost-found", "/modules",
 	"/objects", "/refs", "/remotes", "/repos", "/rr-cache", "/svn",
-	"config", "gc.pid", "packed-refs", "shallow",
+	"config", "!gc.pid", "packed-refs", "shallow",
 	NULL
 };
 
@@ -107,6 +108,8 @@ static void update_common_dir(struct strbuf *buf, int git_dir_len)
 	for (p = common_list; *p; p++) {
 		const char *path = *p;
 		int is_dir = 0;
+		if (*path == '!')
+			path++;
 		if (*path == '/') {
 			path++;
 			is_dir = 1;
@@ -122,6 +125,28 @@ static void update_common_dir(struct strbuf *buf, int git_dir_len)
 	}
 }
 
+void report_linked_checkout_garbage(void)
+{
+	struct strbuf sb = STRBUF_INIT;
+	const char **p;
+	int len;
+
+	if (!git_common_dir_env)
+		return;
+	strbuf_addf(&sb, "%s/", get_git_dir());
+	len = sb.len;
+	for (p = common_list; *p; p++) {
+		const char *path = *p;
+		if (*path == '!')
+			continue;
+		strbuf_setlen(&sb, len);
+		strbuf_addstr(&sb, path);
+		if (file_exists(sb.buf))
+			report_garbage("unused in linked checkout", sb.buf);
+	}
+	strbuf_release(&sb);
+}
+
 static void adjust_git_path(struct strbuf *buf, int git_dir_len)
 {
 	const char *base = buf->buf + git_dir_len;
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 30/32] git_path(): keep "info/sparse-checkout" per work-tree
  2014-08-30  8:33 [PATCH 00/32] nd/multiple-work-trees cleanup Nguyễn Thái Ngọc Duy
                   ` (28 preceding siblings ...)
  2014-08-30  8:33 ` [PATCH 29/32] count-objects: report unused files in $GIT_DIR/repos/ Nguyễn Thái Ngọc Duy
@ 2014-08-30  8:34 ` Nguyễn Thái Ngọc Duy
  2014-08-30  8:34 ` [PATCH 31/32] checkout: don't require a work tree when checking out into a new one Nguyễn Thái Ngọc Duy
                   ` (4 subsequent siblings)
  34 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-08-30  8:34 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Currently git_path("info/sparse-checkout") resolves to
$GIT_COMMON_DIR/info/sparse-checkout in multiple worktree mode. It
makes more sense for the sparse checkout patterns to be per worktree,
so you can have multiple checkouts with different parts of the tree.

With this, "git checkout --to <new>" on a sparse checkout will create
<new> as a full checkout. Which is expected, it's how a new checkout
is made. The user can reshape the worktree afterwards.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 path.c                | 3 ++-
 t/t0060-path-utils.sh | 1 +
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/path.c b/path.c
index b5af137..b1dd2bc 100644
--- a/path.c
+++ b/path.c
@@ -103,7 +103,8 @@ static void update_common_dir(struct strbuf *buf, int git_dir_len)
 	char *base = buf->buf + git_dir_len;
 	const char **p;
 
-	if (is_dir_file(base, "logs", "HEAD"))
+	if (is_dir_file(base, "logs", "HEAD") ||
+	    is_dir_file(base, "info", "sparse-checkout"))
 		return;	/* keep this in $GIT_DIR */
 	for (p = common_list; *p; p++) {
 		const char *path = *p;
diff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh
index f5d6f80..93605f4 100755
--- a/t/t0060-path-utils.sh
+++ b/t/t0060-path-utils.sh
@@ -270,6 +270,7 @@ test_git_path GIT_COMMON_DIR=bar objects                  bar/objects
 test_git_path GIT_COMMON_DIR=bar objects/bar              bar/objects/bar
 test_git_path GIT_COMMON_DIR=bar info/exclude             bar/info/exclude
 test_git_path GIT_COMMON_DIR=bar info/grafts              bar/info/grafts
+test_git_path GIT_COMMON_DIR=bar info/sparse-checkout     .git/info/sparse-checkout
 test_git_path GIT_COMMON_DIR=bar remotes/bar              bar/remotes/bar
 test_git_path GIT_COMMON_DIR=bar branches/bar             bar/branches/bar
 test_git_path GIT_COMMON_DIR=bar logs/refs/heads/master   bar/logs/refs/heads/master
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 31/32] checkout: don't require a work tree when checking out into a new one
  2014-08-30  8:33 [PATCH 00/32] nd/multiple-work-trees cleanup Nguyễn Thái Ngọc Duy
                   ` (29 preceding siblings ...)
  2014-08-30  8:34 ` [PATCH 30/32] git_path(): keep "info/sparse-checkout" per work-tree Nguyễn Thái Ngọc Duy
@ 2014-08-30  8:34 ` Nguyễn Thái Ngọc Duy
  2014-08-30  8:34 ` [PATCH 32/32] t2025: add a test to make sure grafts is working from a linked checkout Nguyễn Thái Ngọc Duy
                   ` (3 subsequent siblings)
  34 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-08-30  8:34 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Dennis Kaarsemaker,
	Nguyễn Thái Ngọc Duy

From: Dennis Kaarsemaker <dennis@kaarsemaker.net>

For normal use cases, it does not make sense for 'checkout' to work on
a bare repository, without a worktree. But "checkout --to" is an
exception because it _creates_ a new worktree. Allow this option to
run on bare repositories.

People who check out from a bare repository should remember that
core.logallrefupdates is off by default and it should be turned back
on. `--to` cannot do this automatically behind the user's back because
some user may deliberately want no reflog.

For people interested in repository setup/discovery code,
is_bare_repository_cfg (aka "core.bare") is unchanged by this patch,
which means 'true' by default for bare repos. Fortunately when we get
the repo through a linked checkout, is_bare_repository_cfg is never
used. So all is still good.

[nd: commit message]

Signed-off-by: Dennis Kaarsemaker <dennis@kaarsemaker.net>
Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
---
 builtin/checkout.c     |  3 +++
 git.c                  |  2 +-
 t/t2025-checkout-to.sh | 15 +++++++++++++++
 3 files changed, 19 insertions(+), 1 deletion(-)

diff --git a/builtin/checkout.c b/builtin/checkout.c
index 0f28b4a..b88c646 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -1361,6 +1361,9 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
 	if (opts.new_worktree_mode)
 		opts.new_worktree = NULL;
 
+	if (!opts.new_worktree)
+		setup_work_tree();
+
 	if (conflict_style) {
 		opts.merge = 1; /* implied */
 		git_xmerge_config("merge.conflictstyle", conflict_style, NULL);
diff --git a/git.c b/git.c
index 5b6c761..7426651 100644
--- a/git.c
+++ b/git.c
@@ -383,7 +383,7 @@ static struct cmd_struct commands[] = {
 	{ "check-ignore", cmd_check_ignore, RUN_SETUP | NEED_WORK_TREE },
 	{ "check-mailmap", cmd_check_mailmap, RUN_SETUP },
 	{ "check-ref-format", cmd_check_ref_format },
-	{ "checkout", cmd_checkout, RUN_SETUP | NEED_WORK_TREE },
+	{ "checkout", cmd_checkout, RUN_SETUP },
 	{ "checkout-index", cmd_checkout_index,
 		RUN_SETUP | NEED_WORK_TREE},
 	{ "cherry", cmd_cherry, RUN_SETUP },
diff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh
index 796472b..9f701a1 100755
--- a/t/t2025-checkout-to.sh
+++ b/t/t2025-checkout-to.sh
@@ -81,4 +81,19 @@ test_expect_success 'not die on re-checking out current branch' '
 	)
 '
 
+test_expect_success 'checkout --to from a bare repo' '
+	(
+		git clone --bare . bare &&
+		cd bare &&
+		git checkout --to ../there2 -b bare-master master
+	)
+'
+
+test_expect_success 'checkout from a bare repo without --to' '
+	(
+		cd bare &&
+		test_must_fail git checkout master
+	)
+'
+
 test_done
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH 32/32] t2025: add a test to make sure grafts is working from a linked checkout
  2014-08-30  8:33 [PATCH 00/32] nd/multiple-work-trees cleanup Nguyễn Thái Ngọc Duy
                   ` (30 preceding siblings ...)
  2014-08-30  8:34 ` [PATCH 31/32] checkout: don't require a work tree when checking out into a new one Nguyễn Thái Ngọc Duy
@ 2014-08-30  8:34 ` Nguyễn Thái Ngọc Duy
  2014-08-30 11:11 ` [PATCH 00/32] nd/multiple-work-trees cleanup Eric Sunshine
                   ` (2 subsequent siblings)
  34 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-08-30  8:34 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
---
 t/t2025-checkout-to.sh | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)

diff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh
index 9f701a1..aa24ea8 100755
--- a/t/t2025-checkout-to.sh
+++ b/t/t2025-checkout-to.sh
@@ -96,4 +96,22 @@ test_expect_success 'checkout from a bare repo without --to' '
 	)
 '
 
+test_expect_success 'checkout with grafts' '
+	test_when_finished rm .git/info/grafts &&
+	test_commit abc &&
+	SHA1=`git rev-parse HEAD` &&
+	test_commit def &&
+	test_commit xyz &&
+	echo "`git rev-parse HEAD` $SHA1" >.git/info/grafts &&
+	cat >expected <<-\EOF &&
+	xyz
+	abc
+	EOF
+	git log --format=%s -2 >actual &&
+	test_cmp expected actual &&
+	git checkout --detach --to grafted master &&
+	git --git-dir=grafted/.git log --format=%s -2 >actual &&
+	test_cmp expected actual
+'
+
 test_done
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* Re: [PATCH 00/32] nd/multiple-work-trees cleanup
  2014-08-30  8:33 [PATCH 00/32] nd/multiple-work-trees cleanup Nguyễn Thái Ngọc Duy
                   ` (31 preceding siblings ...)
  2014-08-30  8:34 ` [PATCH 32/32] t2025: add a test to make sure grafts is working from a linked checkout Nguyễn Thái Ngọc Duy
@ 2014-08-30 11:11 ` Eric Sunshine
  2014-08-30 11:14   ` Duy Nguyen
  2014-09-02 17:29 ` Junio C Hamano
  2014-09-10 22:41 ` [PATCH v2 00/32] nd/multiple-work-trees Nguyễn Thái Ngọc Duy
  34 siblings, 1 reply; 134+ messages in thread
From: Eric Sunshine @ 2014-08-30 11:11 UTC (permalink / raw)
  To: Nguyễn Thái Ngọc Duy; +Cc: Git List, Junio C Hamano

On Sat, Aug 30, 2014 at 4:33 AM, Nguyễn Thái Ngọc Duy <pclouds@gmail.com> wrote:
> This collapes some bug fix patches into the main ones, adds a few more
> tests to cover recent changes, and removes advice.checkoutLocked (when
> things are controversal, probably best to go without them until they
> are settled). Diff against current version in 'pu'
>
> diff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt
> index 6bd82af..c9a7d1a 100644
> --- a/Documentation/gitrepository-layout.txt
> +++ b/Documentation/gitrepository-layout.txt
> @@ -218,11 +218,10 @@ remotes::
>         "$GIT_COMMON_DIR/remotes" will be used instead.
>
>  logs::
> -       Records of changes made to refs are stored in this
> -       directory.  See linkgit:git-update-ref[1]
> -       for more information. This directory is ignored if
> -       $GIT_COMMON_DIR is set and "$GIT_COMMON_DIR/logs" will be used
> -       instead.
> +       Records of changes made to refs are stored in this directory.
> +       See linkgit:git-update-ref[1] for more information. This
> +       directory is ignored $GIT_COMMON_DIR is set and

The 'if' got lost again: s/ignored/ignored if/

> +       "$GIT_COMMON_DIR/logs" will be used instead.
>
>  logs/refs/heads/`name`::
>         Records all changes made to the branch tip named `name`.

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH 00/32] nd/multiple-work-trees cleanup
  2014-08-30 11:11 ` [PATCH 00/32] nd/multiple-work-trees cleanup Eric Sunshine
@ 2014-08-30 11:14   ` Duy Nguyen
  0 siblings, 0 replies; 134+ messages in thread
From: Duy Nguyen @ 2014-08-30 11:14 UTC (permalink / raw)
  To: Eric Sunshine; +Cc: Git List, Junio C Hamano

On Sat, Aug 30, 2014 at 6:11 PM, Eric Sunshine <sunshine@sunshineco.com> wrote:
> On Sat, Aug 30, 2014 at 4:33 AM, Nguyễn Thái Ngọc Duy <pclouds@gmail.com> wrote:
>> This collapes some bug fix patches into the main ones, adds a few more
>> tests to cover recent changes, and removes advice.checkoutLocked (when
>> things are controversal, probably best to go without them until they
>> are settled). Diff against current version in 'pu'
>>
>> diff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt
>> index 6bd82af..c9a7d1a 100644
>> --- a/Documentation/gitrepository-layout.txt
>> +++ b/Documentation/gitrepository-layout.txt
>> @@ -218,11 +218,10 @@ remotes::
>>         "$GIT_COMMON_DIR/remotes" will be used instead.
>>
>>  logs::
>> -       Records of changes made to refs are stored in this
>> -       directory.  See linkgit:git-update-ref[1]
>> -       for more information. This directory is ignored if
>> -       $GIT_COMMON_DIR is set and "$GIT_COMMON_DIR/logs" will be used
>> -       instead.
>> +       Records of changes made to refs are stored in this directory.
>> +       See linkgit:git-update-ref[1] for more information. This
>> +       directory is ignored $GIT_COMMON_DIR is set and
>
> The 'if' got lost again: s/ignored/ignored if/

Argghh.. 'if' hates me, or I hate it. Junio please put it back. I
don't think I should send another 32 mails for one 'if'.
-- 
Duy

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH 22/32] checkout: support checking out into a new working directory
  2014-08-30  8:33 ` [PATCH 22/32] checkout: support checking out into a new working directory Nguyễn Thái Ngọc Duy
@ 2014-08-30 20:50   ` Philip Oakley
  2014-08-31  4:49     ` Duy Nguyen
  0 siblings, 1 reply; 134+ messages in thread
From: Philip Oakley @ 2014-08-30 20:50 UTC (permalink / raw)
  To: Nguyễn Thái Ngọc Duy, git
  Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

From: "Nguyễn Thái Ngọc Duy" <pclouds@gmail.com>
> "git checkout --to" sets up a new working directory with a .git file
> pointing to $GIT_DIR/repos/<id>. It then executes "git checkout" again
> on the new worktree with the same arguments except "--to" is taken
> out. The second checkout execution, which is not contaminated with any
> info from the current repository, will actually check out and
> everything that normal "git checkout" does.
>
> Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
> ---
> Documentation/git-checkout.txt         | 34 ++++++++++++
> Documentation/git.txt                  |  3 +-
> Documentation/gitrepository-layout.txt |  7 +++
> builtin/checkout.c                     | 95 
> +++++++++++++++++++++++++++++++++-
> path.c                                 |  2 +-
> t/t2025-checkout-to.sh (new +x)        | 63 ++++++++++++++++++++++
> 6 files changed, 200 insertions(+), 4 deletions(-)
> create mode 100755 t/t2025-checkout-to.sh
>
> diff --git a/Documentation/git-checkout.txt 
> b/Documentation/git-checkout.txt
> index 33ad2ad..38c70c5 100644
> --- a/Documentation/git-checkout.txt
> +++ b/Documentation/git-checkout.txt
> @@ -225,6 +225,13 @@ This means that you can use `git checkout -p` to 
> selectively discard
> edits from your current working tree. See the ``Interactive Mode''
> section of linkgit:git-add[1] to learn how to operate the `--patch` 
> mode.
>
> +--to=<path>::
> + Check out a new branch in a separate working directory at

Is this actually a 'new' branch? Perhaps s/new //. More below.

> + `<path>`. A new working directory is linked to the current
> + repository, sharing everything except working directory
> + specific files such as HEAD, index... See "MULTIPLE CHECKOUT
> + MODE" section for more information.
> +
> <branch>::
>  Branch to checkout; if it refers to a branch (i.e., a name that,
>  when prepended with "refs/heads/", is a valid ref), then that
> @@ -388,6 +395,33 @@ $ git reflog -2 HEAD # or
> $ git log -g -2 HEAD
> ------------
>
> +MULTIPLE CHECKOUT MODE
> +----------------------
> +Normally a working directory is attached to repository. When "git
> +checkout --to" is used, a new working directory is attached to the
> +current repository. This new working directory is called "linked
> +checkout" as compared to the "main checkout" prepared by "git init" 
> or
> +"git clone". A repository has one main checkout and zero or more
> +linked checkouts.
> +
> +All checkouts share the same repository. Linked checkouts see the
> +repository a bit different from the main checkout. When the checkout
> +"new" reads the path $GIT_DIR/HEAD for example, the actual path

I was caught out by "new" (in quotes) here. I see that the commit msg in 
patch 24 uses 'new-branch', while in patch 30 <new> is used. Is this 
"new" (above) refering to the branch name (or detatched head), or a 
nmemonic for the path? The use of "new working directory" in the 
previous paragraph is a well qualified use of new, so I'm looking to 
ensure that it's well qualified here.

My immediate reaction was that a consistent use of <new> in the three 
patches may be all that's needed

> +returned could be $GIT_DIR/repos/new/HEAD. This ensures checkouts

surely s/could/would/? or have I misunderstood?
Also maybe s/new/<new-name>/, given the 'usually named' below, or 
s/new/<id>/ based on the commit message.

> +won't step on each other.
> +
> +Each linked checkout has a private space in $GIT_DIR/repos, usually
> +named after the base name of the working directory with a number 
> added
> +to make it unique. The linked checkout's $GIT_DIR points to this
> +private space while $GIT_COMMON_DIR points to the main checkout's
> +$GIT_DIR. These settings are done by "git checkout --to".
> +
> +Because in this mode $GIT_DIR becomes a lightweight virtual file
> +system where a path could be rewritten to some place else, accessing
> +$GIT_DIR from scripts should use `git rev-parse --git-path` to 
> resolve
> +a path instead of using it directly unless the path is known to be
> +private to the working directory.
> +
> EXAMPLES
> --------
>
> diff --git a/Documentation/git.txt b/Documentation/git.txt
> index 749052f..c0a4940 100644
> --- a/Documentation/git.txt
> +++ b/Documentation/git.txt
> @@ -792,7 +792,8 @@ Git so take care if using Cogito etc.
>  If this variable is set to a path, non-worktree files that are
>  normally in $GIT_DIR will be taken from this path
>  instead. Worktree-specific files such as HEAD or index are
> - taken from $GIT_DIR. See linkgit:gitrepository-layout[5] for
> + taken from $GIT_DIR. See linkgit:gitrepository-layout[5] and
> + the section 'MULTIPLE CHECKOUT MODE' in linkgit:checkout[1]
>  details. This variable has lower precedence than other path
>  variables such as GIT_INDEX_FILE, GIT_OBJECT_DIRECTORY...
>
> diff --git a/Documentation/gitrepository-layout.txt 
> b/Documentation/gitrepository-layout.txt
> index 58d1087..fab398a 100644
> --- a/Documentation/gitrepository-layout.txt
> +++ b/Documentation/gitrepository-layout.txt
> @@ -248,6 +248,13 @@ modules::
>  directory is ignored if $GIT_COMMON_DIR is set and
>  "$GIT_COMMON_DIR/modules" will be used instead.
>
> +repos::
> + Contains worktree specific information of linked
> + checkouts. Each subdirectory contains the worktree-related
> + part of a linked checkout. This directory is ignored if
> + $GIT_COMMON_DIR is set and "$GIT_COMMON_DIR/repos" will be
> + used instead.
> +
> SEE ALSO
> --------
> linkgit:git-init[1],
> diff --git a/builtin/checkout.c b/builtin/checkout.c
> index 8023987..6373823 100644
> --- a/builtin/checkout.c
> +++ b/builtin/checkout.c
> @@ -48,6 +48,10 @@ struct checkout_opts {
>  const char *prefix;
>  struct pathspec pathspec;
>  struct tree *source_tree;
> +
> + const char *new_worktree;
> + const char **saved_argv;
> + int new_worktree_mode;
> };
>
> static int post_checkout_hook(struct commit *old, struct commit *new,
> @@ -250,6 +254,9 @@ static int checkout_paths(const struct 
> checkout_opts *opts,
>  die(_("Cannot update paths and switch to branch '%s' at the same 
> time."),
>      opts->new_branch);
>
> + if (opts->new_worktree)
> + die(_("'%s' cannot be used with updating paths"), "--to");
> +
>  if (opts->patch_mode)
>  return run_add_interactive(revision, "--patch=checkout",
>     &opts->pathspec);
> @@ -485,7 +492,7 @@ static int merge_working_tree(const struct 
> checkout_opts *opts,
>  topts.dir->flags |= DIR_SHOW_IGNORED;
>  setup_standard_excludes(topts.dir);
>  }
> - tree = parse_tree_indirect(old->commit ?
> + tree = parse_tree_indirect(old->commit && !opts->new_worktree_mode ?
>     old->commit->object.sha1 :
>     EMPTY_TREE_SHA1_BIN);
>  init_tree_desc(&trees[0], tree->buffer, tree->size);
> @@ -796,7 +803,8 @@ static int switch_branches(const struct 
> checkout_opts *opts,
>  return ret;
>  }
>
> - if (!opts->quiet && !old.path && old.commit && new->commit != 
> old.commit)
> + if (!opts->quiet && !old.path && old.commit &&
> +     new->commit != old.commit && !opts->new_worktree_mode)
>  orphaned_commit_warning(old.commit, new->commit);
>
>  update_refs_for_switch(opts, &old, new);
> @@ -806,6 +814,76 @@ static int switch_branches(const struct 
> checkout_opts *opts,
>  return ret || writeout_error;
> }
>
> +static int prepare_linked_checkout(const struct checkout_opts *opts,
> +    struct branch_info *new)
> +{
> + struct strbuf sb_git = STRBUF_INIT, sb_repo = STRBUF_INIT;
> + struct strbuf sb = STRBUF_INIT;
> + const char *path = opts->new_worktree, *name;
> + struct stat st;
> + struct child_process cp;
> + int counter = 0, len;
> +
> + if (!new->commit)
> + die(_("no branch specified"));
> + if (file_exists(path))
> + die(_("'%s' already exists"), path);
> +
> + len = strlen(path);
> + while (len && is_dir_sep(path[len - 1]))
> + len--;
> +
> + for (name = path + len - 1; name > path; name--)
> + if (is_dir_sep(*name)) {
> + name++;
> + break;
> + }
> + strbuf_addstr(&sb_repo,
> +       git_path("repos/%.*s", (int)(path + len - name), name));
> + len = sb_repo.len;
> + if (safe_create_leading_directories_const(sb_repo.buf))
> + die_errno(_("could not create leading directories of '%s'"),
> +   sb_repo.buf);
> + while (!stat(sb_repo.buf, &st)) {
> + counter++;
> + strbuf_setlen(&sb_repo, len);
> + strbuf_addf(&sb_repo, "%d", counter);
> + }
> + name = strrchr(sb_repo.buf, '/') + 1;
> + if (mkdir(sb_repo.buf, 0777))
> + die_errno(_("could not create directory of '%s'"), sb_repo.buf);
> +
> + strbuf_addf(&sb_git, "%s/.git", path);
> + if (safe_create_leading_directories_const(sb_git.buf))
> + die_errno(_("could not create leading directories of '%s'"),
> +   sb_git.buf);
> +
> + write_file(sb_git.buf, 1, "gitdir: %s/repos/%s\n",
> +    real_path(get_git_common_dir()), name);
> + /*
> + * This is to keep resolve_ref() happy. We need a valid HEAD
> + * or is_git_directory() will reject the directory. Any valid
> + * value would do because this value will be ignored and
> + * replaced at the next (real) checkout.
> + */
> + strbuf_addf(&sb, "%s/HEAD", sb_repo.buf);
> + write_file(sb.buf, 1, "%s\n", 
> sha1_to_hex(new->commit->object.sha1));
> + strbuf_reset(&sb);
> + strbuf_addf(&sb, "%s/commondir", sb_repo.buf);
> + write_file(sb.buf, 1, "../..\n");
> +
> + if (!opts->quiet)
> + fprintf_ln(stderr, _("Enter %s (identifier %s)"), path, name);
> +
> + setenv("GIT_CHECKOUT_NEW_WORKTREE", "1", 1);
> + setenv(GIT_DIR_ENVIRONMENT, sb_git.buf, 1);
> + setenv(GIT_WORK_TREE_ENVIRONMENT, path, 1);
> + memset(&cp, 0, sizeof(cp));
> + cp.git_cmd = 1;
> + cp.argv = opts->saved_argv;
> + return run_command(&cp);
> +}
> +
> static int git_checkout_config(const char *var, const char *value, 
> void *cb)
> {
>  if (!strcmp(var, "diff.ignoresubmodules")) {
> @@ -1067,6 +1145,9 @@ static int checkout_branch(struct checkout_opts 
> *opts,
>  die(_("Cannot switch branch to a non-commit '%s'"),
>      new->name);
>
> + if (opts->new_worktree)
> + return prepare_linked_checkout(opts, new);
> +
>  if (!new->commit && opts->new_branch) {
>  unsigned char rev[20];
>  int flag;
> @@ -1109,6 +1190,8 @@ int cmd_checkout(int argc, const char **argv, 
> const char *prefix)
>  N_("do not limit pathspecs to sparse entries only")),
>  OPT_HIDDEN_BOOL(0, "guess", &dwim_new_local_branch,
>  N_("second guess 'git checkout no-such-branch'")),
> + OPT_FILENAME(0, "to", &opts.new_worktree,
> +    N_("check a branch out in a separate working directory")),
>  OPT_END(),
>  };
>
> @@ -1117,6 +1200,9 @@ int cmd_checkout(int argc, const char **argv, 
> const char *prefix)
>  opts.overwrite_ignore = 1;
>  opts.prefix = prefix;
>
> + opts.saved_argv = xmalloc(sizeof(const char *) * (argc + 2));
> + memcpy(opts.saved_argv, argv, sizeof(const char *) * (argc + 1));
> +
>  gitmodules_config();
>  git_config(git_checkout_config, &opts);
>
> @@ -1125,6 +1211,11 @@ int cmd_checkout(int argc, const char **argv, 
> const char *prefix)
>  argc = parse_options(argc, argv, prefix, options, checkout_usage,
>       PARSE_OPT_KEEP_DASHDASH);
>
> + /* recursive execution from checkout_new_worktree() */
> + opts.new_worktree_mode = getenv("GIT_CHECKOUT_NEW_WORKTREE") != 
> NULL;
> + if (opts.new_worktree_mode)
> + opts.new_worktree = NULL;
> +
>  if (conflict_style) {
>  opts.merge = 1; /* implied */
>  git_xmerge_config("merge.conflictstyle", conflict_style, NULL);
> diff --git a/path.c b/path.c
> index 8a6586c..e41d6b3 100644
> --- a/path.c
> +++ b/path.c
> @@ -92,7 +92,7 @@ static void replace_dir(struct strbuf *buf, int len, 
> const char *newdir)
>
> static const char *common_list[] = {
>  "/branches", "/hooks", "/info", "/logs", "/lost-found", "/modules",
> - "/objects", "/refs", "/remotes", "/rr-cache", "/svn",
> + "/objects", "/refs", "/remotes", "/repos", "/rr-cache", "/svn",
>  "config", "gc.pid", "packed-refs", "shallow",
>  NULL
> };
> diff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh
> new file mode 100755
> index 0000000..8c73b18
> --- /dev/null
> +++ b/t/t2025-checkout-to.sh
> @@ -0,0 +1,63 @@
> +#!/bin/sh
> +
> +test_description='test git checkout --to'
> +
> +. ./test-lib.sh
> +
> +test_expect_success 'setup' '
> + test_commit init
> +'
> +
> +test_expect_success 'checkout --to not updating paths' '
> + test_must_fail git checkout --to -- init.t
> +'
> +
> +test_expect_success 'checkout --to an existing worktree' '
> + mkdir existing &&
> + test_must_fail git checkout --detach --to existing master
> +'
> +
> +test_expect_success 'checkout --to a new worktree' '
> + git checkout --to here master &&
> + (
> + cd here &&
> + test_cmp ../init.t init.t &&
> + git symbolic-ref HEAD >actual &&
> + echo refs/heads/master >expect &&
> + test_cmp expect actual &&
> + git fsck
> + )
> +'
> +
> +test_expect_success 'checkout --to a new worktree from a subdir' '
> + (
> + mkdir sub &&
> + cd sub &&
> + git checkout --detach --to here master &&
> + cd here &&
> + test_cmp ../../init.t init.t
> + )
> +'
> +
> +test_expect_success 'checkout --to from a linked checkout' '
> + (
> + cd here &&
> + git checkout --to nested-here master
> + cd nested-here &&
> + git fsck
> + )
> +'
> +
> +test_expect_success 'checkout --to a new worktree creating new 
> branch' '
> + git checkout --to there -b newmaster master &&
> + (
> + cd there &&
> + test_cmp ../init.t init.t &&
> + git symbolic-ref HEAD >actual &&
> + echo refs/heads/newmaster >expect &&
> + test_cmp expect actual &&
> + git fsck
> + )
> +'
> +
> +test_done
> -- 
> 2.1.0.rc0.78.gc0d8480
>
> --
Philip 

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH 22/32] checkout: support checking out into a new working directory
  2014-08-30 20:50   ` Philip Oakley
@ 2014-08-31  4:49     ` Duy Nguyen
  2014-08-31  4:52       ` Duy Nguyen
  0 siblings, 1 reply; 134+ messages in thread
From: Duy Nguyen @ 2014-08-31  4:49 UTC (permalink / raw)
  To: Philip Oakley; +Cc: Git Mailing List, Junio C Hamano

On Sun, Aug 31, 2014 at 3:50 AM, Philip Oakley <philipoakley@iee.org> wrote:
>> @@ -225,6 +225,13 @@ This means that you can use `git checkout -p` to
>> selectively discard
>> edits from your current working tree. See the ``Interactive Mode''
>> section of linkgit:git-add[1] to learn how to operate the `--patch` mode.
>>
>> +--to=<path>::
>> + Check out a new branch in a separate working directory at
>
>
> Is this actually a 'new' branch? Perhaps s/new //. More below.

Only when -b is also used. Will fix.

>> +All checkouts share the same repository. Linked checkouts see the
>> +repository a bit different from the main checkout. When the checkout
>> +"new" reads the path $GIT_DIR/HEAD for example, the actual path
>
>
> I was caught out by "new" (in quotes) here. I see that the commit msg in
> patch 24 uses 'new-branch', while in patch 30 <new> is used. Is this "new"
> (above) refering to the branch name (or detatched head), or a nmemonic for
> the path? The use of "new working directory" in the previous paragraph is a
> well qualified use of new, so I'm looking to ensure that it's well qualified
> here.
>
> My immediate reaction was that a consistent use of <new> in the three
> patches may be all that's needed

It's probably clearer if I start that paragraph with "git checkout
--to <somewhere> <branch>" then "new" is replaced with "<branch>". The
thing is, each checkout needs a unique id, and this id is usually the
branch name to be checked out. But after worktree creation, even if
the user switches branch, the id remains the same.

>> +returned could be $GIT_DIR/repos/new/HEAD. This ensures checkouts
>
>
> surely s/could/would/? or have I misunderstood?

It's ../repos/new/...if the directory with the same name does not
exist. If it does, a number will follow after 'new' to make it unique,
e.g. ../repos/new1/... Perhaps this paragraph could be rewritten like
this

All checkouts share the same repository. Linked checkouts see the
repository a bit different from the main checkout. When you perform
the command

------------
git checkout --to <some-location> <some-branch>
------------

The checkout at <some-location> will have a unique id that is also the
branch name (e.g. "<some-branch>"). A number may be appended to the id
to make it unique. All worktree-specific files of this new checkout
are in $GIT_DIR/repos/<unique-id> of the main checkout. So "HEAD"
inside the linked checkout will be resolved to
"$GIT_DIR/repos/<some-branch>/HEAD", while "HEAD" from the main
checkout remains "$GIT_DIR/HEAD".
-- 
Duy

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH 22/32] checkout: support checking out into a new working directory
  2014-08-31  4:49     ` Duy Nguyen
@ 2014-08-31  4:52       ` Duy Nguyen
  2014-08-31 11:08         ` Philip Oakley
  0 siblings, 1 reply; 134+ messages in thread
From: Duy Nguyen @ 2014-08-31  4:52 UTC (permalink / raw)
  To: Philip Oakley; +Cc: Git Mailing List, Junio C Hamano

On Sun, Aug 31, 2014 at 11:49 AM, Duy Nguyen <pclouds@gmail.com> wrote:
> All checkouts share the same repository. Linked checkouts see the
> repository a bit different from the main checkout. When you perform
> the command
>
> ------------
> git checkout --to <some-location> <some-branch>
> ------------
>
> The checkout at <some-location> will have a unique id that is also the
> branch name (e.g. "<some-branch>"). A number may be appended to the id

.. and I got my facts wrong. The above line should be:

last component of <some-location>. A number may be appended to the id

> to make it unique. All worktree-specific files of this new checkout
> are in $GIT_DIR/repos/<unique-id> of the main checkout. So "HEAD"
> inside the linked checkout will be resolved to
> "$GIT_DIR/repos/<some-branch>/HEAD", while "HEAD" from the main

s/<some-branch>/<unique-id>/

> checkout remains "$GIT_DIR/HEAD".



-- 
Duy

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH 22/32] checkout: support checking out into a new working directory
  2014-08-31  4:52       ` Duy Nguyen
@ 2014-08-31 11:08         ` Philip Oakley
  2014-09-02 12:27           ` Duy Nguyen
  0 siblings, 1 reply; 134+ messages in thread
From: Philip Oakley @ 2014-08-31 11:08 UTC (permalink / raw)
  To: Duy Nguyen; +Cc: Git Mailing List, Junio C Hamano

From: "Duy Nguyen" <pclouds@gmail.com>
> On Sun, Aug 31, 2014 at 11:49 AM, Duy Nguyen <pclouds@gmail.com> 
> wrote:
>> All checkouts share the same repository. Linked checkouts see the
>> repository a bit different from the main checkout. When you perform
>> the command
>>
>> ------------
>> git checkout --to <some-location> <some-branch>
>> ------------
>>
>> The checkout at <some-location> will have a unique id that is also 
>> the
>> branch name (e.g. "<some-branch>"). A number may be appended to the 
>> id
>
> .. and I got my facts wrong. The above line should be:
>
> last component of <some-location>. A number may be appended to the id
>

That's fine. Glad that we're on the same wavelength for the 
documentation.

>> to make it unique. All worktree-specific files of this new checkout
>> are in $GIT_DIR/repos/<unique-id> of the main checkout. So "HEAD"
>> inside the linked checkout will be resolved to
>> "$GIT_DIR/repos/<some-branch>/HEAD", while "HEAD" from the main
>
> s/<some-branch>/<unique-id>/
>
>> checkout remains "$GIT_DIR/HEAD".
>
>
>
> -- 
Philip 

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH 22/32] checkout: support checking out into a new working directory
  2014-08-31 11:08         ` Philip Oakley
@ 2014-09-02 12:27           ` Duy Nguyen
  2014-09-02 14:51             ` Marc Branchaud
  0 siblings, 1 reply; 134+ messages in thread
From: Duy Nguyen @ 2014-09-02 12:27 UTC (permalink / raw)
  To: Git Mailing List; +Cc: Junio C Hamano, Philip Oakley

After reading this "multiple checkout mode" in git-checkout.txt, I'm
tempted to rewrite it like this. I think the example makes it clearer
what I mean. If nobody has any comments, I'm going to send v2 with
this (and other comments collected so far)

MULTIPLE CHECKOUT MODE
----------------------
Normally a working directory is attached to repository. When "git
checkout --to" is used, a new working directory is attached to the
current repository. This new working directory is called "linked
checkout" as compared to the "main checkout" prepared by "git init" or
"git clone". A repository has one main checkout and zero or more
linked checkouts.

Each linked checkout has a private directory in $GIT_DIR/repos in the
main checkout, usually named after the base name of the new working
directory, optionally with a number added to make it unique. For
example, the command `git checkout --to ../test-next next` running
with $GIT_DIR=/path/main may create the directory
`$GIT_DIR/repos/test-next` (or `$GIT_DIR/repos/test-next1` if
`test-next` is already taken).

Within a linked checkout, $GIT_DIR is set to point to this private
directory (e.g. `/path/main/repos/test-next` in the example) and
$GIT_COMMON_DIR is set to point back to the main checkout's $GIT_DIR
(e.g. `/path/main`). Setting is done via a .git file located at the
top directory of the linked checkout.

Path resolution via `git rev-parse --git-path` would use either
$GIT_DIR or $GIT_COMMON_DIR depending on the path. For example, the
linked checkout's `$GIT_DIR/HEAD` resolve to
`/path/main/repos/test-next/HEAD` (not `/path/main/HEAD` which is the
main checkout's HEAD) while `$GIT_DIR/refs/heads/master` would use
$GIT_COMMON_DIR and resolve to `/path/main/refs/heads/master`,
which is shared across checkouts.

See linkgit:gitrepository-layout[5] for more information. The rule of
thumb is do not make any assumption about whether a path belongs to
$GIT_DIR or $GIT_COMMON_DIR when you need to directly access something
inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.
--
Duy

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH 22/32] checkout: support checking out into a new working directory
  2014-09-02 12:27           ` Duy Nguyen
@ 2014-09-02 14:51             ` Marc Branchaud
  2014-09-02 17:33               ` Junio C Hamano
  2014-09-05  3:26               ` Scott Schmit
  0 siblings, 2 replies; 134+ messages in thread
From: Marc Branchaud @ 2014-09-02 14:51 UTC (permalink / raw)
  To: Duy Nguyen, Git Mailing List; +Cc: Junio C Hamano, Philip Oakley

On 14-09-02 08:27 AM, Duy Nguyen wrote:
> After reading this "multiple checkout mode" in git-checkout.txt, I'm
> tempted to rewrite it like this. I think the example makes it clearer
> what I mean. If nobody has any comments, I'm going to send v2 with
> this (and other comments collected so far)

Overall I think focusing on the word "checkout" for this feature makes the
documentation confusing.  It's also not a "mode" but just another git
feature.  Since this is all about multiple working directories (the phrase is
actually "working tree" in the existing docs) we should just stick to that
rather than introduce new terminology.

Finally, a bit of bikeshedding, but I think "$GIT_DIR/repos" is also an
unfortunate choice and that "$GIT_DIR/worktrees" would be better.

So I suggest the following for the new section:


MULTIPLE WORKING TREES
----------------------

A git repository can support multiple working trees, allowing you to check
out more than one branch at a time.  With `git checkout --to` a new working
tree is associated with the repository.  This new working tree is called a
"linked working tree" as opposed to the "main working tree" prepared by "git
init" or "git clone".  A repository has one main working tree (if it's not a
bare repository) and zero or more linked working trees.

Each linked working tree has a private sub-directory in the repository's
$GIT_DIR/worktrees directory.  The private sub-directory's name is usually
the base name of the linked working tree's path, possibly appended with a
number to make it unique.  For example, when `$GIT_DIR=/path/main/.git` the
command `git checkout --to /path/other/test-next next` creates the linked
working tree in `/path/other/test-next` and also creates a
`$GIT_DIR/worktrees/test-next` directory (or `$GIT_DIR/worktrees/test-next1`
if `test-next` is already taken).

Within a linked working tree, $GIT_DIR is set to point to this private
directory (e.g. `/path/main/.git/worktrees/test-next` in the example) and
$GIT_COMMON_DIR is set to point back to the main working tree's $GIT_DIR
(e.g. `/path/main/.git`). These settings are made in a `.git` file located at
the top directory of the linked working tree.

Path resolution via `git rev-parse --git-path` uses either
$GIT_DIR or $GIT_COMMON_DIR depending on the path. For example, in the
linked working tree `git rev-parse --git-path HEAD` returns
`/path/main/.git/worktrees/test-next/HEAD` (not
`/path/other/test-next/.git/HEAD` or `/path/main/.git/HEAD`) while `git
rev-parse --git-path refs/heads/master` uses
$GIT_COMMON_DIR and returns `/path/main/.git/refs/heads/master`,
since refs are shared across all working trees.

See linkgit:gitrepository-layout[5] for more information. The rule of
thumb is do not make any assumption about whether a path belongs to
$GIT_DIR or $GIT_COMMON_DIR when you need to directly access something
inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.


		M.

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH 00/32] nd/multiple-work-trees cleanup
  2014-08-30  8:33 [PATCH 00/32] nd/multiple-work-trees cleanup Nguyễn Thái Ngọc Duy
                   ` (32 preceding siblings ...)
  2014-08-30 11:11 ` [PATCH 00/32] nd/multiple-work-trees cleanup Eric Sunshine
@ 2014-09-02 17:29 ` Junio C Hamano
  2014-09-10 22:41 ` [PATCH v2 00/32] nd/multiple-work-trees Nguyễn Thái Ngọc Duy
  34 siblings, 0 replies; 134+ messages in thread
From: Junio C Hamano @ 2014-09-02 17:29 UTC (permalink / raw)
  To: Nguyễn Thái Ngọc Duy; +Cc: git

Nguyễn Thái Ngọc Duy  <pclouds@gmail.com> writes:

> This collapes some bug fix patches into the main ones, adds a few more
> tests to cover recent changes, and removes advice.checkoutLocked (when
> things are controversal, probably best to go without them until they
> are settled).

This essentially replaces how the refs (and other things, but the
most invasive part of the change is about refs) in $GIT_DIR/ are
accessed from other working trees via symbolic links in the
contrib/workdir script with a built-in logic.

Unfortunately the way $GIT_DIR/refs/ and $GIT_DIR/packed-refs are
accessed even in the main tree is being updated with a series of
large topics that are in flight X-<.

How well would they play together?

Will drop the one in 'pu' and replace with this round, but it may
turn out that I may have to ask you and Ronnie to coordinate these
topics.  We'll see.

Thanks.

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH 22/32] checkout: support checking out into a new working directory
  2014-09-02 14:51             ` Marc Branchaud
@ 2014-09-02 17:33               ` Junio C Hamano
  2014-09-04 14:19                 ` Duy Nguyen
  2014-09-08 10:52                 ` Duy Nguyen
  2014-09-05  3:26               ` Scott Schmit
  1 sibling, 2 replies; 134+ messages in thread
From: Junio C Hamano @ 2014-09-02 17:33 UTC (permalink / raw)
  To: Marc Branchaud; +Cc: Duy Nguyen, Git Mailing List, Philip Oakley

Marc Branchaud <marcnarc@xiplink.com> writes:

> On 14-09-02 08:27 AM, Duy Nguyen wrote:
>> After reading this "multiple checkout mode" in git-checkout.txt, I'm
>> tempted to rewrite it like this. I think the example makes it clearer
>> what I mean. If nobody has any comments, I'm going to send v2 with
>> this (and other comments collected so far)
>
> Overall I think focusing on the word "checkout" for this feature makes the
> documentation confusing.  It's also not a "mode" but just another git
> feature.  Since this is all about multiple working directories (the phrase is
> actually "working tree" in the existing docs) we should just stick to that
> rather than introduce new terminology.
>
> Finally, a bit of bikeshedding, but I think "$GIT_DIR/repos" is also an
> unfortunate choice and that "$GIT_DIR/worktrees" would be better.

I tried to stay away from bikeshedding, but a good phrasing is
important.  I think $GIT_DIR/worktrees captures what they are trying
to represent better in that they are not storing repository
information, but they are about storing per-worktree state.

> So I suggest the following for the new section:
>
>
> MULTIPLE WORKING TREES
> ----------------------
>
> A git repository can support multiple working trees, allowing you to check
> out more than one branch at a time.  With `git checkout --to` a new working
> tree is associated with the repository.  This new working tree is called a
> "linked working tree" as opposed to the "main working tree" prepared by "git
> init" or "git clone".  A repository has one main working tree (if it's not a
> bare repository) and zero or more linked working trees.
>
> Each linked working tree has a private sub-directory in the repository's
> $GIT_DIR/worktrees directory.  The private sub-directory's name is usually
> the base name of the linked working tree's path, possibly appended with a
> number to make it unique.  For example, when `$GIT_DIR=/path/main/.git` the
> command `git checkout --to /path/other/test-next next` creates the linked
> working tree in `/path/other/test-next` and also creates a
> `$GIT_DIR/worktrees/test-next` directory (or `$GIT_DIR/worktrees/test-next1`
> if `test-next` is already taken).
>
> Within a linked working tree, $GIT_DIR is set to point to this private
> directory (e.g. `/path/main/.git/worktrees/test-next` in the example) and
> $GIT_COMMON_DIR is set to point back to the main working tree's $GIT_DIR
> (e.g. `/path/main/.git`). These settings are made in a `.git` file located at
> the top directory of the linked working tree.
>
> Path resolution via `git rev-parse --git-path` uses either
> $GIT_DIR or $GIT_COMMON_DIR depending on the path. For example, in the
> linked working tree `git rev-parse --git-path HEAD` returns
> `/path/main/.git/worktrees/test-next/HEAD` (not
> `/path/other/test-next/.git/HEAD` or `/path/main/.git/HEAD`) while `git
> rev-parse --git-path refs/heads/master` uses
> $GIT_COMMON_DIR and returns `/path/main/.git/refs/heads/master`,
> since refs are shared across all working trees.
>
> See linkgit:gitrepository-layout[5] for more information. The rule of
> thumb is do not make any assumption about whether a path belongs to
> $GIT_DIR or $GIT_COMMON_DIR when you need to directly access something
> inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.
>
>
> 		M.

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH 22/32] checkout: support checking out into a new working directory
  2014-09-02 17:33               ` Junio C Hamano
@ 2014-09-04 14:19                 ` Duy Nguyen
  2014-09-08 10:52                 ` Duy Nguyen
  1 sibling, 0 replies; 134+ messages in thread
From: Duy Nguyen @ 2014-09-04 14:19 UTC (permalink / raw)
  To: Junio C Hamano, Marc Branchaud; +Cc: Git Mailing List, Philip Oakley

On Wed, Sep 3, 2014 at 12:33 AM, Junio C Hamano <gitster@pobox.com> wrote:
> Marc Branchaud <marcnarc@xiplink.com> writes:
>
>> On 14-09-02 08:27 AM, Duy Nguyen wrote:
>>> After reading this "multiple checkout mode" in git-checkout.txt, I'm
>>> tempted to rewrite it like this. I think the example makes it clearer
>>> what I mean. If nobody has any comments, I'm going to send v2 with
>>> this (and other comments collected so far)
>>
>> Overall I think focusing on the word "checkout" for this feature makes the
>> documentation confusing.  It's also not a "mode" but just another git
>> feature.  Since this is all about multiple working directories (the phrase is
>> actually "working tree" in the existing docs) we should just stick to that
>> rather than introduce new terminology.
>>
>> Finally, a bit of bikeshedding, but I think "$GIT_DIR/repos" is also an
>> unfortunate choice and that "$GIT_DIR/worktrees" would be better.
>
> I tried to stay away from bikeshedding, but a good phrasing is
> important.  I think $GIT_DIR/worktrees captures what they are trying
> to represent better in that they are not storing repository
> information, but they are about storing per-worktree state.

Good thing this topic is in pu. Will s/repos/worktrees/ then.

>> So I suggest the following for the new section:

Will replace my text with yours. Thanks.
-- 
Duy

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH 22/32] checkout: support checking out into a new working directory
  2014-09-02 14:51             ` Marc Branchaud
  2014-09-02 17:33               ` Junio C Hamano
@ 2014-09-05  3:26               ` Scott Schmit
  2014-09-05 13:42                 ` Duy Nguyen
  1 sibling, 1 reply; 134+ messages in thread
From: Scott Schmit @ 2014-09-05  3:26 UTC (permalink / raw)
  To: Marc Branchaud
  Cc: Duy Nguyen, Git Mailing List, Junio C Hamano, Philip Oakley

On Tue, Sep 02, 2014 at 10:51:47AM -0400, Marc Branchaud wrote:
> MULTIPLE WORKING TREES
> ----------------------
> 
> A git repository can support multiple working trees, allowing you to check
> out more than one branch at a time.  With `git checkout --to` a new working
> tree is associated with the repository.  This new working tree is called a
> "linked working tree" as opposed to the "main working tree" prepared by "git
> init" or "git clone".  A repository has one main working tree (if it's not a
> bare repository) and zero or more linked working trees.
> 
> Each linked working tree has a private sub-directory in the repository's
> $GIT_DIR/worktrees directory.  The private sub-directory's name is usually
> the base name of the linked working tree's path, possibly appended with a
> number to make it unique.  For example, when `$GIT_DIR=/path/main/.git` the
> command `git checkout --to /path/other/test-next next` creates the linked
> working tree in `/path/other/test-next` and also creates a
> `$GIT_DIR/worktrees/test-next` directory (or `$GIT_DIR/worktrees/test-next1`
> if `test-next` is already taken).

As a user, this leaves me with one other question -- what happens when
I'm done with the test-next working tree and want to delete/rename it?
Is that cleaned up automatically, or do I need to register that I'm
getting rid of/renaming it?  (Another use case is if I put the working
tree on removable media for some reason.)

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH 22/32] checkout: support checking out into a new working directory
  2014-09-05  3:26               ` Scott Schmit
@ 2014-09-05 13:42                 ` Duy Nguyen
  0 siblings, 0 replies; 134+ messages in thread
From: Duy Nguyen @ 2014-09-05 13:42 UTC (permalink / raw)
  To: Scott Schmit
  Cc: Marc Branchaud, Git Mailing List, Junio C Hamano, Philip Oakley

On Fri, Sep 5, 2014 at 10:26 AM, Scott Schmit <i.grok@comcast.net> wrote:
>> Each linked working tree has a private sub-directory in the repository's
>> $GIT_DIR/worktrees directory.  The private sub-directory's name is usually
>> the base name of the linked working tree's path, possibly appended with a
>> number to make it unique.  For example, when `$GIT_DIR=/path/main/.git` the
>> command `git checkout --to /path/other/test-next next` creates the linked
>> working tree in `/path/other/test-next` and also creates a
>> `$GIT_DIR/worktrees/test-next` directory (or `$GIT_DIR/worktrees/test-next1`
>> if `test-next` is already taken).
>
> As a user, this leaves me with one other question -- what happens when
> I'm done with the test-next working tree and want to delete/rename it?

If you rename it, the link remains and you can still continue to use
test-next with its new name. If you move it, or rename on a filesystem
without hardlink support, then you may need to run something from the
linked worktree for it to fix up the link to main checkout. If you
move it on the same filesystem that supports hardlink, it's all good.

If you delete it, the part in $GIT_DIR/worktrees remains and can be
cleaned up using "git prune --repos" (which I should rename to "git
prune --worktrees" as well). At some point in future, this pruning
should be part of git-gc. See patch 25/32 [1] for detail. I know I
haven't added some user documentation about pruning. You're welcome to
write up something about this ;)

Note to self, you someone moves the _main_ checkout away, then they're
screwed. Should probably make a note about that somewhere in
documents.

> Is that cleaned up automatically, or do I need to register that I'm
> getting rid of/renaming it?  (Another use case is if I put the working
> tree on removable media for some reason.)

Removable media is covered in [1] as well. Though you'll need to
"lock" the worktree in order to stop it from being pruned. In earlier
iterations, this locking could be done automatically at "git checkout
--to" time, if it detects that the new worktree is on a different
filesystem than the main one. But that got dropped. We can add it back
when this feature matures a bit.

[1] http://article.gmane.org/gmane.comp.version-control.git/256236
-- 
Duy

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH 22/32] checkout: support checking out into a new working directory
  2014-09-02 17:33               ` Junio C Hamano
  2014-09-04 14:19                 ` Duy Nguyen
@ 2014-09-08 10:52                 ` Duy Nguyen
  2014-09-08 14:06                   ` Marc Branchaud
  1 sibling, 1 reply; 134+ messages in thread
From: Duy Nguyen @ 2014-09-08 10:52 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Marc Branchaud, Git Mailing List, Philip Oakley

On Wed, Sep 3, 2014 at 12:33 AM, Junio C Hamano <gitster@pobox.com> wrote:
> Marc Branchaud <marcnarc@xiplink.com> writes:
>
>> On 14-09-02 08:27 AM, Duy Nguyen wrote:
>>> After reading this "multiple checkout mode" in git-checkout.txt, I'm
>>> tempted to rewrite it like this. I think the example makes it clearer
>>> what I mean. If nobody has any comments, I'm going to send v2 with
>>> this (and other comments collected so far)
>>
>> Overall I think focusing on the word "checkout" for this feature makes the
>> documentation confusing.  It's also not a "mode" but just another git
>> feature.  Since this is all about multiple working directories (the phrase is
>> actually "working tree" in the existing docs) we should just stick to that
>> rather than introduce new terminology.
>>
>> Finally, a bit of bikeshedding, but I think "$GIT_DIR/repos" is also an
>> unfortunate choice and that "$GIT_DIR/worktrees" would be better.
>
> I tried to stay away from bikeshedding, but a good phrasing is
> important.  I think $GIT_DIR/worktrees captures what they are trying
> to represent better in that they are not storing repository
> information, but they are about storing per-worktree state.

While we're changing the terms, I wonder if "primary working
directory" and "secondary working directories" are better than "main
checkout" and "linked checkout".
-- 
Duy

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH 22/32] checkout: support checking out into a new working directory
  2014-09-08 10:52                 ` Duy Nguyen
@ 2014-09-08 14:06                   ` Marc Branchaud
  0 siblings, 0 replies; 134+ messages in thread
From: Marc Branchaud @ 2014-09-08 14:06 UTC (permalink / raw)
  To: Duy Nguyen, Junio C Hamano; +Cc: Git Mailing List, Philip Oakley

On 14-09-08 06:52 AM, Duy Nguyen wrote:
> 
> While we're changing the terms, I wonder if "primary working
> directory" and "secondary working directories" are better than "main
> checkout" and "linked checkout".

I might have a slight preference for main/linked, because primary/secondary
can imply that there are further orders -- tertiary, quaternary, etc.  Also,
at least in English, "linked" is commonly used and it doesn't necessarily
imply an implementation (e.g. with hard or soft filesystem links).

(How many angels can dance on the threshold of an open bikeshed door? :) )

		M.

^ permalink raw reply	[flat|nested] 134+ messages in thread

* [PATCH v2 00/32] nd/multiple-work-trees
  2014-08-30  8:33 [PATCH 00/32] nd/multiple-work-trees cleanup Nguyễn Thái Ngọc Duy
                   ` (33 preceding siblings ...)
  2014-09-02 17:29 ` Junio C Hamano
@ 2014-09-10 22:41 ` Nguyễn Thái Ngọc Duy
  2014-09-10 22:41   ` [PATCH v2 01/32] path.c: make get_pathname() return strbuf instead of static buffer Nguyễn Thái Ngọc Duy
                     ` (32 more replies)
  34 siblings, 33 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-10 22:41 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Changes since last send (diff below):

 - rename $GIT_DIR/repos to $GIT_DIR/worktrees. update documents and
   command options accordingly
 - rewrite the multiple checkout section in git-checkout.txt
 - reorder some patches to make sure the test suite always pass

diff --git a/Documentation/config.txt b/Documentation/config.txt
index 57999fa..a85f684 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -1211,12 +1211,12 @@ gc.pruneexpire::
 	"now" may be used to disable this  grace period and always prune
 	unreachable objects immediately.
 
-gc.prunereposexpire::
+gc.pruneworktreesexpire::
 	When 'git gc' is run, it will call
-	'prune --repos --expire 3.months.ago'.
+	'prune --worktrees --expire 3.months.ago'.
 	Override the grace period with this config variable. The value
 	"now" may be used to disable the grace period and prune
-	$GIT_DIR/repos immediately.
+	$GIT_DIR/worktrees immediately.
 
 gc.reflogexpire::
 gc.<pattern>.reflogexpire::
diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt
index 38c70c5..23f0c80 100644
--- a/Documentation/git-checkout.txt
+++ b/Documentation/git-checkout.txt
@@ -226,7 +226,7 @@ edits from your current working tree. See the ``Interactive Mode''
 section of linkgit:git-add[1] to learn how to operate the `--patch` mode.
 
 --to=<path>::
-	Check out a new branch in a separate working directory at
+	Check out a branch in a separate working directory at
 	`<path>`. A new working directory is linked to the current
 	repository, sharing everything except working directory
 	specific files such as HEAD, index... See "MULTIPLE CHECKOUT
@@ -404,23 +404,50 @@ checkout" as compared to the "main checkout" prepared by "git init" or
 "git clone". A repository has one main checkout and zero or more
 linked checkouts.
 
-All checkouts share the same repository. Linked checkouts see the
-repository a bit different from the main checkout. When the checkout
-"new" reads the path $GIT_DIR/HEAD for example, the actual path
-returned could be $GIT_DIR/repos/new/HEAD. This ensures checkouts
-won't step on each other.
-
-Each linked checkout has a private space in $GIT_DIR/repos, usually
-named after the base name of the working directory with a number added
-to make it unique. The linked checkout's $GIT_DIR points to this
-private space while $GIT_COMMON_DIR points to the main checkout's
-$GIT_DIR. These settings are done by "git checkout --to".
-
-Because in this mode $GIT_DIR becomes a lightweight virtual file
-system where a path could be rewritten to some place else, accessing
-$GIT_DIR from scripts should use `git rev-parse --git-path` to resolve
-a path instead of using it directly unless the path is known to be
-private to the working directory.
+Each linked checkout has a private directory in $GIT_DIR/worktrees in
+the main checkout, usually named after the base name of the new
+working directory, optionally with a number added to make it
+unique. For example, the command `git checkout --to ../test-next next`
+running with $GIT_DIR=/path/main may create the directory
+`$GIT_DIR/worktrees/test-next` (or `$GIT_DIR/worktrees/test-next1` if
+`test-next` is already taken).
+
+Within a linked checkout, $GIT_DIR is set to point to this private
+directory (e.g. `/path/main/worktrees/test-next` in the example) and
+$GIT_COMMON_DIR is set to point back to the main checkout's $GIT_DIR
+(e.g. `/path/main`). Setting is done via a .git file located at the
+top directory of the linked checkout.
+
+Path resolution via `git rev-parse --git-path` would use either
+$GIT_DIR or $GIT_COMMON_DIR depending on the path. For example, the
+linked checkout's `$GIT_DIR/HEAD` resolve to
+`/path/main/worktrees/test-next/HEAD` (not `/path/main/HEAD` which is
+the main checkout's HEAD) while `$GIT_DIR/refs/heads/master` would use
+$GIT_COMMON_DIR and resolve to `/path/main/refs/heads/master`, which
+is shared across checkouts.
+
+See linkgit:gitrepository-layout[5] for more information. The rule of
+thumb is do not make any assumption about whether a path belongs to
+$GIT_DIR or $GIT_COMMON_DIR when you need to directly access something
+inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.
+
+When you are done, simply deleting the linked working directory would
+suffice. $GIT_DIR/worktrees can be cleaned up using `git prune
+--worktrees`, which is part of automated garbage collection.
+
+After you move a linked working directory to another file system, or
+on a file system that does not support hard link, execute any git
+command (e.g. `git status`) in the new working directory so that it
+could update its location in $GIT_DIR/worktrees and not be
+accidentally pruned.
+
+To stop `git prune --worktrees` from deleting a specific working
+directory (e.g. because it's on a portable device), you could add the
+file 'locked' to $GIT_DIR/worktrees. For example, if `.git` file of
+the new working directory points to `/path/main/worktrees/test-next`,
+the full path of the 'locked' file would be
+`/path/main/worktrees/test-next/locked`. See
+linkgit:gitrepository-layout[5] for details.
 
 EXAMPLES
 --------
diff --git a/Documentation/git-prune.txt b/Documentation/git-prune.txt
index 50e39ec..a0ea381 100644
--- a/Documentation/git-prune.txt
+++ b/Documentation/git-prune.txt
@@ -48,8 +48,8 @@ OPTIONS
 --expire <time>::
 	Only expire loose objects older than <time>.
 
---repos::
-	Prune directories in $GIT_DIR/repos.
+--worktrees::
+	Prune dead worktree information in $GIT_DIR/worktrees.
 
 <head>...::
 	In addition to objects
diff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt
index c9a7d1a..5ceff51 100644
--- a/Documentation/gitrepository-layout.txt
+++ b/Documentation/gitrepository-layout.txt
@@ -220,7 +220,7 @@ remotes::
 logs::
 	Records of changes made to refs are stored in this directory.
 	See linkgit:git-update-ref[1] for more information. This
-	directory is ignored $GIT_COMMON_DIR is set and
+	directory is ignored if $GIT_COMMON_DIR is set and
 	"$GIT_COMMON_DIR/logs" will be used instead.
 
 logs/refs/heads/`name`::
@@ -248,28 +248,28 @@ modules::
 	directory is ignored if $GIT_COMMON_DIR is set and
 	"$GIT_COMMON_DIR/modules" will be used instead.
 
-repos::
+worktrees::
 	Contains worktree specific information of linked
 	checkouts. Each subdirectory contains the worktree-related
 	part of a linked checkout. This directory is ignored if
-	$GIT_COMMON_DIR is set and "$GIT_COMMON_DIR/repos" will be
+	$GIT_COMMON_DIR is set and "$GIT_COMMON_DIR/worktrees" will be
 	used instead.
 
-repos/<id>/gitdir::
+worktrees/<id>/gitdir::
 	A text file containing the absolute path back to the .git file
 	that points to here. This is used to check if the linked
 	repository has been manually removed and there is no need to
 	keep this directory any more. mtime of this file should be
 	updated every time the linked repository is accessed.
 
-repos/<id>/locked::
+worktrees/<id>/locked::
 	If this file exists, the linked repository may be on a
 	portable device and not available. It does not mean that the
-	linked repository is gone and `repos/<id>` could be
+	linked repository is gone and `worktrees/<id>` could be
 	removed. The file's content contains a reason string on why
 	the repository is locked.
 
-repos/<id>/link::
+worktrees/<id>/link::
 	If this file exists, it is a hard link to the linked .git
 	file. It is used to detect if the linked repository is
 	manually removed.
diff --git a/builtin/checkout.c b/builtin/checkout.c
index b88c646..6106b81 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -874,7 +874,7 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
 			break;
 		}
 	strbuf_addstr(&sb_repo,
-		      git_path("repos/%.*s", (int)(path + len - name), name));
+		      git_path("worktrees/%.*s", (int)(path + len - name), name));
 	len = sb_repo.len;
 	if (safe_create_leading_directories_const(sb_repo.buf))
 		die_errno(_("could not create leading directories of '%s'"),
@@ -911,7 +911,7 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
 	strbuf_reset(&sb);
 	strbuf_addf(&sb, "%s/gitdir", sb_repo.buf);
 	write_file(sb.buf, 1, "%s\n", real_path(sb_git.buf));
-	write_file(sb_git.buf, 1, "gitdir: %s/repos/%s\n",
+	write_file(sb_git.buf, 1, "gitdir: %s/worktrees/%s\n",
 		   real_path(get_git_common_dir()), name);
 	/*
 	 * This is to keep resolve_ref() happy. We need a valid HEAD
@@ -950,7 +950,6 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
 	strbuf_release(&sb_repo);
 	strbuf_release(&sb_git);
 	return ret;
-
 }
 
 static int git_checkout_config(const char *var, const char *value, void *cb)
@@ -1016,7 +1015,7 @@ static void check_linked_checkout(struct branch_info *new, const char *id)
 	const char *start, *end;
 
 	if (id)
-		strbuf_addf(&path, "%s/repos/%s/HEAD", get_git_common_dir(), id);
+		strbuf_addf(&path, "%s/worktrees/%s/HEAD", get_git_common_dir(), id);
 	else
 		strbuf_addf(&path, "%s/HEAD", get_git_common_dir());
 
@@ -1032,7 +1031,7 @@ static void check_linked_checkout(struct branch_info *new, const char *id)
 		goto done;
 	if (id) {
 		strbuf_reset(&path);
-		strbuf_addf(&path, "%s/repos/%s/gitdir", get_git_common_dir(), id);
+		strbuf_addf(&path, "%s/worktrees/%s/gitdir", get_git_common_dir(), id);
 		if (strbuf_read_file(&gitdir, path.buf, 0) <= 0)
 			goto done;
 		strbuf_rtrim(&gitdir);
@@ -1051,7 +1050,7 @@ static void check_linked_checkouts(struct branch_info *new)
 	DIR *dir;
 	struct dirent *d;
 
-	strbuf_addf(&path, "%s/repos", get_git_common_dir());
+	strbuf_addf(&path, "%s/worktrees", get_git_common_dir());
 	if ((dir = opendir(path.buf)) == NULL) {
 		strbuf_release(&path);
 		return;
diff --git a/builtin/gc.c b/builtin/gc.c
index 0c65808..00239ca 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -33,13 +33,13 @@ static int gc_auto_threshold = 6700;
 static int gc_auto_pack_limit = 50;
 static int detach_auto = 1;
 static const char *prune_expire = "2.weeks.ago";
-static const char *prune_repos_expire = "3.months.ago";
+static const char *prune_worktrees_expire = "3.months.ago";
 
 static struct argv_array pack_refs_cmd = ARGV_ARRAY_INIT;
 static struct argv_array reflog = ARGV_ARRAY_INIT;
 static struct argv_array repack = ARGV_ARRAY_INIT;
 static struct argv_array prune = ARGV_ARRAY_INIT;
-static struct argv_array prune_repos = ARGV_ARRAY_INIT;
+static struct argv_array prune_worktrees = ARGV_ARRAY_INIT;
 static struct argv_array rerere = ARGV_ARRAY_INIT;
 
 static char *pidfile;
@@ -99,8 +99,8 @@ static int gc_config(const char *var, const char *value, void *cb)
 	}
 	if (!strcmp(var, "gc.pruneexpire"))
 		return git_config_date_string(&prune_expire, var, value);
-	if (!strcmp(var, "gc.prunereposexpire"))
-		return git_config_date_string(&prune_repos_expire, var, value);
+	if (!strcmp(var, "gc.pruneworktreesexpire"))
+		return git_config_date_string(&prune_worktrees_expire, var, value);
 	return git_default_config(var, value, cb);
 }
 
@@ -308,7 +308,7 @@ int cmd_gc(int argc, const char **argv, const char *prefix)
 	argv_array_pushl(&reflog, "reflog", "expire", "--all", NULL);
 	argv_array_pushl(&repack, "repack", "-d", "-l", NULL);
 	argv_array_pushl(&prune, "prune", "--expire", NULL);
-	argv_array_pushl(&prune_repos, "prune", "--repos", "--expire", NULL);
+	argv_array_pushl(&prune_worktrees, "prune", "--worktrees", "--expire", NULL);
 	argv_array_pushl(&rerere, "rerere", "gc", NULL);
 
 	git_config(gc_config, NULL);
@@ -378,10 +378,10 @@ int cmd_gc(int argc, const char **argv, const char *prefix)
 			return error(FAILED_RUN, prune.argv[0]);
 	}
 
-	if (prune_repos_expire) {
-		argv_array_push(&prune_repos, prune_repos_expire);
-		if (run_command_v_opt(prune_repos.argv, RUN_GIT_CMD))
-			return error(FAILED_RUN, prune_repos.argv[0]);
+	if (prune_worktrees_expire) {
+		argv_array_push(&prune_worktrees, prune_worktrees_expire);
+		if (run_command_v_opt(prune_worktrees.argv, RUN_GIT_CMD))
+			return error(FAILED_RUN, prune_worktrees.argv[0]);
 	}
 
 	if (run_command_v_opt(rerere.argv, RUN_GIT_CMD))
diff --git a/builtin/prune.c b/builtin/prune.c
index e72c391..cf56110 100644
--- a/builtin/prune.c
+++ b/builtin/prune.c
@@ -112,25 +112,25 @@ static void prune_object_dir(const char *path)
 	}
 }
 
-static int prune_repo_dir(const char *id, struct strbuf *reason)
+static int prune_worktree(const char *id, struct strbuf *reason)
 {
 	struct stat st;
 	char *path;
 	int fd, len;
 
-	if (!is_directory(git_path("repos/%s", id))) {
-		strbuf_addf(reason, _("Removing repos/%s: not a valid directory"), id);
+	if (!is_directory(git_path("worktrees/%s", id))) {
+		strbuf_addf(reason, _("Removing worktrees/%s: not a valid directory"), id);
 		return 1;
 	}
-	if (file_exists(git_path("repos/%s/locked", id)))
+	if (file_exists(git_path("worktrees/%s/locked", id)))
 		return 0;
-	if (stat(git_path("repos/%s/gitdir", id), &st)) {
-		strbuf_addf(reason, _("Removing repos/%s: gitdir file does not exist"), id);
+	if (stat(git_path("worktrees/%s/gitdir", id), &st)) {
+		strbuf_addf(reason, _("Removing worktrees/%s: gitdir file does not exist"), id);
 		return 1;
 	}
-	fd = open(git_path("repos/%s/gitdir", id), O_RDONLY);
+	fd = open(git_path("worktrees/%s/gitdir", id), O_RDONLY);
 	if (fd < 0) {
-		strbuf_addf(reason, _("Removing repos/%s: unable to read gitdir file (%s)"),
+		strbuf_addf(reason, _("Removing worktrees/%s: unable to read gitdir file (%s)"),
 			    id, strerror(errno));
 		return 1;
 	}
@@ -141,7 +141,7 @@ static int prune_repo_dir(const char *id, struct strbuf *reason)
 	while (len && (path[len - 1] == '\n' || path[len - 1] == '\r'))
 		len--;
 	if (!len) {
-		strbuf_addf(reason, _("Removing repos/%s: invalid gitdir file"), id);
+		strbuf_addf(reason, _("Removing worktrees/%s: invalid gitdir file"), id);
 		free(path);
 		return 1;
 	}
@@ -153,21 +153,21 @@ static int prune_repo_dir(const char *id, struct strbuf *reason)
 		 * the repo is moved manually and has not been
 		 * accessed since?
 		 */
-		if (!stat(git_path("repos/%s/link", id), &st_link) &&
+		if (!stat(git_path("worktrees/%s/link", id), &st_link) &&
 		    st_link.st_nlink > 1)
 			return 0;
-		strbuf_addf(reason, _("Removing repos/%s: gitdir file points to non-existent location"), id);
+		strbuf_addf(reason, _("Removing worktrees/%s: gitdir file points to non-existent location"), id);
 		return 1;
 	}
 	free(path);
 	return st.st_mtime <= expire;
 }
 
-static void prune_repos_dir(void)
+static void prune_worktrees(void)
 {
 	struct strbuf reason = STRBUF_INIT;
 	struct strbuf path = STRBUF_INIT;
-	DIR *dir = opendir(git_path("repos"));
+	DIR *dir = opendir(git_path("worktrees"));
 	struct dirent *d;
 	int ret;
 	if (!dir)
@@ -176,14 +176,14 @@ static void prune_repos_dir(void)
 		if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, ".."))
 			continue;
 		strbuf_reset(&reason);
-		if (!prune_repo_dir(d->d_name, &reason))
+		if (!prune_worktree(d->d_name, &reason))
 			continue;
 		if (show_only || verbose)
 			printf("%s\n", reason.buf);
 		if (show_only)
 			continue;
 		strbuf_reset(&path);
-		strbuf_addstr(&path, git_path("repos/%s", d->d_name));
+		strbuf_addstr(&path, git_path("worktrees/%s", d->d_name));
 		ret = remove_dir_recursively(&path, 0);
 		if (ret < 0 && errno == ENOTDIR)
 			ret = unlink(path.buf);
@@ -192,7 +192,7 @@ static void prune_repos_dir(void)
 	}
 	closedir(dir);
 	if (!show_only)
-		rmdir(git_path("repos"));
+		rmdir(git_path("worktrees"));
 	strbuf_release(&reason);
 	strbuf_release(&path);
 }
@@ -223,12 +223,12 @@ int cmd_prune(int argc, const char **argv, const char *prefix)
 {
 	struct rev_info revs;
 	struct progress *progress = NULL;
-	int prune_repos = 0;
+	int do_prune_worktrees = 0;
 	const struct option options[] = {
 		OPT__DRY_RUN(&show_only, N_("do not remove, show only")),
 		OPT__VERBOSE(&verbose, N_("report pruned objects")),
 		OPT_BOOL(0, "progress", &show_progress, N_("show progress")),
-		OPT_BOOL(0, "repos", &prune_repos, N_("prune .git/repos/")),
+		OPT_BOOL(0, "worktrees", &do_prune_worktrees, N_("prune .git/worktrees")),
 		OPT_EXPIRY_DATE(0, "expire", &expire,
 				N_("expire objects older than <time>")),
 		OPT_END()
@@ -242,10 +242,10 @@ int cmd_prune(int argc, const char **argv, const char *prefix)
 
 	argc = parse_options(argc, argv, prefix, options, prune_usage, 0);
 
-	if (prune_repos) {
+	if (do_prune_worktrees) {
 		if (argc)
-			die(_("--repos does not take extra arguments"));
-		prune_repos_dir();
+			die(_("--worktrees does not take extra arguments"));
+		prune_worktrees();
 		return 0;
 	}
 
diff --git a/path.c b/path.c
index b1dd2bc..1f1881a 100644
--- a/path.c
+++ b/path.c
@@ -93,7 +93,7 @@ static void replace_dir(struct strbuf *buf, int len, const char *newdir)
 
 static const char *common_list[] = {
 	"/branches", "/hooks", "/info", "!/logs", "/lost-found", "/modules",
-	"/objects", "/refs", "/remotes", "/repos", "/rr-cache", "/svn",
+	"/objects", "/refs", "/remotes", "/worktrees", "/rr-cache", "/svn",
 	"config", "!gc.pid", "packed-refs", "shallow",
 	NULL
 };
diff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh
index aa24ea8..27384a1 100755
--- a/t/t2025-checkout-to.sh
+++ b/t/t2025-checkout-to.sh
@@ -20,7 +20,7 @@ test_expect_success 'checkout --to an existing worktree' '
 test_expect_success 'checkout --to refuses to checkout locked branch' '
 	test_must_fail git checkout --to zere master &&
 	! test -d zere &&
-	! test -d .git/repos/zere
+	! test -d .git/worktrees/zere
 '
 
 test_expect_success 'checkout --to a new worktree' '
diff --git a/t/t2026-prune-linked-checkouts.sh b/t/t2026-prune-linked-checkouts.sh
index 79d84cb..3622800 100755
--- a/t/t2026-prune-linked-checkouts.sh
+++ b/t/t2026-prune-linked-checkouts.sh
@@ -1,84 +1,84 @@
 #!/bin/sh
 
-test_description='prune $GIT_DIR/repos'
+test_description='prune $GIT_DIR/worktrees'
 
 . ./test-lib.sh
 
-test_expect_success 'prune --repos on normal repo' '
-	git prune --repos &&
-	test_must_fail git prune --repos abc
+test_expect_success 'prune --worktrees on normal repo' '
+	git prune --worktrees &&
+	test_must_fail git prune --worktrees abc
 '
 
-test_expect_success 'prune files inside $GIT_DIR/repos' '
-	mkdir .git/repos &&
-	: >.git/repos/abc &&
-	git prune --repos --verbose >actual &&
+test_expect_success 'prune files inside $GIT_DIR/worktrees' '
+	mkdir .git/worktrees &&
+	: >.git/worktrees/abc &&
+	git prune --worktrees --verbose >actual &&
 	cat >expect <<EOF &&
-Removing repos/abc: not a valid directory
+Removing worktrees/abc: not a valid directory
 EOF
 	test_i18ncmp expect actual &&
-	! test -f .git/repos/abc &&
-	! test -d .git/repos
+	! test -f .git/worktrees/abc &&
+	! test -d .git/worktrees
 '
 
 test_expect_success 'prune directories without gitdir' '
-	mkdir -p .git/repos/def/abc &&
-	: >.git/repos/def/def &&
+	mkdir -p .git/worktrees/def/abc &&
+	: >.git/worktrees/def/def &&
 	cat >expect <<EOF &&
-Removing repos/def: gitdir file does not exist
+Removing worktrees/def: gitdir file does not exist
 EOF
-	git prune --repos --verbose >actual &&
+	git prune --worktrees --verbose >actual &&
 	test_i18ncmp expect actual &&
-	! test -d .git/repos/def &&
-	! test -d .git/repos
+	! test -d .git/worktrees/def &&
+	! test -d .git/worktrees
 '
 
 test_expect_success POSIXPERM 'prune directories with unreadable gitdir' '
-	mkdir -p .git/repos/def/abc &&
-	: >.git/repos/def/def &&
-	: >.git/repos/def/gitdir &&
-	chmod u-r .git/repos/def/gitdir &&
-	git prune --repos --verbose >actual &&
-	test_i18ngrep "Removing repos/def: unable to read gitdir file" actual &&
-	! test -d .git/repos/def &&
-	! test -d .git/repos
+	mkdir -p .git/worktrees/def/abc &&
+	: >.git/worktrees/def/def &&
+	: >.git/worktrees/def/gitdir &&
+	chmod u-r .git/worktrees/def/gitdir &&
+	git prune --worktrees --verbose >actual &&
+	test_i18ngrep "Removing worktrees/def: unable to read gitdir file" actual &&
+	! test -d .git/worktrees/def &&
+	! test -d .git/worktrees
 '
 
 test_expect_success 'prune directories with invalid gitdir' '
-	mkdir -p .git/repos/def/abc &&
-	: >.git/repos/def/def &&
-	: >.git/repos/def/gitdir &&
-	git prune --repos --verbose >actual &&
-	test_i18ngrep "Removing repos/def: invalid gitdir file" actual &&
-	! test -d .git/repos/def &&
-	! test -d .git/repos
+	mkdir -p .git/worktrees/def/abc &&
+	: >.git/worktrees/def/def &&
+	: >.git/worktrees/def/gitdir &&
+	git prune --worktrees --verbose >actual &&
+	test_i18ngrep "Removing worktrees/def: invalid gitdir file" actual &&
+	! test -d .git/worktrees/def &&
+	! test -d .git/worktrees
 '
 
 test_expect_success 'prune directories with gitdir pointing to nowhere' '
-	mkdir -p .git/repos/def/abc &&
-	: >.git/repos/def/def &&
-	echo "$TRASH_DIRECTORY"/nowhere >.git/repos/def/gitdir &&
-	git prune --repos --verbose >actual &&
-	test_i18ngrep "Removing repos/def: gitdir file points to non-existent location" actual &&
-	! test -d .git/repos/def &&
-	! test -d .git/repos
+	mkdir -p .git/worktrees/def/abc &&
+	: >.git/worktrees/def/def &&
+	echo "$TRASH_DIRECTORY"/nowhere >.git/worktrees/def/gitdir &&
+	git prune --worktrees --verbose >actual &&
+	test_i18ngrep "Removing worktrees/def: gitdir file points to non-existent location" actual &&
+	! test -d .git/worktrees/def &&
+	! test -d .git/worktrees
 '
 
 test_expect_success 'not prune locked checkout' '
-	test_when_finished rm -r .git/repos
-	mkdir -p .git/repos/ghi &&
-	: >.git/repos/ghi/locked &&
-	git prune --repos &&
-	test -d .git/repos/ghi
+	test_when_finished rm -r .git/worktrees
+	mkdir -p .git/worktrees/ghi &&
+	: >.git/worktrees/ghi/locked &&
+	git prune --worktrees &&
+	test -d .git/worktrees/ghi
 '
 
 test_expect_success 'not prune recent checkouts' '
-	test_when_finished rm -r .git/repos
+	test_when_finished rm -r .git/worktrees
 	mkdir zz &&
-	mkdir -p .git/repos/jlm &&
-	echo "$TRASH_DIRECTORY"/zz >.git/repos/jlm/gitdir &&
-	git prune --repos --verbose --expire=2.days.ago &&
-	test -d .git/repos/jlm
+	mkdir -p .git/worktrees/jlm &&
+	echo "$TRASH_DIRECTORY"/zz >.git/worktrees/jlm/gitdir &&
+	git prune --worktrees --verbose --expire=2.days.ago &&
+	test -d .git/worktrees/jlm
 '
 
 test_done

-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 01/32] path.c: make get_pathname() return strbuf instead of static buffer
  2014-09-10 22:41 ` [PATCH v2 00/32] nd/multiple-work-trees Nguyễn Thái Ngọc Duy
@ 2014-09-10 22:41   ` Nguyễn Thái Ngọc Duy
  2014-09-10 22:41   ` [PATCH v2 02/32] path.c: make get_pathname() call sites return const char * Nguyễn Thái Ngọc Duy
                     ` (31 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-10 22:41 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

We've been avoiding PATH_MAX whenever possible. This patch makes
get_pathname() return a strbuf and updates the callers to take
advantage of this. The code is simplified as we no longer need to
worry about buffer overflow.

vsnpath() behavior is changed slightly: previously it always clears
the buffer before writing, now it just appends. Fortunately this is a
static function and all of its callers prepare the buffer properly:
git_path() gets the buffer from get_pathname() which resets the
buffer, the remaining call sites start with STRBUF_INIT'd buffer.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 path.c | 120 ++++++++++++++++++++++++++++-------------------------------------
 1 file changed, 51 insertions(+), 69 deletions(-)

diff --git a/path.c b/path.c
index bc804a3..42ef3af 100644
--- a/path.c
+++ b/path.c
@@ -16,11 +16,15 @@ static int get_st_mode_bits(const char *path, int *mode)
 
 static char bad_path[] = "/bad-path/";
 
-static char *get_pathname(void)
+static struct strbuf *get_pathname(void)
 {
-	static char pathname_array[4][PATH_MAX];
+	static struct strbuf pathname_array[4] = {
+		STRBUF_INIT, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT
+	};
 	static int index;
-	return pathname_array[3 & ++index];
+	struct strbuf *sb = &pathname_array[3 & ++index];
+	strbuf_reset(sb);
+	return sb;
 }
 
 static char *cleanup_path(char *path)
@@ -34,6 +38,13 @@ static char *cleanup_path(char *path)
 	return path;
 }
 
+static void strbuf_cleanup_path(struct strbuf *sb)
+{
+	char *path = cleanup_path(sb->buf);
+	if (path > sb->buf)
+		strbuf_remove(sb, 0, path - sb->buf);
+}
+
 char *mksnpath(char *buf, size_t n, const char *fmt, ...)
 {
 	va_list args;
@@ -49,85 +60,70 @@ char *mksnpath(char *buf, size_t n, const char *fmt, ...)
 	return cleanup_path(buf);
 }
 
-static char *vsnpath(char *buf, size_t n, const char *fmt, va_list args)
+static void vsnpath(struct strbuf *buf, const char *fmt, va_list args)
 {
 	const char *git_dir = get_git_dir();
-	size_t len;
-
-	len = strlen(git_dir);
-	if (n < len + 1)
-		goto bad;
-	memcpy(buf, git_dir, len);
-	if (len && !is_dir_sep(git_dir[len-1]))
-		buf[len++] = '/';
-	len += vsnprintf(buf + len, n - len, fmt, args);
-	if (len >= n)
-		goto bad;
-	return cleanup_path(buf);
-bad:
-	strlcpy(buf, bad_path, n);
-	return buf;
+	strbuf_addstr(buf, git_dir);
+	if (buf->len && !is_dir_sep(buf->buf[buf->len - 1]))
+		strbuf_addch(buf, '/');
+	strbuf_vaddf(buf, fmt, args);
+	strbuf_cleanup_path(buf);
 }
 
 char *git_snpath(char *buf, size_t n, const char *fmt, ...)
 {
-	char *ret;
+	struct strbuf sb = STRBUF_INIT;
 	va_list args;
 	va_start(args, fmt);
-	ret = vsnpath(buf, n, fmt, args);
+	vsnpath(&sb, fmt, args);
 	va_end(args);
-	return ret;
+	if (sb.len >= n)
+		strlcpy(buf, bad_path, n);
+	else
+		memcpy(buf, sb.buf, sb.len + 1);
+	strbuf_release(&sb);
+	return buf;
 }
 
 char *git_pathdup(const char *fmt, ...)
 {
-	char path[PATH_MAX], *ret;
+	struct strbuf path = STRBUF_INIT;
 	va_list args;
 	va_start(args, fmt);
-	ret = vsnpath(path, sizeof(path), fmt, args);
+	vsnpath(&path, fmt, args);
 	va_end(args);
-	return xstrdup(ret);
+	return strbuf_detach(&path, NULL);
 }
 
 char *mkpathdup(const char *fmt, ...)
 {
-	char *path;
 	struct strbuf sb = STRBUF_INIT;
 	va_list args;
-
 	va_start(args, fmt);
 	strbuf_vaddf(&sb, fmt, args);
 	va_end(args);
-	path = xstrdup(cleanup_path(sb.buf));
-
-	strbuf_release(&sb);
-	return path;
+	strbuf_cleanup_path(&sb);
+	return strbuf_detach(&sb, NULL);
 }
 
 char *mkpath(const char *fmt, ...)
 {
 	va_list args;
-	unsigned len;
-	char *pathname = get_pathname();
-
+	struct strbuf *pathname = get_pathname();
 	va_start(args, fmt);
-	len = vsnprintf(pathname, PATH_MAX, fmt, args);
+	strbuf_vaddf(pathname, fmt, args);
 	va_end(args);
-	if (len >= PATH_MAX)
-		return bad_path;
-	return cleanup_path(pathname);
+	return cleanup_path(pathname->buf);
 }
 
 char *git_path(const char *fmt, ...)
 {
-	char *pathname = get_pathname();
+	struct strbuf *pathname = get_pathname();
 	va_list args;
-	char *ret;
-
 	va_start(args, fmt);
-	ret = vsnpath(pathname, PATH_MAX, fmt, args);
+	vsnpath(pathname, fmt, args);
 	va_end(args);
-	return ret;
+	return pathname->buf;
 }
 
 void home_config_paths(char **global, char **xdg, char *file)
@@ -158,41 +154,27 @@ void home_config_paths(char **global, char **xdg, char *file)
 
 char *git_path_submodule(const char *path, const char *fmt, ...)
 {
-	char *pathname = get_pathname();
-	struct strbuf buf = STRBUF_INIT;
+	struct strbuf *buf = get_pathname();
 	const char *git_dir;
 	va_list args;
-	unsigned len;
-
-	len = strlen(path);
-	if (len > PATH_MAX-100)
-		return bad_path;
 
-	strbuf_addstr(&buf, path);
-	if (len && path[len-1] != '/')
-		strbuf_addch(&buf, '/');
-	strbuf_addstr(&buf, ".git");
+	strbuf_addstr(buf, path);
+	if (buf->len && buf->buf[buf->len - 1] != '/')
+		strbuf_addch(buf, '/');
+	strbuf_addstr(buf, ".git");
 
-	git_dir = read_gitfile(buf.buf);
+	git_dir = read_gitfile(buf->buf);
 	if (git_dir) {
-		strbuf_reset(&buf);
-		strbuf_addstr(&buf, git_dir);
+		strbuf_reset(buf);
+		strbuf_addstr(buf, git_dir);
 	}
-	strbuf_addch(&buf, '/');
-
-	if (buf.len >= PATH_MAX)
-		return bad_path;
-	memcpy(pathname, buf.buf, buf.len + 1);
-
-	strbuf_release(&buf);
-	len = strlen(pathname);
+	strbuf_addch(buf, '/');
 
 	va_start(args, fmt);
-	len += vsnprintf(pathname + len, PATH_MAX - len, fmt, args);
+	strbuf_vaddf(buf, fmt, args);
 	va_end(args);
-	if (len >= PATH_MAX)
-		return bad_path;
-	return cleanup_path(pathname);
+	strbuf_cleanup_path(buf);
+	return buf->buf;
 }
 
 int validate_headref(const char *path)
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 02/32] path.c: make get_pathname() call sites return const char *
  2014-09-10 22:41 ` [PATCH v2 00/32] nd/multiple-work-trees Nguyễn Thái Ngọc Duy
  2014-09-10 22:41   ` [PATCH v2 01/32] path.c: make get_pathname() return strbuf instead of static buffer Nguyễn Thái Ngọc Duy
@ 2014-09-10 22:41   ` Nguyễn Thái Ngọc Duy
  2014-09-10 22:41   ` [PATCH v2 03/32] git_snpath(): retire and replace with strbuf_git_path() Nguyễn Thái Ngọc Duy
                     ` (30 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-10 22:41 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Before the previous commit, get_pathname returns an array of PATH_MAX
length. Even if git_path() and similar functions does not use the
whole array, git_path() caller can, in theory.

After the commit, get_pathname() may return a buffer that has just
enough room for the returned string and git_path() caller should never
write beyond that.

Make git_path(), mkpath() and git_path_submodule() return a const
buffer to make sure callers do not write in it at all.

This could have been part of the previous commit, but the "const"
conversion is too much distraction from the core changes in path.c.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 builtin/checkout.c     | 2 +-
 builtin/clone.c        | 9 +++++----
 builtin/fetch.c        | 5 +++--
 builtin/fsck.c         | 4 ++--
 builtin/receive-pack.c | 2 +-
 builtin/remote.c       | 2 +-
 builtin/repack.c       | 8 +++++---
 cache.h                | 6 +++---
 fast-import.c          | 2 +-
 notes-merge.c          | 6 +++---
 path.c                 | 6 +++---
 refs.c                 | 8 ++++----
 run-command.c          | 4 ++--
 run-command.h          | 2 +-
 sha1_file.c            | 2 +-
 15 files changed, 36 insertions(+), 32 deletions(-)

diff --git a/builtin/checkout.c b/builtin/checkout.c
index 463cfee..3abcef1 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -585,7 +585,7 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
 			if (opts->new_branch_log && !log_all_ref_updates) {
 				int temp;
 				char log_file[PATH_MAX];
-				char *ref_name = mkpath("refs/heads/%s", opts->new_orphan_branch);
+				const char *ref_name = mkpath("refs/heads/%s", opts->new_orphan_branch);
 
 				temp = log_all_ref_updates;
 				log_all_ref_updates = 1;
diff --git a/builtin/clone.c b/builtin/clone.c
index e15ca33..91fac9d 100644
--- a/builtin/clone.c
+++ b/builtin/clone.c
@@ -289,16 +289,17 @@ static void copy_alternates(struct strbuf *src, struct strbuf *dst,
 	struct strbuf line = STRBUF_INIT;
 
 	while (strbuf_getline(&line, in, '\n') != EOF) {
-		char *abs_path, abs_buf[PATH_MAX];
+		char *abs_path;
 		if (!line.len || line.buf[0] == '#')
 			continue;
 		if (is_absolute_path(line.buf)) {
 			add_to_alternates_file(line.buf);
 			continue;
 		}
-		abs_path = mkpath("%s/objects/%s", src_repo, line.buf);
-		normalize_path_copy(abs_buf, abs_path);
-		add_to_alternates_file(abs_buf);
+		abs_path = mkpathdup("%s/objects/%s", src_repo, line.buf);
+		normalize_path_copy(abs_path, abs_path);
+		add_to_alternates_file(abs_path);
+		free(abs_path);
 	}
 	strbuf_release(&line);
 	fclose(in);
diff --git a/builtin/fetch.c b/builtin/fetch.c
index e8d0cca..9522b1b 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -573,7 +573,8 @@ static int store_updated_refs(const char *raw_url, const char *remote_name,
 	struct strbuf note = STRBUF_INIT;
 	const char *what, *kind;
 	struct ref *rm;
-	char *url, *filename = dry_run ? "/dev/null" : git_path("FETCH_HEAD");
+	char *url;
+	const char *filename = dry_run ? "/dev/null" : git_path("FETCH_HEAD");
 	int want_status;
 
 	fp = fopen(filename, "a");
@@ -807,7 +808,7 @@ static void check_not_current_branch(struct ref *ref_map)
 
 static int truncate_fetch_head(void)
 {
-	char *filename = git_path("FETCH_HEAD");
+	const char *filename = git_path("FETCH_HEAD");
 	FILE *fp = fopen(filename, "w");
 
 	if (!fp)
diff --git a/builtin/fsck.c b/builtin/fsck.c
index 8aadca1..d414962 100644
--- a/builtin/fsck.c
+++ b/builtin/fsck.c
@@ -225,12 +225,12 @@ static void check_unreachable_object(struct object *obj)
 			printf("dangling %s %s\n", typename(obj->type),
 			       sha1_to_hex(obj->sha1));
 		if (write_lost_and_found) {
-			char *filename = git_path("lost-found/%s/%s",
+			const char *filename = git_path("lost-found/%s/%s",
 				obj->type == OBJ_COMMIT ? "commit" : "other",
 				sha1_to_hex(obj->sha1));
 			FILE *f;
 
-			if (safe_create_leading_directories(filename)) {
+			if (safe_create_leading_directories_const(filename)) {
 				error("Could not create lost-found");
 				return;
 			}
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 18458e8..ed11e7e 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -599,7 +599,7 @@ static void run_update_post_hook(struct command *commands)
 	int argc;
 	const char **argv;
 	struct child_process proc;
-	char *hook;
+	const char *hook;
 
 	hook = find_hook("post-update");
 	for (argc = 0, cmd = commands; cmd; cmd = cmd->next) {
diff --git a/builtin/remote.c b/builtin/remote.c
index a8efe3d..3d99c9c 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -580,7 +580,7 @@ static int migrate_file(struct remote *remote)
 {
 	struct strbuf buf = STRBUF_INIT;
 	int i;
-	char *path = NULL;
+	const char *path = NULL;
 
 	strbuf_addf(&buf, "remote.%s.url", remote->name);
 	for (i = 0; i < remote->url_nr; i++)
diff --git a/builtin/repack.c b/builtin/repack.c
index ff2216a..a64a4a9 100644
--- a/builtin/repack.c
+++ b/builtin/repack.c
@@ -285,7 +285,8 @@ int cmd_repack(int argc, const char **argv, const char *prefix)
 	failed = 0;
 	for_each_string_list_item(item, &names) {
 		for (ext = 0; ext < ARRAY_SIZE(exts); ext++) {
-			char *fname, *fname_old;
+			const char *fname_old;
+			char *fname;
 			fname = mkpathdup("%s/pack-%s%s", packdir,
 						item->string, exts[ext].name);
 			if (!file_exists(fname)) {
@@ -313,7 +314,8 @@ int cmd_repack(int argc, const char **argv, const char *prefix)
 	if (failed) {
 		struct string_list rollback_failure = STRING_LIST_INIT_DUP;
 		for_each_string_list_item(item, &rollback) {
-			char *fname, *fname_old;
+			const char *fname_old;
+			char *fname;
 			fname = mkpathdup("%s/%s", packdir, item->string);
 			fname_old = mkpath("%s/old-%s", packdir, item->string);
 			if (rename(fname_old, fname))
@@ -366,7 +368,7 @@ int cmd_repack(int argc, const char **argv, const char *prefix)
 	/* Remove the "old-" files */
 	for_each_string_list_item(item, &names) {
 		for (ext = 0; ext < ARRAY_SIZE(exts); ext++) {
-			char *fname;
+			const char *fname;
 			fname = mkpath("%s/old-%s%s",
 					packdir,
 					item->string,
diff --git a/cache.h b/cache.h
index 44aa439..707408b 100644
--- a/cache.h
+++ b/cache.h
@@ -682,9 +682,9 @@ extern char *mkpathdup(const char *fmt, ...)
 	__attribute__((format (printf, 1, 2)));
 
 /* Return a statically allocated filename matching the sha1 signature */
-extern char *mkpath(const char *fmt, ...) __attribute__((format (printf, 1, 2)));
-extern char *git_path(const char *fmt, ...) __attribute__((format (printf, 1, 2)));
-extern char *git_path_submodule(const char *path, const char *fmt, ...)
+extern const char *mkpath(const char *fmt, ...) __attribute__((format (printf, 1, 2)));
+extern const char *git_path(const char *fmt, ...) __attribute__((format (printf, 1, 2)));
+extern const char *git_path_submodule(const char *path, const char *fmt, ...)
 	__attribute__((format (printf, 2, 3)));
 
 /*
diff --git a/fast-import.c b/fast-import.c
index fa635f7..d9c068b 100644
--- a/fast-import.c
+++ b/fast-import.c
@@ -404,7 +404,7 @@ static void dump_marks_helper(FILE *, uintmax_t, struct mark_set *);
 
 static void write_crash_report(const char *err)
 {
-	char *loc = git_path("fast_import_crash_%"PRIuMAX, (uintmax_t) getpid());
+	const char *loc = git_path("fast_import_crash_%"PRIuMAX, (uintmax_t) getpid());
 	FILE *rpt = fopen(loc, "w");
 	struct branch *b;
 	unsigned long lu;
diff --git a/notes-merge.c b/notes-merge.c
index fd5fae2..a9e6b15 100644
--- a/notes-merge.c
+++ b/notes-merge.c
@@ -280,7 +280,7 @@ static void check_notes_merge_worktree(struct notes_merge_options *o)
 				    "(%s exists).", git_path("NOTES_MERGE_*"));
 		}
 
-		if (safe_create_leading_directories(git_path(
+		if (safe_create_leading_directories_const(git_path(
 				NOTES_MERGE_WORKTREE "/.test")))
 			die_errno("unable to create directory %s",
 				  git_path(NOTES_MERGE_WORKTREE));
@@ -295,8 +295,8 @@ static void write_buf_to_worktree(const unsigned char *obj,
 				  const char *buf, unsigned long size)
 {
 	int fd;
-	char *path = git_path(NOTES_MERGE_WORKTREE "/%s", sha1_to_hex(obj));
-	if (safe_create_leading_directories(path))
+	const char *path = git_path(NOTES_MERGE_WORKTREE "/%s", sha1_to_hex(obj));
+	if (safe_create_leading_directories_const(path))
 		die_errno("unable to create directory for '%s'", path);
 	if (file_exists(path))
 		die("found existing file at '%s'", path);
diff --git a/path.c b/path.c
index 42ef3af..a3f8826 100644
--- a/path.c
+++ b/path.c
@@ -106,7 +106,7 @@ char *mkpathdup(const char *fmt, ...)
 	return strbuf_detach(&sb, NULL);
 }
 
-char *mkpath(const char *fmt, ...)
+const char *mkpath(const char *fmt, ...)
 {
 	va_list args;
 	struct strbuf *pathname = get_pathname();
@@ -116,7 +116,7 @@ char *mkpath(const char *fmt, ...)
 	return cleanup_path(pathname->buf);
 }
 
-char *git_path(const char *fmt, ...)
+const char *git_path(const char *fmt, ...)
 {
 	struct strbuf *pathname = get_pathname();
 	va_list args;
@@ -152,7 +152,7 @@ void home_config_paths(char **global, char **xdg, char *file)
 	free(to_free);
 }
 
-char *git_path_submodule(const char *path, const char *fmt, ...)
+const char *git_path_submodule(const char *path, const char *fmt, ...)
 {
 	struct strbuf *buf = get_pathname();
 	const char *git_dir;
diff --git a/refs.c b/refs.c
index 82e4842..dbd131e 100644
--- a/refs.c
+++ b/refs.c
@@ -1442,7 +1442,7 @@ static int resolve_gitlink_ref_recursive(struct ref_cache *refs,
 {
 	int fd, len;
 	char buffer[128], *p;
-	char *path;
+	const char *path;
 
 	if (recursion > MAXDEPTH || strlen(refname) > MAXREFLEN)
 		return -1;
@@ -2255,7 +2255,7 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname,
 					    const unsigned char *old_sha1,
 					    int flags, int *type_p)
 {
-	char *ref_file;
+	const char *ref_file;
 	const char *orig_refname = refname;
 	struct ref_lock *lock;
 	int last_errno = 0;
@@ -2318,7 +2318,7 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname,
 		lock->force_write = 1;
 
  retry:
-	switch (safe_create_leading_directories(ref_file)) {
+	switch (safe_create_leading_directories_const(ref_file)) {
 	case SCLD_OK:
 		break; /* success */
 	case SCLD_VANISHED:
@@ -2756,7 +2756,7 @@ static int rename_tmp_log(const char *newrefname)
 	int attempts_remaining = 4;
 
  retry:
-	switch (safe_create_leading_directories(git_path("logs/%s", newrefname))) {
+	switch (safe_create_leading_directories_const(git_path("logs/%s", newrefname))) {
 	case SCLD_OK:
 		break; /* success */
 	case SCLD_VANISHED:
diff --git a/run-command.c b/run-command.c
index be07d4a..614b8ac 100644
--- a/run-command.c
+++ b/run-command.c
@@ -758,9 +758,9 @@ int finish_async(struct async *async)
 #endif
 }
 
-char *find_hook(const char *name)
+const char *find_hook(const char *name)
 {
-	char *path = git_path("hooks/%s", name);
+	const char *path = git_path("hooks/%s", name);
 	if (access(path, X_OK) < 0)
 		path = NULL;
 
diff --git a/run-command.h b/run-command.h
index ea73de3..890cc95 100644
--- a/run-command.h
+++ b/run-command.h
@@ -48,7 +48,7 @@ int start_command(struct child_process *);
 int finish_command(struct child_process *);
 int run_command(struct child_process *);
 
-extern char *find_hook(const char *name);
+extern const char *find_hook(const char *name);
 LAST_ARG_MUST_BE_NULL
 extern int run_hook_le(const char *const *env, const char *name, ...);
 extern int run_hook_ve(const char *const *env, const char *name, va_list args);
diff --git a/sha1_file.c b/sha1_file.c
index a38854c..9700108 100644
--- a/sha1_file.c
+++ b/sha1_file.c
@@ -402,7 +402,7 @@ void add_to_alternates_file(const char *reference)
 {
 	struct lock_file *lock = xcalloc(1, sizeof(struct lock_file));
 	int fd = hold_lock_file_for_append(lock, git_path("objects/info/alternates"), LOCK_DIE_ON_ERROR);
-	char *alt = mkpath("%s\n", reference);
+	const char *alt = mkpath("%s\n", reference);
 	write_or_die(fd, alt, strlen(alt));
 	if (commit_lock_file(lock))
 		die("could not close alternates file");
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 03/32] git_snpath(): retire and replace with strbuf_git_path()
  2014-09-10 22:41 ` [PATCH v2 00/32] nd/multiple-work-trees Nguyễn Thái Ngọc Duy
  2014-09-10 22:41   ` [PATCH v2 01/32] path.c: make get_pathname() return strbuf instead of static buffer Nguyễn Thái Ngọc Duy
  2014-09-10 22:41   ` [PATCH v2 02/32] path.c: make get_pathname() call sites return const char * Nguyễn Thái Ngọc Duy
@ 2014-09-10 22:41   ` Nguyễn Thái Ngọc Duy
  2014-09-10 22:41   ` [PATCH v2 04/32] path.c: rename vsnpath() to do_git_path() Nguyễn Thái Ngọc Duy
                     ` (29 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-10 22:41 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

In the previous patch, git_snpath() is modified to allocate a new
strbuf buffer because vsnpath() needs that. But that makes it
awkward because git_snpath() receives a pre-allocated buffer from
outside and has to copy data back. Rename it to strbuf_git_path()
and make it receive strbuf directly.

Using git_path() in update_refs_for_switch() which used to call
git_snpath() is safe because that function and all of its callers do
not keep any pointer to the round-robin buffer pool allocated by
get_pathname().

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 builtin/checkout.c | 13 +++++----
 cache.h            |  4 +--
 path.c             | 11 ++------
 refs.c             | 78 +++++++++++++++++++++++++++++++++---------------------
 refs.h             |  2 +-
 5 files changed, 61 insertions(+), 47 deletions(-)

diff --git a/builtin/checkout.c b/builtin/checkout.c
index 3abcef1..8023987 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -584,18 +584,21 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
 		if (opts->new_orphan_branch) {
 			if (opts->new_branch_log && !log_all_ref_updates) {
 				int temp;
-				char log_file[PATH_MAX];
-				const char *ref_name = mkpath("refs/heads/%s", opts->new_orphan_branch);
+				struct strbuf log_file = STRBUF_INIT;
+				int ret;
+				const char *ref_name;
 
+				ref_name = mkpath("refs/heads/%s", opts->new_orphan_branch);
 				temp = log_all_ref_updates;
 				log_all_ref_updates = 1;
-				if (log_ref_setup(ref_name, log_file, sizeof(log_file))) {
+				ret = log_ref_setup(ref_name, &log_file);
+				log_all_ref_updates = temp;
+				strbuf_release(&log_file);
+				if (ret) {
 					fprintf(stderr, _("Can not do reflog for '%s'\n"),
 					    opts->new_orphan_branch);
-					log_all_ref_updates = temp;
 					return;
 				}
-				log_all_ref_updates = temp;
 			}
 		}
 		else
diff --git a/cache.h b/cache.h
index 707408b..279b581 100644
--- a/cache.h
+++ b/cache.h
@@ -674,8 +674,8 @@ extern int check_repository_format(void);
 
 extern char *mksnpath(char *buf, size_t n, const char *fmt, ...)
 	__attribute__((format (printf, 3, 4)));
-extern char *git_snpath(char *buf, size_t n, const char *fmt, ...)
-	__attribute__((format (printf, 3, 4)));
+extern void strbuf_git_path(struct strbuf *sb, const char *fmt, ...)
+	__attribute__((format (printf, 2, 3)));
 extern char *git_pathdup(const char *fmt, ...)
 	__attribute__((format (printf, 1, 2)));
 extern char *mkpathdup(const char *fmt, ...)
diff --git a/path.c b/path.c
index a3f8826..e545064 100644
--- a/path.c
+++ b/path.c
@@ -70,19 +70,12 @@ static void vsnpath(struct strbuf *buf, const char *fmt, va_list args)
 	strbuf_cleanup_path(buf);
 }
 
-char *git_snpath(char *buf, size_t n, const char *fmt, ...)
+void strbuf_git_path(struct strbuf *sb, const char *fmt, ...)
 {
-	struct strbuf sb = STRBUF_INIT;
 	va_list args;
 	va_start(args, fmt);
-	vsnpath(&sb, fmt, args);
+	vsnpath(sb, fmt, args);
 	va_end(args);
-	if (sb.len >= n)
-		strlcpy(buf, bad_path, n);
-	else
-		memcpy(buf, sb.buf, sb.len + 1);
-	strbuf_release(&sb);
-	return buf;
 }
 
 char *git_pathdup(const char *fmt, ...)
diff --git a/refs.c b/refs.c
index dbd131e..00b2312 100644
--- a/refs.c
+++ b/refs.c
@@ -1535,10 +1535,12 @@ static const char *handle_missing_loose_ref(const char *refname,
 
 const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int reading, int *flag)
 {
+	struct strbuf sb_path = STRBUF_INIT;
 	int depth = MAXDEPTH;
 	ssize_t len;
 	char buffer[256];
 	static char refname_buffer[256];
+	const char *ret;
 
 	if (flag)
 		*flag = 0;
@@ -1547,15 +1549,17 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea
 		return NULL;
 
 	for (;;) {
-		char path[PATH_MAX];
+		const char *path;
 		struct stat st;
 		char *buf;
 		int fd;
 
 		if (--depth < 0)
-			return NULL;
+			goto fail;
 
-		git_snpath(path, sizeof(path), "%s", refname);
+		strbuf_reset(&sb_path);
+		strbuf_git_path(&sb_path, "%s", refname);
+		path = sb_path.buf;
 
 		/*
 		 * We might have to loop back here to avoid a race
@@ -1569,10 +1573,11 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea
 	stat_ref:
 		if (lstat(path, &st) < 0) {
 			if (errno == ENOENT)
-				return handle_missing_loose_ref(refname, sha1,
-								reading, flag);
+				ret = handle_missing_loose_ref(refname, sha1,
+							       reading, flag);
 			else
-				return NULL;
+				ret = NULL;
+			goto done;
 		}
 
 		/* Follow "normalized" - ie "refs/.." symlinks by hand */
@@ -1583,7 +1588,7 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea
 					/* inconsistent with lstat; retry */
 					goto stat_ref;
 				else
-					return NULL;
+					goto fail;
 			}
 			buffer[len] = 0;
 			if (starts_with(buffer, "refs/") &&
@@ -1599,7 +1604,7 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea
 		/* Is it a directory? */
 		if (S_ISDIR(st.st_mode)) {
 			errno = EISDIR;
-			return NULL;
+			goto fail;
 		}
 
 		/*
@@ -1612,12 +1617,13 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea
 				/* inconsistent with lstat; retry */
 				goto stat_ref;
 			else
-				return NULL;
+				goto fail;
 		}
+
 		len = read_in_full(fd, buffer, sizeof(buffer)-1);
 		close(fd);
 		if (len < 0)
-			return NULL;
+			goto fail;
 		while (len && isspace(buffer[len-1]))
 			len--;
 		buffer[len] = '\0';
@@ -1634,9 +1640,10 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea
 			    (buffer[40] != '\0' && !isspace(buffer[40]))) {
 				if (flag)
 					*flag |= REF_ISBROKEN;
-				return NULL;
+				goto fail;
 			}
-			return refname;
+			ret = refname;
+			goto done;
 		}
 		if (flag)
 			*flag |= REF_ISSYMREF;
@@ -1646,10 +1653,15 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea
 		if (check_refname_format(buf, REFNAME_ALLOW_ONELEVEL)) {
 			if (flag)
 				*flag |= REF_ISBROKEN;
-			return NULL;
+			goto fail;
 		}
 		refname = strcpy(refname_buffer, buf);
 	}
+fail:
+	ret = NULL;
+done:
+	strbuf_release(&sb_path);
+	return ret;
 }
 
 char *resolve_refdup(const char *ref, unsigned char *sha1, int reading, int *flag)
@@ -2940,41 +2952,41 @@ static int copy_msg(char *buf, const char *msg)
 	return cp - buf;
 }
 
-int log_ref_setup(const char *refname, char *logfile, int bufsize)
+int log_ref_setup(const char *refname, struct strbuf *logfile)
 {
 	int logfd, oflags = O_APPEND | O_WRONLY;
 
-	git_snpath(logfile, bufsize, "logs/%s", refname);
+	strbuf_git_path(logfile, "logs/%s", refname);
 	if (log_all_ref_updates &&
 	    (starts_with(refname, "refs/heads/") ||
 	     starts_with(refname, "refs/remotes/") ||
 	     starts_with(refname, "refs/notes/") ||
 	     !strcmp(refname, "HEAD"))) {
-		if (safe_create_leading_directories(logfile) < 0)
+		if (safe_create_leading_directories(logfile->buf) < 0)
 			return error("unable to create directory for %s",
-				     logfile);
+				     logfile->buf);
 		oflags |= O_CREAT;
 	}
 
-	logfd = open(logfile, oflags, 0666);
+	logfd = open(logfile->buf, oflags, 0666);
 	if (logfd < 0) {
 		if (!(oflags & O_CREAT) && errno == ENOENT)
 			return 0;
 
 		if ((oflags & O_CREAT) && errno == EISDIR) {
-			if (remove_empty_directories(logfile)) {
+			if (remove_empty_directories(logfile->buf)) {
 				return error("There are still logs under '%s'",
-					     logfile);
+					     logfile->buf);
 			}
-			logfd = open(logfile, oflags, 0666);
+			logfd = open(logfile->buf, oflags, 0666);
 		}
 
 		if (logfd < 0)
 			return error("Unable to append to %s: %s",
-				     logfile, strerror(errno));
+				     logfile->buf, strerror(errno));
 	}
 
-	adjust_shared_perm(logfile);
+	adjust_shared_perm(logfile->buf);
 	close(logfd);
 	return 0;
 }
@@ -2985,20 +2997,22 @@ static int log_ref_write(const char *refname, const unsigned char *old_sha1,
 	int logfd, result, written, oflags = O_APPEND | O_WRONLY;
 	unsigned maxlen, len;
 	int msglen;
-	char log_file[PATH_MAX];
+	struct strbuf sb_log_file = STRBUF_INIT;
+	const char *log_file;
 	char *logrec;
 	const char *committer;
 
 	if (log_all_ref_updates < 0)
 		log_all_ref_updates = !is_bare_repository();
 
-	result = log_ref_setup(refname, log_file, sizeof(log_file));
+	result = log_ref_setup(refname, &sb_log_file);
 	if (result)
-		return result;
+		goto done;
+	log_file = sb_log_file.buf;
 
 	logfd = open(log_file, oflags);
 	if (logfd < 0)
-		return 0;
+		goto done;
 	msglen = msg ? strlen(msg) : 0;
 	committer = git_committer_info(0);
 	maxlen = strlen(committer) + msglen + 100;
@@ -3011,9 +3025,13 @@ static int log_ref_write(const char *refname, const unsigned char *old_sha1,
 		len += copy_msg(logrec + len - 1, msg) - 1;
 	written = len <= maxlen ? write_in_full(logfd, logrec, len) : -1;
 	free(logrec);
-	if (close(logfd) != 0 || written != len)
-		return error("Unable to append to %s", log_file);
-	return 0;
+	if (close(logfd) != 0 || written != len) {
+		error("Unable to append to %s", log_file);
+		result = -1;
+	}
+done:
+	strbuf_release(&sb_log_file);
+	return result;
 }
 
 static int is_branch(const char *refname)
diff --git a/refs.h b/refs.h
index 4e3050d..99d88a7 100644
--- a/refs.h
+++ b/refs.h
@@ -157,7 +157,7 @@ extern void unlock_ref(struct ref_lock *lock);
 extern int write_ref_sha1(struct ref_lock *lock, const unsigned char *sha1, const char *msg);
 
 /** Setup reflog before using. **/
-int log_ref_setup(const char *refname, char *logfile, int bufsize);
+int log_ref_setup(const char *refname, struct strbuf *logfile);
 
 /** Reads log for the value of ref during at_time. **/
 extern int read_ref_at(const char *refname, unsigned long at_time, int cnt,
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 04/32] path.c: rename vsnpath() to do_git_path()
  2014-09-10 22:41 ` [PATCH v2 00/32] nd/multiple-work-trees Nguyễn Thái Ngọc Duy
                     ` (2 preceding siblings ...)
  2014-09-10 22:41   ` [PATCH v2 03/32] git_snpath(): retire and replace with strbuf_git_path() Nguyễn Thái Ngọc Duy
@ 2014-09-10 22:41   ` Nguyễn Thái Ngọc Duy
  2014-09-10 22:41   ` [PATCH v2 05/32] path.c: group git_path(), git_pathdup() and strbuf_git_path() together Nguyễn Thái Ngọc Duy
                     ` (28 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-10 22:41 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

The name vsnpath() gives an impression that this is general path
handling function. It's not. This is the underlying implementation of
git_path(), git_pathdup() and strbuf_git_path() which will prefix
$GIT_DIR in the result string.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 path.c | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/path.c b/path.c
index e545064..2cb2e61 100644
--- a/path.c
+++ b/path.c
@@ -60,7 +60,7 @@ char *mksnpath(char *buf, size_t n, const char *fmt, ...)
 	return cleanup_path(buf);
 }
 
-static void vsnpath(struct strbuf *buf, const char *fmt, va_list args)
+static void do_git_path(struct strbuf *buf, const char *fmt, va_list args)
 {
 	const char *git_dir = get_git_dir();
 	strbuf_addstr(buf, git_dir);
@@ -74,7 +74,7 @@ void strbuf_git_path(struct strbuf *sb, const char *fmt, ...)
 {
 	va_list args;
 	va_start(args, fmt);
-	vsnpath(sb, fmt, args);
+	do_git_path(sb, fmt, args);
 	va_end(args);
 }
 
@@ -83,7 +83,7 @@ char *git_pathdup(const char *fmt, ...)
 	struct strbuf path = STRBUF_INIT;
 	va_list args;
 	va_start(args, fmt);
-	vsnpath(&path, fmt, args);
+	do_git_path(&path, fmt, args);
 	va_end(args);
 	return strbuf_detach(&path, NULL);
 }
@@ -114,7 +114,7 @@ const char *git_path(const char *fmt, ...)
 	struct strbuf *pathname = get_pathname();
 	va_list args;
 	va_start(args, fmt);
-	vsnpath(pathname, fmt, args);
+	do_git_path(pathname, fmt, args);
 	va_end(args);
 	return pathname->buf;
 }
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 05/32] path.c: group git_path(), git_pathdup() and strbuf_git_path() together
  2014-09-10 22:41 ` [PATCH v2 00/32] nd/multiple-work-trees Nguyễn Thái Ngọc Duy
                     ` (3 preceding siblings ...)
  2014-09-10 22:41   ` [PATCH v2 04/32] path.c: rename vsnpath() to do_git_path() Nguyễn Thái Ngọc Duy
@ 2014-09-10 22:41   ` Nguyễn Thái Ngọc Duy
  2014-09-10 22:41   ` [PATCH v2 06/32] git_path(): be aware of file relocation in $GIT_DIR Nguyễn Thái Ngọc Duy
                     ` (27 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-10 22:41 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 path.c | 20 ++++++++++----------
 1 file changed, 10 insertions(+), 10 deletions(-)

diff --git a/path.c b/path.c
index 2cb2e61..65881aa 100644
--- a/path.c
+++ b/path.c
@@ -78,6 +78,16 @@ void strbuf_git_path(struct strbuf *sb, const char *fmt, ...)
 	va_end(args);
 }
 
+const char *git_path(const char *fmt, ...)
+{
+	struct strbuf *pathname = get_pathname();
+	va_list args;
+	va_start(args, fmt);
+	do_git_path(pathname, fmt, args);
+	va_end(args);
+	return pathname->buf;
+}
+
 char *git_pathdup(const char *fmt, ...)
 {
 	struct strbuf path = STRBUF_INIT;
@@ -109,16 +119,6 @@ const char *mkpath(const char *fmt, ...)
 	return cleanup_path(pathname->buf);
 }
 
-const char *git_path(const char *fmt, ...)
-{
-	struct strbuf *pathname = get_pathname();
-	va_list args;
-	va_start(args, fmt);
-	do_git_path(pathname, fmt, args);
-	va_end(args);
-	return pathname->buf;
-}
-
 void home_config_paths(char **global, char **xdg, char *file)
 {
 	char *xdg_home = getenv("XDG_CONFIG_HOME");
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 06/32] git_path(): be aware of file relocation in $GIT_DIR
  2014-09-10 22:41 ` [PATCH v2 00/32] nd/multiple-work-trees Nguyễn Thái Ngọc Duy
                     ` (4 preceding siblings ...)
  2014-09-10 22:41   ` [PATCH v2 05/32] path.c: group git_path(), git_pathdup() and strbuf_git_path() together Nguyễn Thái Ngọc Duy
@ 2014-09-10 22:41   ` Nguyễn Thái Ngọc Duy
  2014-09-10 22:41   ` [PATCH v2 07/32] *.sh: respect $GIT_INDEX_FILE Nguyễn Thái Ngọc Duy
                     ` (26 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-10 22:41 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

We allow the user to relocate certain paths out of $GIT_DIR via
environment variables, e.g. GIT_OBJECT_DIRECTORY, GIT_INDEX_FILE and
GIT_GRAFT_FILE. Callers are not supposed to use git_path() or
git_pathdup() to get those paths. Instead they must use
get_object_directory(), get_index_file() and get_graft_file()
respectively. This is inconvenient and could be missed in review (for
example, there's git_path("objects/info/alternates") somewhere in
sha1_file.c).

This patch makes git_path() and git_pathdup() understand those
environment variables. So if you set GIT_OBJECT_DIRECTORY to /foo/bar,
git_path("objects/abc") should return /foo/bar/abc. The same is done
for the two remaining env variables.

"git rev-parse --git-path" is the wrapper for script use.

This patch kinda reverts a0279e1 (setup_git_env: use git_pathdup
instead of xmalloc + sprintf - 2014-06-19) because using git_pathdup
here would result in infinite recursion:

  setup_git_env() -> git_pathdup("objects") -> .. -> adjust_git_path()
  -> get_object_directory() -> oops, git_object_directory is NOT set
  yet -> setup_git_env()

I wanted to make git_pathdup_literal() that skips adjust_git_path().
But that won't work because later on when $GIT_COMMON_DIR is
introduced, git_pathdup_literal("objects") needs adjust_git_path() to
replace $GIT_DIR with $GIT_COMMON_DIR.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 Documentation/git-rev-parse.txt |  7 ++++++
 builtin/rev-parse.c             |  7 ++++++
 cache.h                         |  1 +
 environment.c                   | 19 +++++++++++-----
 path.c                          | 49 +++++++++++++++++++++++++++++++++++++++--
 t/t0060-path-utils.sh           | 19 ++++++++++++++++
 6 files changed, 95 insertions(+), 7 deletions(-)

diff --git a/Documentation/git-rev-parse.txt b/Documentation/git-rev-parse.txt
index 987395d..9465399 100644
--- a/Documentation/git-rev-parse.txt
+++ b/Documentation/git-rev-parse.txt
@@ -232,6 +232,13 @@ print a message to stderr and exit with nonzero status.
 	repository.  If <path> is a gitfile then the resolved path
 	to the real repository is printed.
 
+--git-path <path>::
+	Resolve "$GIT_DIR/<path>" and takes other path relocation
+	variables such as $GIT_OBJECT_DIRECTORY,
+	$GIT_INDEX_FILE... into account. For example, if
+	$GIT_OBJECT_DIRECTORY is set to /foo/bar then "git rev-parse
+	--git-path objects/abc" returns /foo/bar/abc.
+
 --show-cdup::
 	When the command is invoked from a subdirectory, show the
 	path of the top-level directory relative to the current
diff --git a/builtin/rev-parse.c b/builtin/rev-parse.c
index 1a6122d..7606d43 100644
--- a/builtin/rev-parse.c
+++ b/builtin/rev-parse.c
@@ -529,6 +529,13 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)
 	for (i = 1; i < argc; i++) {
 		const char *arg = argv[i];
 
+		if (!strcmp(arg, "--git-path")) {
+			if (!argv[i + 1])
+				die("--git-path requires an argument");
+			puts(git_path("%s", argv[i + 1]));
+			i++;
+			continue;
+		}
 		if (as_is) {
 			if (show_file(arg, output_prefix) && as_is < 2)
 				verify_filename(prefix, arg, 0);
diff --git a/cache.h b/cache.h
index 279b581..0128b9a 100644
--- a/cache.h
+++ b/cache.h
@@ -612,6 +612,7 @@ extern int fsync_object_files;
 extern int core_preload_index;
 extern int core_apply_sparse_checkout;
 extern int precomposed_unicode;
+extern int git_db_env, git_index_env, git_graft_env;
 
 /*
  * The character that begins a commented line in user-editable file
diff --git a/environment.c b/environment.c
index 565f652..fee12a6 100644
--- a/environment.c
+++ b/environment.c
@@ -83,6 +83,7 @@ static size_t namespace_len;
 
 static const char *git_dir;
 static char *git_object_dir, *git_index_file, *git_graft_file;
+int git_db_env, git_index_env, git_graft_env;
 
 /*
  * Repository-local GIT_* environment variables; see cache.h for details.
@@ -124,10 +125,18 @@ static char *expand_namespace(const char *raw_namespace)
 	return strbuf_detach(&buf, NULL);
 }
 
-static char *git_path_from_env(const char *envvar, const char *path)
+static char *git_path_from_env(const char *envvar, const char *path,
+			       int *fromenv)
 {
 	const char *value = getenv(envvar);
-	return value ? xstrdup(value) : git_pathdup("%s", path);
+	if (!value) {
+		char *buf = xmalloc(strlen(git_dir) + strlen(path) + 2);
+		sprintf(buf, "%s/%s", git_dir, path);
+		return buf;
+	}
+	if (fromenv)
+		*fromenv = 1;
+	return xstrdup(value);
 }
 
 static void setup_git_env(void)
@@ -140,9 +149,9 @@ static void setup_git_env(void)
 		git_dir = DEFAULT_GIT_DIR_ENVIRONMENT;
 	gitfile = read_gitfile(git_dir);
 	git_dir = xstrdup(gitfile ? gitfile : git_dir);
-	git_object_dir = git_path_from_env(DB_ENVIRONMENT, "objects");
-	git_index_file = git_path_from_env(INDEX_ENVIRONMENT, "index");
-	git_graft_file = git_path_from_env(GRAFT_ENVIRONMENT, "info/grafts");
+	git_object_dir = git_path_from_env(DB_ENVIRONMENT, "objects", &git_db_env);
+	git_index_file = git_path_from_env(INDEX_ENVIRONMENT, "index", &git_index_env);
+	git_graft_file = git_path_from_env(GRAFT_ENVIRONMENT, "info/grafts", &git_graft_env);
 	if (getenv(NO_REPLACE_OBJECTS_ENVIRONMENT))
 		check_replace_refs = 0;
 	namespace = expand_namespace(getenv(GIT_NAMESPACE_ENVIRONMENT));
diff --git a/path.c b/path.c
index 65881aa..3deb80c 100644
--- a/path.c
+++ b/path.c
@@ -60,13 +60,58 @@ char *mksnpath(char *buf, size_t n, const char *fmt, ...)
 	return cleanup_path(buf);
 }
 
+static int dir_prefix(const char *buf, const char *dir)
+{
+	int len = strlen(dir);
+	return !strncmp(buf, dir, len) &&
+		(is_dir_sep(buf[len]) || buf[len] == '\0');
+}
+
+/* $buf =~ m|$dir/+$file| but without regex */
+static int is_dir_file(const char *buf, const char *dir, const char *file)
+{
+	int len = strlen(dir);
+	if (strncmp(buf, dir, len) || !is_dir_sep(buf[len]))
+		return 0;
+	while (is_dir_sep(buf[len]))
+		len++;
+	return !strcmp(buf + len, file);
+}
+
+static void replace_dir(struct strbuf *buf, int len, const char *newdir)
+{
+	int newlen = strlen(newdir);
+	int need_sep = (buf->buf[len] && !is_dir_sep(buf->buf[len])) &&
+		!is_dir_sep(newdir[newlen - 1]);
+	if (need_sep)
+		len--;	 /* keep one char, to be replaced with '/'  */
+	strbuf_splice(buf, 0, len, newdir, newlen);
+	if (need_sep)
+		buf->buf[newlen] = '/';
+}
+
+static void adjust_git_path(struct strbuf *buf, int git_dir_len)
+{
+	const char *base = buf->buf + git_dir_len;
+	if (git_graft_env && is_dir_file(base, "info", "grafts"))
+		strbuf_splice(buf, 0, buf->len,
+			      get_graft_file(), strlen(get_graft_file()));
+	else if (git_index_env && !strcmp(base, "index"))
+		strbuf_splice(buf, 0, buf->len,
+			      get_index_file(), strlen(get_index_file()));
+	else if (git_db_env && dir_prefix(base, "objects"))
+		replace_dir(buf, git_dir_len + 7, get_object_directory());
+}
+
 static void do_git_path(struct strbuf *buf, const char *fmt, va_list args)
 {
-	const char *git_dir = get_git_dir();
-	strbuf_addstr(buf, git_dir);
+	int gitdir_len;
+	strbuf_addstr(buf, get_git_dir());
 	if (buf->len && !is_dir_sep(buf->buf[buf->len - 1]))
 		strbuf_addch(buf, '/');
+	gitdir_len = buf->len;
 	strbuf_vaddf(buf, fmt, args);
+	adjust_git_path(buf, gitdir_len);
 	strbuf_cleanup_path(buf);
 }
 
diff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh
index c0143a0..33d2818 100755
--- a/t/t0060-path-utils.sh
+++ b/t/t0060-path-utils.sh
@@ -19,6 +19,14 @@ relative_path() {
 	"test \"\$(test-path-utils relative_path '$1' '$2')\" = '$expected'"
 }
 
+test_git_path() {
+	test_expect_success "git-path $1 $2 => $3" "
+		$1 git rev-parse --git-path $2 >actual &&
+		echo $3 >expect &&
+		test_cmp expect actual
+	"
+}
+
 # On Windows, we are using MSYS's bash, which mangles the paths.
 # Absolute paths are anchored at the MSYS installation directory,
 # which means that the path / accounts for this many characters:
@@ -244,4 +252,15 @@ relative_path "<null>"		"<empty>"	./
 relative_path "<null>"		"<null>"	./
 relative_path "<null>"		/foo/a/b	./
 
+test_git_path A=B                info/grafts .git/info/grafts
+test_git_path GIT_GRAFT_FILE=foo info/grafts foo
+test_git_path GIT_GRAFT_FILE=foo info/////grafts foo
+test_git_path GIT_INDEX_FILE=foo index foo
+test_git_path GIT_INDEX_FILE=foo index/foo .git/index/foo
+test_git_path GIT_INDEX_FILE=foo index2 .git/index2
+test_expect_success 'setup fake objects directory foo' 'mkdir foo'
+test_git_path GIT_OBJECT_DIRECTORY=foo objects foo
+test_git_path GIT_OBJECT_DIRECTORY=foo objects/foo foo/foo
+test_git_path GIT_OBJECT_DIRECTORY=foo objects2 .git/objects2
+
 test_done
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 07/32] *.sh: respect $GIT_INDEX_FILE
  2014-09-10 22:41 ` [PATCH v2 00/32] nd/multiple-work-trees Nguyễn Thái Ngọc Duy
                     ` (5 preceding siblings ...)
  2014-09-10 22:41   ` [PATCH v2 06/32] git_path(): be aware of file relocation in $GIT_DIR Nguyễn Thái Ngọc Duy
@ 2014-09-10 22:41   ` Nguyễn Thái Ngọc Duy
  2014-09-10 22:41   ` [PATCH v2 08/32] reflog: avoid constructing .lock path with git_path Nguyễn Thái Ngọc Duy
                     ` (25 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-10 22:41 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 git-pull.sh  | 2 +-
 git-stash.sh | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/git-pull.sh b/git-pull.sh
index 18a394f..6ab0c31 100755
--- a/git-pull.sh
+++ b/git-pull.sh
@@ -240,7 +240,7 @@ test true = "$rebase" && {
 	if ! git rev-parse -q --verify HEAD >/dev/null
 	then
 		# On an unborn branch
-		if test -f "$GIT_DIR/index"
+		if test -f "$(git rev-parse --git-path index)"
 		then
 			die "$(gettext "updating an unborn branch with changes added to the index")"
 		fi
diff --git a/git-stash.sh b/git-stash.sh
index bcc757b..393e1ec 100755
--- a/git-stash.sh
+++ b/git-stash.sh
@@ -20,7 +20,7 @@ require_work_tree
 cd_to_toplevel
 
 TMP="$GIT_DIR/.git-stash.$$"
-TMPindex=${GIT_INDEX_FILE-"$GIT_DIR/index"}.stash.$$
+TMPindex=${GIT_INDEX_FILE-"$(git rev-parse --git-path index)"}.stash.$$
 trap 'rm -f "$TMP-"* "$TMPindex"' 0
 
 ref_stash=refs/stash
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 08/32] reflog: avoid constructing .lock path with git_path
  2014-09-10 22:41 ` [PATCH v2 00/32] nd/multiple-work-trees Nguyễn Thái Ngọc Duy
                     ` (6 preceding siblings ...)
  2014-09-10 22:41   ` [PATCH v2 07/32] *.sh: respect $GIT_INDEX_FILE Nguyễn Thái Ngọc Duy
@ 2014-09-10 22:41   ` Nguyễn Thái Ngọc Duy
  2014-09-10 22:41   ` [PATCH v2 09/32] fast-import: use git_path() for accessing .git dir instead of get_git_dir() Nguyễn Thái Ngọc Duy
                     ` (24 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-10 22:41 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Among pathnames in $GIT_DIR, e.g. "index" or "packed-refs", we want to
automatically and silently map some of them to the $GIT_DIR of the
repository we are borrowing from via $GIT_COMMON_DIR mechanism.  When
we formulate the pathname for its lockfile, we want it to be in the
same location as its final destination.  "index" is not shared and
needs to remain in the borrowing repository, while "packed-refs" is
shared and needs to go to the borrowed repository.

git_path() could be taught about the ".lock" suffix and map
"index.lock" and "packed-refs.lock" the same way their basenames are
mapped, but instead the caller can help by asking where the basename
(e.g. "index") is mapped to git_path() and then appending ".lock"
after the mapping is done.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 builtin/reflog.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/builtin/reflog.c b/builtin/reflog.c
index e8a8fb1..9bd874d 100644
--- a/builtin/reflog.c
+++ b/builtin/reflog.c
@@ -372,7 +372,7 @@ static int expire_reflog(const char *ref, const unsigned char *sha1, int unused,
 	if (!reflog_exists(ref))
 		goto finish;
 	if (!cmd->dry_run) {
-		newlog_path = git_pathdup("logs/%s.lock", ref);
+		newlog_path = mkpathdup("%s.lock", log_file);
 		cb.newlog = fopen(newlog_path, "w");
 	}
 
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 09/32] fast-import: use git_path() for accessing .git dir instead of get_git_dir()
  2014-09-10 22:41 ` [PATCH v2 00/32] nd/multiple-work-trees Nguyễn Thái Ngọc Duy
                     ` (7 preceding siblings ...)
  2014-09-10 22:41   ` [PATCH v2 08/32] reflog: avoid constructing .lock path with git_path Nguyễn Thái Ngọc Duy
@ 2014-09-10 22:41   ` Nguyễn Thái Ngọc Duy
  2014-09-10 22:41   ` [PATCH v2 10/32] commit: use SEQ_DIR instead of hardcoding "sequencer" Nguyễn Thái Ngọc Duy
                     ` (23 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-10 22:41 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

This allows git_path() to redirect info/fast-import to another place
if needed

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 fast-import.c | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/fast-import.c b/fast-import.c
index d9c068b..ea426c4 100644
--- a/fast-import.c
+++ b/fast-import.c
@@ -3103,12 +3103,9 @@ static void parse_progress(void)
 
 static char* make_fast_import_path(const char *path)
 {
-	struct strbuf abs_path = STRBUF_INIT;
-
 	if (!relative_marks_paths || is_absolute_path(path))
 		return xstrdup(path);
-	strbuf_addf(&abs_path, "%s/info/fast-import/%s", get_git_dir(), path);
-	return strbuf_detach(&abs_path, NULL);
+	return xstrdup(git_path("info/fast-import/%s", path));
 }
 
 static void option_import_marks(const char *marks,
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 10/32] commit: use SEQ_DIR instead of hardcoding "sequencer"
  2014-09-10 22:41 ` [PATCH v2 00/32] nd/multiple-work-trees Nguyễn Thái Ngọc Duy
                     ` (8 preceding siblings ...)
  2014-09-10 22:41   ` [PATCH v2 09/32] fast-import: use git_path() for accessing .git dir instead of get_git_dir() Nguyễn Thái Ngọc Duy
@ 2014-09-10 22:41   ` Nguyễn Thái Ngọc Duy
  2014-09-10 22:41   ` [PATCH v2 11/32] $GIT_COMMON_DIR: a new environment variable Nguyễn Thái Ngọc Duy
                     ` (22 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-10 22:41 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 builtin/commit.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/builtin/commit.c b/builtin/commit.c
index 461c3b1..4b9f012 100644
--- a/builtin/commit.c
+++ b/builtin/commit.c
@@ -156,7 +156,7 @@ static void determine_whence(struct wt_status *s)
 		whence = FROM_MERGE;
 	else if (file_exists(git_path("CHERRY_PICK_HEAD"))) {
 		whence = FROM_CHERRY_PICK;
-		if (file_exists(git_path("sequencer")))
+		if (file_exists(git_path(SEQ_DIR)))
 			sequencer_in_use = 1;
 	}
 	else
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 11/32] $GIT_COMMON_DIR: a new environment variable
  2014-09-10 22:41 ` [PATCH v2 00/32] nd/multiple-work-trees Nguyễn Thái Ngọc Duy
                     ` (9 preceding siblings ...)
  2014-09-10 22:41   ` [PATCH v2 10/32] commit: use SEQ_DIR instead of hardcoding "sequencer" Nguyễn Thái Ngọc Duy
@ 2014-09-10 22:41   ` Nguyễn Thái Ngọc Duy
  2014-09-10 22:41   ` [PATCH v2 12/32] git-sh-setup.sh: use rev-parse --git-path to get $GIT_DIR/objects Nguyễn Thái Ngọc Duy
                     ` (21 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-10 22:41 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

This variable is intended to support multiple working directories
attached to a repository. Such a repository may have a main working
directory, created by either "git init" or "git clone" and one or more
linked working directories. These working directories and the main
repository share the same repository directory.

In linked working directories, $GIT_COMMON_DIR must be defined to point
to the real repository directory and $GIT_DIR points to an unused
subdirectory inside $GIT_COMMON_DIR. File locations inside the
repository are reorganized from the linked worktree view point:

 - worktree-specific such as HEAD, logs/HEAD, index, other top-level
   refs and unrecognized files are from $GIT_DIR.

 - the rest like objects, refs, info, hooks, packed-refs, shallow...
   are from $GIT_COMMON_DIR (except info/sparse-checkout, but that's
   a separate patch)

Scripts are supposed to retrieve paths in $GIT_DIR with "git rev-parse
--git-path", which will take care of "$GIT_DIR vs $GIT_COMMON_DIR"
business.

The redirection is done by git_path(), git_pathdup() and
strbuf_git_path(). The selected list of paths goes to $GIT_COMMON_DIR,
not the other way around in case a developer adds a new
worktree-specific file and it's accidentally promoted to be shared
across repositories (this includes unknown files added by third party
commands)

The list of known files that belong to $GIT_DIR are:

ADD_EDIT.patch BISECT_ANCESTORS_OK BISECT_EXPECTED_REV BISECT_LOG
BISECT_NAMES CHERRY_PICK_HEAD COMMIT_MSG FETCH_HEAD HEAD MERGE_HEAD
MERGE_MODE MERGE_RR NOTES_EDITMSG NOTES_MERGE_WORKTREE ORIG_HEAD
REVERT_HEAD SQUASH_MSG TAG_EDITMSG fast_import_crash_* logs/HEAD
next-index-* rebase-apply rebase-merge rsync-refs-* sequencer/*
shallow_*

Path mapping is NOT done for git_path_submodule(). Multi-checkouts are
not supported as submodules.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
---
 Documentation/git.txt                  |  8 ++++++
 Documentation/gitrepository-layout.txt | 45 +++++++++++++++++++++++++---------
 cache.h                                |  4 ++-
 environment.c                          | 28 +++++++++++++++------
 path.c                                 | 34 +++++++++++++++++++++++++
 t/t0060-path-utils.sh                  | 16 ++++++++++++
 6 files changed, 116 insertions(+), 19 deletions(-)

diff --git a/Documentation/git.txt b/Documentation/git.txt
index 7924209..749052f 100644
--- a/Documentation/git.txt
+++ b/Documentation/git.txt
@@ -788,6 +788,14 @@ Git so take care if using Cogito etc.
 	an explicit repository directory set via 'GIT_DIR' or on the
 	command line.
 
+'GIT_COMMON_DIR'::
+	If this variable is set to a path, non-worktree files that are
+	normally in $GIT_DIR will be taken from this path
+	instead. Worktree-specific files such as HEAD or index are
+	taken from $GIT_DIR. See linkgit:gitrepository-layout[5] for
+	details. This variable has lower precedence than other path
+	variables such as GIT_INDEX_FILE, GIT_OBJECT_DIRECTORY...
+
 Git Commits
 ~~~~~~~~~~~
 'GIT_AUTHOR_NAME'::
diff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt
index 17d2ea6..b7b9dfb 100644
--- a/Documentation/gitrepository-layout.txt
+++ b/Documentation/gitrepository-layout.txt
@@ -46,6 +46,9 @@ of incomplete object store is not suitable to be published for
 use with dumb transports but otherwise is OK as long as
 `objects/info/alternates` points at the object stores it
 borrows from.
++
+This directory is ignored if $GIT_COMMON_DIR is set and
+"$GIT_COMMON_DIR/objects" will be used instead.
 
 objects/[0-9a-f][0-9a-f]::
 	A newly created object is stored in its own file.
@@ -92,7 +95,8 @@ refs::
 	References are stored in subdirectories of this
 	directory.  The 'git prune' command knows to preserve
 	objects reachable from refs found in this directory and
-	its subdirectories.
+	its subdirectories. This directory is ignored if $GIT_COMMON_DIR
+	is set and "$GIT_COMMON_DIR/refs" will be used instead.
 
 refs/heads/`name`::
 	records tip-of-the-tree commit objects of branch `name`
@@ -114,7 +118,8 @@ refs/replace/`<obj-sha1>`::
 packed-refs::
 	records the same information as refs/heads/, refs/tags/,
 	and friends record in a more efficient way.  See
-	linkgit:git-pack-refs[1].
+	linkgit:git-pack-refs[1]. This file is ignored if $GIT_COMMON_DIR
+	is set and "$GIT_COMMON_DIR/packed-refs" will be used instead.
 
 HEAD::
 	A symref (see glossary) to the `refs/heads/` namespace
@@ -133,6 +138,11 @@ being a symref to point at the current branch.  Such a state
 is often called 'detached HEAD.'  See linkgit:git-checkout[1]
 for details.
 
+config::
+	Repository specific configuration file. This file is ignored
+	if $GIT_COMMON_DIR is set and "$GIT_COMMON_DIR/config" will be
+	used instead.
+
 branches::
 	A slightly deprecated way to store shorthands to be used
 	to specify a URL to 'git fetch', 'git pull' and 'git push'.
@@ -140,7 +150,10 @@ branches::
 	'name' can be given to these commands in place of
 	'repository' argument.  See the REMOTES section in
 	linkgit:git-fetch[1] for details.  This mechanism is legacy
-	and not likely to be found in modern repositories.
+	and not likely to be found in modern repositories. This
+	directory is ignored if $GIT_COMMON_DIR is set and
+	"$GIT_COMMON_DIR/branches" will be used instead.
+
 
 hooks::
 	Hooks are customization scripts used by various Git
@@ -149,7 +162,9 @@ hooks::
 	default.  To enable, the `.sample` suffix has to be
 	removed from the filename by renaming.
 	Read linkgit:githooks[5] for more details about
-	each hook.
+	each hook. This directory is ignored if $GIT_COMMON_DIR is set
+	and "$GIT_COMMON_DIR/hooks" will be used instead.
+
 
 index::
 	The current index file for the repository.  It is
@@ -157,7 +172,8 @@ index::
 
 info::
 	Additional information about the repository is recorded
-	in this directory.
+	in this directory. This directory is ignored if $GIT_COMMON_DIR
+	is set and "$GIT_COMMON_DIR/index" will be used instead.
 
 info/refs::
 	This file helps dumb transports discover what refs are
@@ -197,12 +213,15 @@ remotes::
 	when interacting with remote repositories via 'git fetch',
 	'git pull' and 'git push' commands.  See the REMOTES section
 	in linkgit:git-fetch[1] for details.  This mechanism is legacy
-	and not likely to be found in modern repositories.
+	and not likely to be found in modern repositories. This
+	directory is ignored if $GIT_COMMON_DIR is set and
+	"$GIT_COMMON_DIR/remotes" will be used instead.
 
 logs::
-	Records of changes made to refs are stored in this
-	directory.  See linkgit:git-update-ref[1]
-	for more information.
+	Records of changes made to refs are stored in this directory.
+	See linkgit:git-update-ref[1] for more information. This
+	directory is ignored if $GIT_COMMON_DIR is set and
+	"$GIT_COMMON_DIR/logs" will be used instead.
 
 logs/refs/heads/`name`::
 	Records all changes made to the branch tip named `name`.
@@ -213,10 +232,14 @@ logs/refs/tags/`name`::
 shallow::
 	This is similar to `info/grafts` but is internally used
 	and maintained by shallow clone mechanism.  See `--depth`
-	option to linkgit:git-clone[1] and linkgit:git-fetch[1].
+	option to linkgit:git-clone[1] and linkgit:git-fetch[1]. This
+	file is ignored if $GIT_COMMON_DIR is set and
+	"$GIT_COMMON_DIR/shallow" will be used instead.
 
 modules::
-	Contains the git-repositories of the submodules.
+	Contains the git-repositories of the submodules. This
+	directory is ignored if $GIT_COMMON_DIR is set and
+	"$GIT_COMMON_DIR/modules" will be used instead.
 
 SEE ALSO
 --------
diff --git a/cache.h b/cache.h
index 0128b9a..b606ee4 100644
--- a/cache.h
+++ b/cache.h
@@ -362,6 +362,7 @@ static inline enum object_type object_type(unsigned int mode)
 
 /* Double-check local_repo_env below if you add to this list. */
 #define GIT_DIR_ENVIRONMENT "GIT_DIR"
+#define GIT_COMMON_DIR_ENVIRONMENT "GIT_COMMON_DIR"
 #define GIT_NAMESPACE_ENVIRONMENT "GIT_NAMESPACE"
 #define GIT_WORK_TREE_ENVIRONMENT "GIT_WORK_TREE"
 #define GIT_PREFIX_ENVIRONMENT "GIT_PREFIX"
@@ -415,6 +416,7 @@ extern int is_inside_git_dir(void);
 extern char *git_work_tree_cfg;
 extern int is_inside_work_tree(void);
 extern const char *get_git_dir(void);
+extern const char *get_git_common_dir(void);
 extern int is_git_directory(const char *path);
 extern char *get_object_directory(void);
 extern char *get_index_file(void);
@@ -612,7 +614,7 @@ extern int fsync_object_files;
 extern int core_preload_index;
 extern int core_apply_sparse_checkout;
 extern int precomposed_unicode;
-extern int git_db_env, git_index_env, git_graft_env;
+extern int git_db_env, git_index_env, git_graft_env, git_common_dir_env;
 
 /*
  * The character that begins a commented line in user-editable file
diff --git a/environment.c b/environment.c
index fee12a6..78a07e4 100644
--- a/environment.c
+++ b/environment.c
@@ -81,9 +81,9 @@ static char *work_tree;
 static const char *namespace;
 static size_t namespace_len;
 
-static const char *git_dir;
+static const char *git_dir, *git_common_dir;
 static char *git_object_dir, *git_index_file, *git_graft_file;
-int git_db_env, git_index_env, git_graft_env;
+int git_db_env, git_index_env, git_graft_env, git_common_dir_env;
 
 /*
  * Repository-local GIT_* environment variables; see cache.h for details.
@@ -125,8 +125,8 @@ static char *expand_namespace(const char *raw_namespace)
 	return strbuf_detach(&buf, NULL);
 }
 
-static char *git_path_from_env(const char *envvar, const char *path,
-			       int *fromenv)
+static char *git_path_from_env(const char *envvar, const char *git_dir,
+			       const char *path, int *fromenv)
 {
 	const char *value = getenv(envvar);
 	if (!value) {
@@ -149,9 +149,18 @@ static void setup_git_env(void)
 		git_dir = DEFAULT_GIT_DIR_ENVIRONMENT;
 	gitfile = read_gitfile(git_dir);
 	git_dir = xstrdup(gitfile ? gitfile : git_dir);
-	git_object_dir = git_path_from_env(DB_ENVIRONMENT, "objects", &git_db_env);
-	git_index_file = git_path_from_env(INDEX_ENVIRONMENT, "index", &git_index_env);
-	git_graft_file = git_path_from_env(GRAFT_ENVIRONMENT, "info/grafts", &git_graft_env);
+	git_common_dir = getenv(GIT_COMMON_DIR_ENVIRONMENT);
+	if (git_common_dir) {
+		git_common_dir_env = 1;
+		git_common_dir = xstrdup(git_common_dir);
+	} else
+		git_common_dir = git_dir;
+	git_object_dir = git_path_from_env(DB_ENVIRONMENT, git_common_dir,
+					   "objects", &git_db_env);
+	git_index_file = git_path_from_env(INDEX_ENVIRONMENT, git_dir,
+					   "index", &git_index_env);
+	git_graft_file = git_path_from_env(GRAFT_ENVIRONMENT, git_common_dir,
+					   "info/grafts", &git_graft_env);
 	if (getenv(NO_REPLACE_OBJECTS_ENVIRONMENT))
 		check_replace_refs = 0;
 	namespace = expand_namespace(getenv(GIT_NAMESPACE_ENVIRONMENT));
@@ -174,6 +183,11 @@ const char *get_git_dir(void)
 	return git_dir;
 }
 
+const char *get_git_common_dir(void)
+{
+	return git_common_dir;
+}
+
 const char *get_git_namespace(void)
 {
 	if (!namespace)
diff --git a/path.c b/path.c
index 3deb80c..8a6586c 100644
--- a/path.c
+++ b/path.c
@@ -90,6 +90,38 @@ static void replace_dir(struct strbuf *buf, int len, const char *newdir)
 		buf->buf[newlen] = '/';
 }
 
+static const char *common_list[] = {
+	"/branches", "/hooks", "/info", "/logs", "/lost-found", "/modules",
+	"/objects", "/refs", "/remotes", "/rr-cache", "/svn",
+	"config", "gc.pid", "packed-refs", "shallow",
+	NULL
+};
+
+static void update_common_dir(struct strbuf *buf, int git_dir_len)
+{
+	char *base = buf->buf + git_dir_len;
+	const char **p;
+
+	if (is_dir_file(base, "logs", "HEAD"))
+		return;	/* keep this in $GIT_DIR */
+	for (p = common_list; *p; p++) {
+		const char *path = *p;
+		int is_dir = 0;
+		if (*path == '/') {
+			path++;
+			is_dir = 1;
+		}
+		if (is_dir && dir_prefix(base, path)) {
+			replace_dir(buf, git_dir_len, get_git_common_dir());
+			return;
+		}
+		if (!is_dir && !strcmp(base, path)) {
+			replace_dir(buf, git_dir_len, get_git_common_dir());
+			return;
+		}
+	}
+}
+
 static void adjust_git_path(struct strbuf *buf, int git_dir_len)
 {
 	const char *base = buf->buf + git_dir_len;
@@ -101,6 +133,8 @@ static void adjust_git_path(struct strbuf *buf, int git_dir_len)
 			      get_index_file(), strlen(get_index_file()));
 	else if (git_db_env && dir_prefix(base, "objects"))
 		replace_dir(buf, git_dir_len + 7, get_object_directory());
+	else if (git_common_dir_env)
+		update_common_dir(buf, git_dir_len);
 }
 
 static void do_git_path(struct strbuf *buf, const char *fmt, va_list args)
diff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh
index 33d2818..f5d6f80 100755
--- a/t/t0060-path-utils.sh
+++ b/t/t0060-path-utils.sh
@@ -262,5 +262,21 @@ test_expect_success 'setup fake objects directory foo' 'mkdir foo'
 test_git_path GIT_OBJECT_DIRECTORY=foo objects foo
 test_git_path GIT_OBJECT_DIRECTORY=foo objects/foo foo/foo
 test_git_path GIT_OBJECT_DIRECTORY=foo objects2 .git/objects2
+test_expect_success 'setup common repository' 'git --git-dir=bar init'
+test_git_path GIT_COMMON_DIR=bar index                    .git/index
+test_git_path GIT_COMMON_DIR=bar HEAD                     .git/HEAD
+test_git_path GIT_COMMON_DIR=bar logs/HEAD                .git/logs/HEAD
+test_git_path GIT_COMMON_DIR=bar objects                  bar/objects
+test_git_path GIT_COMMON_DIR=bar objects/bar              bar/objects/bar
+test_git_path GIT_COMMON_DIR=bar info/exclude             bar/info/exclude
+test_git_path GIT_COMMON_DIR=bar info/grafts              bar/info/grafts
+test_git_path GIT_COMMON_DIR=bar remotes/bar              bar/remotes/bar
+test_git_path GIT_COMMON_DIR=bar branches/bar             bar/branches/bar
+test_git_path GIT_COMMON_DIR=bar logs/refs/heads/master   bar/logs/refs/heads/master
+test_git_path GIT_COMMON_DIR=bar refs/heads/master        bar/refs/heads/master
+test_git_path GIT_COMMON_DIR=bar hooks/me                 bar/hooks/me
+test_git_path GIT_COMMON_DIR=bar config                   bar/config
+test_git_path GIT_COMMON_DIR=bar packed-refs              bar/packed-refs
+test_git_path GIT_COMMON_DIR=bar shallow                  bar/shallow
 
 test_done
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 12/32] git-sh-setup.sh: use rev-parse --git-path to get $GIT_DIR/objects
  2014-09-10 22:41 ` [PATCH v2 00/32] nd/multiple-work-trees Nguyễn Thái Ngọc Duy
                     ` (10 preceding siblings ...)
  2014-09-10 22:41   ` [PATCH v2 11/32] $GIT_COMMON_DIR: a new environment variable Nguyễn Thái Ngọc Duy
@ 2014-09-10 22:41   ` Nguyễn Thái Ngọc Duy
  2014-09-10 22:41   ` [PATCH v2 13/32] *.sh: avoid hardcoding $GIT_DIR/hooks/ Nguyễn Thái Ngọc Duy
                     ` (20 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-10 22:41 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

If $GIT_COMMON_DIR is set, $GIT_OBJECT_DIRECTORY should be
$GIT_COMMON_DIR/objects, not $GIT_DIR/objects. Just let rev-parse
--git-path handle it.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 git-sh-setup.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git-sh-setup.sh b/git-sh-setup.sh
index 9447980..d3dbb2f 100644
--- a/git-sh-setup.sh
+++ b/git-sh-setup.sh
@@ -345,7 +345,7 @@ then
 		echo >&2 "Unable to determine absolute path of git directory"
 		exit 1
 	}
-	: ${GIT_OBJECT_DIRECTORY="$GIT_DIR/objects"}
+	: ${GIT_OBJECT_DIRECTORY="$(git rev-parse --git-path objects)"}
 fi
 
 peel_committish () {
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 13/32] *.sh: avoid hardcoding $GIT_DIR/hooks/...
  2014-09-10 22:41 ` [PATCH v2 00/32] nd/multiple-work-trees Nguyễn Thái Ngọc Duy
                     ` (11 preceding siblings ...)
  2014-09-10 22:41   ` [PATCH v2 12/32] git-sh-setup.sh: use rev-parse --git-path to get $GIT_DIR/objects Nguyễn Thái Ngọc Duy
@ 2014-09-10 22:41   ` Nguyễn Thái Ngọc Duy
  2014-09-10 22:41   ` [PATCH v2 14/32] git-stash: avoid hardcoding $GIT_DIR/logs/ Nguyễn Thái Ngọc Duy
                     ` (19 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-10 22:41 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

If $GIT_COMMON_DIR is set, it should be $GIT_COMMON_DIR/hooks/, not
$GIT_DIR/hooks/. Just let rev-parse --git-path handle it.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 git-am.sh                              | 22 +++++++++++-----------
 git-rebase--interactive.sh             |  6 +++---
 git-rebase--merge.sh                   |  6 ++----
 git-rebase.sh                          |  4 ++--
 templates/hooks--applypatch-msg.sample |  4 ++--
 templates/hooks--pre-applypatch.sample |  4 ++--
 6 files changed, 22 insertions(+), 24 deletions(-)

diff --git a/git-am.sh b/git-am.sh
index ee61a77..66803d1 100755
--- a/git-am.sh
+++ b/git-am.sh
@@ -810,10 +810,10 @@ To restore the original branch and stop patching run \"\$cmdline --abort\"."
 		continue
 	fi
 
-	if test -x "$GIT_DIR"/hooks/applypatch-msg
+	hook="$(git rev-parse --git-path hooks/applypatch-msg)"
+	if test -x "$hook"
 	then
-		"$GIT_DIR"/hooks/applypatch-msg "$dotest/final-commit" ||
-		stop_here $this
+		"$hook" "$dotest/final-commit" || stop_here $this
 	fi
 
 	if test -f "$dotest/final-commit"
@@ -887,9 +887,10 @@ did you forget to use 'git add'?"
 		stop_here_user_resolve $this
 	fi
 
-	if test -x "$GIT_DIR"/hooks/pre-applypatch
+	hook="$(git rev-parse --git-path hooks/pre-applypatch)"
+	if test -x "$hook"
 	then
-		"$GIT_DIR"/hooks/pre-applypatch || stop_here $this
+		"$hook" || stop_here $this
 	fi
 
 	tree=$(git write-tree) &&
@@ -916,18 +917,17 @@ did you forget to use 'git add'?"
 		echo "$(cat "$dotest/original-commit") $commit" >> "$dotest/rewritten"
 	fi
 
-	if test -x "$GIT_DIR"/hooks/post-applypatch
-	then
-		"$GIT_DIR"/hooks/post-applypatch
-	fi
+	hook="$(git rev-parse --git-path hooks/post-applypatch)"
+	test -x "$hook" && "$hook"
 
 	go_next
 done
 
 if test -s "$dotest"/rewritten; then
     git notes copy --for-rewrite=rebase < "$dotest"/rewritten
-    if test -x "$GIT_DIR"/hooks/post-rewrite; then
-	"$GIT_DIR"/hooks/post-rewrite rebase < "$dotest"/rewritten
+    hook="$(git rev-parse --git-path hooks/post-rewrite)"
+    if test -x "$hook"; then
+	"$hook" rebase < "$dotest"/rewritten
     fi
 fi
 
diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
index 7e1eda0..e8995f9 100644
--- a/git-rebase--interactive.sh
+++ b/git-rebase--interactive.sh
@@ -642,9 +642,9 @@ do_next () {
 		git notes copy --for-rewrite=rebase < "$rewritten_list" ||
 		true # we don't care if this copying failed
 	} &&
-	if test -x "$GIT_DIR"/hooks/post-rewrite &&
-		test -s "$rewritten_list"; then
-		"$GIT_DIR"/hooks/post-rewrite rebase < "$rewritten_list"
+	hook="$(git rev-parse --git-path hooks/post-rewrite)"
+	if test -x "$hook" && test -s "$rewritten_list"; then
+		"$hook" rebase < "$rewritten_list"
 		true # we don't care if this hook failed
 	fi &&
 	warn "Successfully rebased and updated $head_name."
diff --git a/git-rebase--merge.sh b/git-rebase--merge.sh
index d3fb67d..2cc2a6d 100644
--- a/git-rebase--merge.sh
+++ b/git-rebase--merge.sh
@@ -94,10 +94,8 @@ finish_rb_merge () {
 	if test -s "$state_dir"/rewritten
 	then
 		git notes copy --for-rewrite=rebase <"$state_dir"/rewritten
-		if test -x "$GIT_DIR"/hooks/post-rewrite
-		then
-			"$GIT_DIR"/hooks/post-rewrite rebase <"$state_dir"/rewritten
-		fi
+		hook="$(git rev-parse --git-path hooks/post-rewrite)"
+		test -x "$hook" && "$hook" rebase <"$state_dir"/rewritten
 	fi
 	say All done.
 }
diff --git a/git-rebase.sh b/git-rebase.sh
index 06c810b..d60e710 100755
--- a/git-rebase.sh
+++ b/git-rebase.sh
@@ -201,9 +201,9 @@ run_specific_rebase () {
 
 run_pre_rebase_hook () {
 	if test -z "$ok_to_skip_pre_rebase" &&
-	   test -x "$GIT_DIR/hooks/pre-rebase"
+	   test -x "$(git rev-parse --git-path hooks/pre-rebase)"
 	then
-		"$GIT_DIR/hooks/pre-rebase" ${1+"$@"} ||
+		"$(git rev-parse --git-path hooks/pre-rebase)" ${1+"$@"} ||
 		die "$(gettext "The pre-rebase hook refused to rebase.")"
 	fi
 }
diff --git a/templates/hooks--applypatch-msg.sample b/templates/hooks--applypatch-msg.sample
index 8b2a2fe..a5d7b84 100755
--- a/templates/hooks--applypatch-msg.sample
+++ b/templates/hooks--applypatch-msg.sample
@@ -10,6 +10,6 @@
 # To enable this hook, rename this file to "applypatch-msg".
 
 . git-sh-setup
-test -x "$GIT_DIR/hooks/commit-msg" &&
-	exec "$GIT_DIR/hooks/commit-msg" ${1+"$@"}
+commitmsg="$(git rev-parse --git-path hooks/commit-msg)"
+test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"}
 :
diff --git a/templates/hooks--pre-applypatch.sample b/templates/hooks--pre-applypatch.sample
index b1f187c..4142082 100755
--- a/templates/hooks--pre-applypatch.sample
+++ b/templates/hooks--pre-applypatch.sample
@@ -9,6 +9,6 @@
 # To enable this hook, rename this file to "pre-applypatch".
 
 . git-sh-setup
-test -x "$GIT_DIR/hooks/pre-commit" &&
-	exec "$GIT_DIR/hooks/pre-commit" ${1+"$@"}
+precommit="$(git rev-parse --git-path hooks/pre-commit)"
+test -x "$precommit" && exec "$precommit" ${1+"$@"}
 :
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 14/32] git-stash: avoid hardcoding $GIT_DIR/logs/....
  2014-09-10 22:41 ` [PATCH v2 00/32] nd/multiple-work-trees Nguyễn Thái Ngọc Duy
                     ` (12 preceding siblings ...)
  2014-09-10 22:41   ` [PATCH v2 13/32] *.sh: avoid hardcoding $GIT_DIR/hooks/ Nguyễn Thái Ngọc Duy
@ 2014-09-10 22:41   ` Nguyễn Thái Ngọc Duy
  2014-09-10 22:41   ` [PATCH v2 15/32] setup.c: convert is_git_directory() to use strbuf Nguyễn Thái Ngọc Duy
                     ` (18 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-10 22:41 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 git-stash.sh | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/git-stash.sh b/git-stash.sh
index 393e1ec..41f8f6b 100755
--- a/git-stash.sh
+++ b/git-stash.sh
@@ -184,7 +184,7 @@ store_stash () {
 	fi
 
 	# Make sure the reflog for stash is kept.
-	: >>"$GIT_DIR/logs/$ref_stash"
+	: >>"$(git rev-parse --git-path logs/$ref_stash)"
 	git update-ref -m "$stash_msg" $ref_stash $w_commit
 	ret=$?
 	test $ret != 0 && test -z $quiet &&
@@ -259,7 +259,7 @@ save_stash () {
 		say "$(gettext "No local changes to save")"
 		exit 0
 	fi
-	test -f "$GIT_DIR/logs/$ref_stash" ||
+	test -f "$(git rev-parse --git-path logs/$ref_stash)" ||
 		clear_stash || die "$(gettext "Cannot initialize stash")"
 
 	create_stash "$stash_msg" $untracked
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 15/32] setup.c: convert is_git_directory() to use strbuf
  2014-09-10 22:41 ` [PATCH v2 00/32] nd/multiple-work-trees Nguyễn Thái Ngọc Duy
                     ` (13 preceding siblings ...)
  2014-09-10 22:41   ` [PATCH v2 14/32] git-stash: avoid hardcoding $GIT_DIR/logs/ Nguyễn Thái Ngọc Duy
@ 2014-09-10 22:41   ` Nguyễn Thái Ngọc Duy
  2014-09-10 22:41   ` [PATCH v2 16/32] setup.c: detect $GIT_COMMON_DIR in is_git_directory() Nguyễn Thái Ngọc Duy
                     ` (17 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-10 22:41 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 setup.c | 37 +++++++++++++++++++++----------------
 1 file changed, 21 insertions(+), 16 deletions(-)

diff --git a/setup.c b/setup.c
index 0a22f8b..425fd79 100644
--- a/setup.c
+++ b/setup.c
@@ -238,31 +238,36 @@ void verify_non_filename(const char *prefix, const char *arg)
  */
 int is_git_directory(const char *suspect)
 {
-	char path[PATH_MAX];
-	size_t len = strlen(suspect);
+	struct strbuf path = STRBUF_INIT;
+	int ret = 0;
+	size_t len;
 
-	if (PATH_MAX <= len + strlen("/objects"))
-		die("Too long path: %.*s", 60, suspect);
-	strcpy(path, suspect);
+	strbuf_addstr(&path, suspect);
+	len = path.len;
 	if (getenv(DB_ENVIRONMENT)) {
 		if (access(getenv(DB_ENVIRONMENT), X_OK))
-			return 0;
+			goto done;
 	}
 	else {
-		strcpy(path + len, "/objects");
-		if (access(path, X_OK))
-			return 0;
+		strbuf_addstr(&path, "/objects");
+		if (access(path.buf, X_OK))
+			goto done;
 	}
 
-	strcpy(path + len, "/refs");
-	if (access(path, X_OK))
-		return 0;
+	strbuf_setlen(&path, len);
+	strbuf_addstr(&path, "/refs");
+	if (access(path.buf, X_OK))
+		goto done;
 
-	strcpy(path + len, "/HEAD");
-	if (validate_headref(path))
-		return 0;
+	strbuf_setlen(&path, len);
+	strbuf_addstr(&path, "/HEAD");
+	if (validate_headref(path.buf))
+		goto done;
 
-	return 1;
+	ret = 1;
+done:
+	strbuf_release(&path);
+	return ret;
 }
 
 int is_inside_git_dir(void)
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 16/32] setup.c: detect $GIT_COMMON_DIR in is_git_directory()
  2014-09-10 22:41 ` [PATCH v2 00/32] nd/multiple-work-trees Nguyễn Thái Ngọc Duy
                     ` (14 preceding siblings ...)
  2014-09-10 22:41   ` [PATCH v2 15/32] setup.c: convert is_git_directory() to use strbuf Nguyễn Thái Ngọc Duy
@ 2014-09-10 22:41   ` Nguyễn Thái Ngọc Duy
  2014-09-10 22:41   ` [PATCH v2 17/32] setup.c: convert check_repository_format_gently to use strbuf Nguyễn Thái Ngọc Duy
                     ` (16 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-10 22:41 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

If the file "$GIT_DIR/commondir" exists, it contains the value of
$GIT_COMMON_DIR.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 Documentation/gitrepository-layout.txt |  7 ++++++
 setup.c                                | 43 +++++++++++++++++++++++++++++-----
 2 files changed, 44 insertions(+), 6 deletions(-)

diff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt
index b7b9dfb..1cc33f3 100644
--- a/Documentation/gitrepository-layout.txt
+++ b/Documentation/gitrepository-layout.txt
@@ -236,6 +236,13 @@ shallow::
 	file is ignored if $GIT_COMMON_DIR is set and
 	"$GIT_COMMON_DIR/shallow" will be used instead.
 
+commondir::
+	If this file exists, $GIT_COMMON_DIR (see linkgit:git[1]) will
+	be set to the path specified in this file if it is not
+	explicitly set. If the specified path is relative, it is
+	relative to $GIT_DIR. The repository with commondir is
+	incomplete without the repository pointed by "commondir".
+
 modules::
 	Contains the git-repositories of the submodules. This
 	directory is ignored if $GIT_COMMON_DIR is set and
diff --git a/setup.c b/setup.c
index 425fd79..176d505 100644
--- a/setup.c
+++ b/setup.c
@@ -224,6 +224,33 @@ void verify_non_filename(const char *prefix, const char *arg)
 	    "'git <command> [<revision>...] -- [<file>...]'", arg);
 }
 
+static void get_common_dir(struct strbuf *sb, const char *gitdir)
+{
+	struct strbuf data = STRBUF_INIT;
+	struct strbuf path = STRBUF_INIT;
+	const char *git_common_dir = getenv(GIT_COMMON_DIR_ENVIRONMENT);
+	if (git_common_dir) {
+		strbuf_addstr(sb, git_common_dir);
+		return;
+	}
+	strbuf_addf(&path, "%s/commondir", gitdir);
+	if (file_exists(path.buf)) {
+		if (strbuf_read_file(&data, path.buf, 0) <= 0)
+			die_errno(_("failed to read %s"), path.buf);
+		while (data.len && (data.buf[data.len - 1] == '\n' ||
+				    data.buf[data.len - 1] == '\r'))
+			data.len--;
+		data.buf[data.len] = '\0';
+		strbuf_reset(&path);
+		if (!is_absolute_path(data.buf))
+			strbuf_addf(&path, "%s/", gitdir);
+		strbuf_addbuf(&path, &data);
+		strbuf_addstr(sb, real_path(path.buf));
+	} else
+		strbuf_addstr(sb, gitdir);
+	strbuf_release(&data);
+	strbuf_release(&path);
+}
 
 /*
  * Test if it looks like we're at a git directory.
@@ -242,13 +269,22 @@ int is_git_directory(const char *suspect)
 	int ret = 0;
 	size_t len;
 
-	strbuf_addstr(&path, suspect);
+	/* Check worktree-related signatures */
+	strbuf_addf(&path, "%s/HEAD", suspect);
+	if (validate_headref(path.buf))
+		goto done;
+
+	strbuf_reset(&path);
+	get_common_dir(&path, suspect);
 	len = path.len;
+
+	/* Check non-worktree-related signatures */
 	if (getenv(DB_ENVIRONMENT)) {
 		if (access(getenv(DB_ENVIRONMENT), X_OK))
 			goto done;
 	}
 	else {
+		strbuf_setlen(&path, len);
 		strbuf_addstr(&path, "/objects");
 		if (access(path.buf, X_OK))
 			goto done;
@@ -259,11 +295,6 @@ int is_git_directory(const char *suspect)
 	if (access(path.buf, X_OK))
 		goto done;
 
-	strbuf_setlen(&path, len);
-	strbuf_addstr(&path, "/HEAD");
-	if (validate_headref(path.buf))
-		goto done;
-
 	ret = 1;
 done:
 	strbuf_release(&path);
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 17/32] setup.c: convert check_repository_format_gently to use strbuf
  2014-09-10 22:41 ` [PATCH v2 00/32] nd/multiple-work-trees Nguyễn Thái Ngọc Duy
                     ` (15 preceding siblings ...)
  2014-09-10 22:41   ` [PATCH v2 16/32] setup.c: detect $GIT_COMMON_DIR in is_git_directory() Nguyễn Thái Ngọc Duy
@ 2014-09-10 22:41   ` Nguyễn Thái Ngọc Duy
  2014-09-10 22:41   ` [PATCH v2 18/32] setup.c: detect $GIT_COMMON_DIR check_repository_format_gently() Nguyễn Thái Ngọc Duy
                     ` (15 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-10 22:41 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 setup.c | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)

diff --git a/setup.c b/setup.c
index 176d505..a17389f 100644
--- a/setup.c
+++ b/setup.c
@@ -342,7 +342,9 @@ void setup_work_tree(void)
 
 static int check_repository_format_gently(const char *gitdir, int *nongit_ok)
 {
-	char repo_config[PATH_MAX+1];
+	struct strbuf sb = STRBUF_INIT;
+	const char *repo_config;
+	int ret = 0;
 
 	/*
 	 * git_config() can't be used here because it calls git_pathdup()
@@ -353,7 +355,8 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)
 	 * Use a gentler version of git_config() to check if this repo
 	 * is a good one.
 	 */
-	snprintf(repo_config, PATH_MAX, "%s/config", gitdir);
+	strbuf_addf(&sb, "%s/config", gitdir);
+	repo_config = sb.buf;
 	git_config_early(check_repository_format_version, NULL, repo_config);
 	if (GIT_REPO_VERSION < repository_format_version) {
 		if (!nongit_ok)
@@ -363,9 +366,10 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)
 			GIT_REPO_VERSION, repository_format_version);
 		warning("Please upgrade Git");
 		*nongit_ok = -1;
-		return -1;
+		ret = -1;
 	}
-	return 0;
+	strbuf_release(&sb);
+	return ret;
 }
 
 /*
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 18/32] setup.c: detect $GIT_COMMON_DIR check_repository_format_gently()
  2014-09-10 22:41 ` [PATCH v2 00/32] nd/multiple-work-trees Nguyễn Thái Ngọc Duy
                     ` (16 preceding siblings ...)
  2014-09-10 22:41   ` [PATCH v2 17/32] setup.c: convert check_repository_format_gently to use strbuf Nguyễn Thái Ngọc Duy
@ 2014-09-10 22:41   ` Nguyễn Thái Ngọc Duy
  2014-09-10 22:41   ` [PATCH v2 19/32] setup.c: support multi-checkout repo setup Nguyễn Thái Ngọc Duy
                     ` (14 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-10 22:41 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 setup.c | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/setup.c b/setup.c
index a17389f..79f79f2 100644
--- a/setup.c
+++ b/setup.c
@@ -346,6 +346,10 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)
 	const char *repo_config;
 	int ret = 0;
 
+	get_common_dir(&sb, gitdir);
+	strbuf_addstr(&sb, "/config");
+	repo_config = sb.buf;
+
 	/*
 	 * git_config() can't be used here because it calls git_pathdup()
 	 * to get $GIT_CONFIG/config. That call will make setup_git_env()
@@ -355,8 +359,6 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)
 	 * Use a gentler version of git_config() to check if this repo
 	 * is a good one.
 	 */
-	strbuf_addf(&sb, "%s/config", gitdir);
-	repo_config = sb.buf;
 	git_config_early(check_repository_format_version, NULL, repo_config);
 	if (GIT_REPO_VERSION < repository_format_version) {
 		if (!nongit_ok)
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 19/32] setup.c: support multi-checkout repo setup
  2014-09-10 22:41 ` [PATCH v2 00/32] nd/multiple-work-trees Nguyễn Thái Ngọc Duy
                     ` (17 preceding siblings ...)
  2014-09-10 22:41   ` [PATCH v2 18/32] setup.c: detect $GIT_COMMON_DIR check_repository_format_gently() Nguyễn Thái Ngọc Duy
@ 2014-09-10 22:41   ` Nguyễn Thái Ngọc Duy
  2014-09-10 22:41   ` [PATCH v2 20/32] wrapper.c: wrapper to open a file, fprintf then close Nguyễn Thái Ngọc Duy
                     ` (13 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-10 22:41 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

The repo setup procedure is updated to detect $GIT_DIR/commondir and
set $GIT_COMMON_DIR properly.

The core.worktree is ignored when $GIT_COMMON_DIR is set. This is
because the config file is shared in multi-checkout setup, but
checkout directories _are_ different. Making core.worktree effective
in all checkouts mean it's back to a single checkout.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 Documentation/config.txt        |  2 ++
 Documentation/git-rev-parse.txt |  3 ++
 builtin/rev-parse.c             |  4 +++
 cache.h                         |  1 +
 environment.c                   |  8 ++---
 setup.c                         | 33 +++++++++++++-----
 t/t1501-worktree.sh             | 76 +++++++++++++++++++++++++++++++++++++++++
 t/t1510-repo-setup.sh           |  1 +
 trace.c                         |  1 +
 9 files changed, 115 insertions(+), 14 deletions(-)

diff --git a/Documentation/config.txt b/Documentation/config.txt
index 1d718bd..286e539 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -380,6 +380,8 @@ false), while all other repositories are assumed to be bare (bare
 
 core.worktree::
 	Set the path to the root of the working tree.
+	If GIT_COMMON_DIR environment variable is set, core.worktree
+	is ignored and not used for determining the root of working tree.
 	This can be overridden by the GIT_WORK_TREE environment
 	variable and the '--work-tree' command-line option.
 	The value can be an absolute path or relative to the path to
diff --git a/Documentation/git-rev-parse.txt b/Documentation/git-rev-parse.txt
index 9465399..f1867d3 100644
--- a/Documentation/git-rev-parse.txt
+++ b/Documentation/git-rev-parse.txt
@@ -215,6 +215,9 @@ If `$GIT_DIR` is not defined and the current directory
 is not detected to lie in a Git repository or work tree
 print a message to stderr and exit with nonzero status.
 
+--git-common-dir::
+	Show `$GIT_COMMON_DIR` if defined, else `$GIT_DIR`.
+
 --is-inside-git-dir::
 	When the current working directory is below the repository
 	directory print "true", otherwise "false".
diff --git a/builtin/rev-parse.c b/builtin/rev-parse.c
index 7606d43..29475c5 100644
--- a/builtin/rev-parse.c
+++ b/builtin/rev-parse.c
@@ -757,6 +757,10 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)
 				printf("%s%s.git\n", cwd, len && cwd[len-1] != '/' ? "/" : "");
 				continue;
 			}
+			if (!strcmp(arg, "--git-common-dir")) {
+				puts(get_git_common_dir());
+				continue;
+			}
 			if (!strcmp(arg, "--resolve-git-dir")) {
 				const char *gitdir = argv[++i];
 				if (!gitdir)
diff --git a/cache.h b/cache.h
index b606ee4..e3ff7dc 100644
--- a/cache.h
+++ b/cache.h
@@ -422,6 +422,7 @@ extern char *get_object_directory(void);
 extern char *get_index_file(void);
 extern char *get_graft_file(void);
 extern int set_git_dir(const char *path);
+extern int get_common_dir(struct strbuf *sb, const char *gitdir);
 extern const char *get_git_namespace(void);
 extern const char *strip_namespace(const char *namespaced_ref);
 extern const char *get_git_work_tree(void);
diff --git a/environment.c b/environment.c
index 78a07e4..d5b0788 100644
--- a/environment.c
+++ b/environment.c
@@ -141,6 +141,7 @@ static char *git_path_from_env(const char *envvar, const char *git_dir,
 
 static void setup_git_env(void)
 {
+	struct strbuf sb = STRBUF_INIT;
 	const char *gitfile;
 	const char *shallow_file;
 
@@ -149,12 +150,9 @@ static void setup_git_env(void)
 		git_dir = DEFAULT_GIT_DIR_ENVIRONMENT;
 	gitfile = read_gitfile(git_dir);
 	git_dir = xstrdup(gitfile ? gitfile : git_dir);
-	git_common_dir = getenv(GIT_COMMON_DIR_ENVIRONMENT);
-	if (git_common_dir) {
+	if (get_common_dir(&sb, git_dir))
 		git_common_dir_env = 1;
-		git_common_dir = xstrdup(git_common_dir);
-	} else
-		git_common_dir = git_dir;
+	git_common_dir = strbuf_detach(&sb, NULL);
 	git_object_dir = git_path_from_env(DB_ENVIRONMENT, git_common_dir,
 					   "objects", &git_db_env);
 	git_index_file = git_path_from_env(INDEX_ENVIRONMENT, git_dir,
diff --git a/setup.c b/setup.c
index 79f79f2..8f90bc3 100644
--- a/setup.c
+++ b/setup.c
@@ -224,14 +224,15 @@ void verify_non_filename(const char *prefix, const char *arg)
 	    "'git <command> [<revision>...] -- [<file>...]'", arg);
 }
 
-static void get_common_dir(struct strbuf *sb, const char *gitdir)
+int get_common_dir(struct strbuf *sb, const char *gitdir)
 {
 	struct strbuf data = STRBUF_INIT;
 	struct strbuf path = STRBUF_INIT;
 	const char *git_common_dir = getenv(GIT_COMMON_DIR_ENVIRONMENT);
+	int ret = 0;
 	if (git_common_dir) {
 		strbuf_addstr(sb, git_common_dir);
-		return;
+		return 1;
 	}
 	strbuf_addf(&path, "%s/commondir", gitdir);
 	if (file_exists(path.buf)) {
@@ -246,10 +247,12 @@ static void get_common_dir(struct strbuf *sb, const char *gitdir)
 			strbuf_addf(&path, "%s/", gitdir);
 		strbuf_addbuf(&path, &data);
 		strbuf_addstr(sb, real_path(path.buf));
+		ret = 1;
 	} else
 		strbuf_addstr(sb, gitdir);
 	strbuf_release(&data);
 	strbuf_release(&path);
+	return ret;
 }
 
 /*
@@ -340,13 +343,26 @@ void setup_work_tree(void)
 	initialized = 1;
 }
 
+static int check_repo_format(const char *var, const char *value, void *cb)
+{
+	if (strcmp(var, "core.repositoryformatversion") == 0)
+		repository_format_version = git_config_int(var, value);
+	else if (strcmp(var, "core.sharedrepository") == 0)
+		shared_repository = git_config_perm(var, value);
+	return 0;
+}
+
 static int check_repository_format_gently(const char *gitdir, int *nongit_ok)
 {
 	struct strbuf sb = STRBUF_INIT;
 	const char *repo_config;
+	config_fn_t fn;
 	int ret = 0;
 
-	get_common_dir(&sb, gitdir);
+	if (get_common_dir(&sb, gitdir))
+		fn = check_repo_format;
+	else
+		fn = check_repository_format_version;
 	strbuf_addstr(&sb, "/config");
 	repo_config = sb.buf;
 
@@ -359,7 +375,7 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)
 	 * Use a gentler version of git_config() to check if this repo
 	 * is a good one.
 	 */
-	git_config_early(check_repository_format_version, NULL, repo_config);
+	git_config_early(fn, NULL, repo_config);
 	if (GIT_REPO_VERSION < repository_format_version) {
 		if (!nongit_ok)
 			die ("Expected git repo version <= %d, found %d",
@@ -831,11 +847,10 @@ int git_config_perm(const char *var, const char *value)
 
 int check_repository_format_version(const char *var, const char *value, void *cb)
 {
-	if (strcmp(var, "core.repositoryformatversion") == 0)
-		repository_format_version = git_config_int(var, value);
-	else if (strcmp(var, "core.sharedrepository") == 0)
-		shared_repository = git_config_perm(var, value);
-	else if (strcmp(var, "core.bare") == 0) {
+	int ret = check_repo_format(var, value, cb);
+	if (ret)
+		return ret;
+	if (strcmp(var, "core.bare") == 0) {
 		is_bare_repository_cfg = git_config_bool(var, value);
 		if (is_bare_repository_cfg == 1)
 			inside_work_tree = -1;
diff --git a/t/t1501-worktree.sh b/t/t1501-worktree.sh
index 8f36aa9..e6ac7a4 100755
--- a/t/t1501-worktree.sh
+++ b/t/t1501-worktree.sh
@@ -346,4 +346,80 @@ test_expect_success 'relative $GIT_WORK_TREE and git subprocesses' '
 	test_cmp expected actual
 '
 
+test_expect_success 'Multi-worktree setup' '
+	mkdir work &&
+	mkdir -p repo.git/repos/foo &&
+	cp repo.git/HEAD repo.git/index repo.git/repos/foo &&
+	sane_unset GIT_DIR GIT_CONFIG GIT_WORK_TREE
+'
+
+test_expect_success 'GIT_DIR set (1)' '
+	echo "gitdir: repo.git/repos/foo" >gitfile &&
+	echo ../.. >repo.git/repos/foo/commondir &&
+	(
+		cd work &&
+		GIT_DIR=../gitfile git rev-parse --git-common-dir >actual &&
+		test-path-utils real_path "$TRASH_DIRECTORY/repo.git" >expect &&
+		test_cmp expect actual
+	)
+'
+
+test_expect_success 'GIT_DIR set (2)' '
+	echo "gitdir: repo.git/repos/foo" >gitfile &&
+	echo "$TRASH_DIRECTORY/repo.git" >repo.git/repos/foo/commondir &&
+	(
+		cd work &&
+		GIT_DIR=../gitfile git rev-parse --git-common-dir >actual &&
+		test-path-utils real_path "$TRASH_DIRECTORY/repo.git" >expect &&
+		test_cmp expect actual
+	)
+'
+
+test_expect_success 'Auto discovery' '
+	echo "gitdir: repo.git/repos/foo" >.git &&
+	echo ../.. >repo.git/repos/foo/commondir &&
+	(
+		cd work &&
+		git rev-parse --git-common-dir >actual &&
+		test-path-utils real_path "$TRASH_DIRECTORY/repo.git" >expect &&
+		test_cmp expect actual &&
+		echo haha >data1 &&
+		git add data1 &&
+		git ls-files --full-name :/ | grep data1 >actual &&
+		echo work/data1 >expect &&
+		test_cmp expect actual
+	)
+'
+
+test_expect_success '$GIT_DIR/common overrides core.worktree' '
+	mkdir elsewhere &&
+	git --git-dir=repo.git config core.worktree "$TRASH_DIRECTORY/elsewhere" &&
+	echo "gitdir: repo.git/repos/foo" >.git &&
+	echo ../.. >repo.git/repos/foo/commondir &&
+	(
+		cd work &&
+		git rev-parse --git-common-dir >actual &&
+		test-path-utils real_path "$TRASH_DIRECTORY/repo.git" >expect &&
+		test_cmp expect actual &&
+		echo haha >data2 &&
+		git add data2 &&
+		git ls-files --full-name :/ | grep data2 >actual &&
+		echo work/data2 >expect &&
+		test_cmp expect actual
+	)
+'
+
+test_expect_success '$GIT_WORK_TREE overrides $GIT_DIR/common' '
+	echo "gitdir: repo.git/repos/foo" >.git &&
+	echo ../.. >repo.git/repos/foo/commondir &&
+	(
+		cd work &&
+		echo haha >data3 &&
+		git --git-dir=../.git --work-tree=. add data3 &&
+		git ls-files --full-name -- :/ | grep data3 >actual &&
+		echo data3 >expect &&
+		test_cmp expect actual
+	)
+'
+
 test_done
diff --git a/t/t1510-repo-setup.sh b/t/t1510-repo-setup.sh
index e1b2a99..33c1a58 100755
--- a/t/t1510-repo-setup.sh
+++ b/t/t1510-repo-setup.sh
@@ -106,6 +106,7 @@ setup_env () {
 expect () {
 	cat >"$1/expected" <<-EOF
 	setup: git_dir: $2
+	setup: git_common_dir: $2
 	setup: worktree: $3
 	setup: cwd: $4
 	setup: prefix: $5
diff --git a/trace.c b/trace.c
index 08180a9..a594761 100644
--- a/trace.c
+++ b/trace.c
@@ -173,6 +173,7 @@ void trace_repo_setup(const char *prefix)
 		prefix = "(null)";
 
 	trace_printf_key(key, "setup: git_dir: %s\n", quote_crnl(get_git_dir()));
+	trace_printf_key(key, "setup: git_common_dir: %s\n", quote_crnl(get_git_common_dir()));
 	trace_printf_key(key, "setup: worktree: %s\n", quote_crnl(git_work_tree));
 	trace_printf_key(key, "setup: cwd: %s\n", quote_crnl(cwd));
 	trace_printf_key(key, "setup: prefix: %s\n", quote_crnl(prefix));
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 20/32] wrapper.c: wrapper to open a file, fprintf then close
  2014-09-10 22:41 ` [PATCH v2 00/32] nd/multiple-work-trees Nguyễn Thái Ngọc Duy
                     ` (18 preceding siblings ...)
  2014-09-10 22:41   ` [PATCH v2 19/32] setup.c: support multi-checkout repo setup Nguyễn Thái Ngọc Duy
@ 2014-09-10 22:41   ` Nguyễn Thái Ngọc Duy
  2014-09-10 22:41   ` [PATCH v2 21/32] use new wrapper write_file() for simple file writing Nguyễn Thái Ngọc Duy
                     ` (12 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-10 22:41 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 cache.h   |  2 ++
 wrapper.c | 31 +++++++++++++++++++++++++++++++
 2 files changed, 33 insertions(+)

diff --git a/cache.h b/cache.h
index e3ff7dc..bf4d15e 100644
--- a/cache.h
+++ b/cache.h
@@ -1359,6 +1359,8 @@ static inline ssize_t write_str_in_full(int fd, const char *str)
 {
 	return write_in_full(fd, str, strlen(str));
 }
+__attribute__((format (printf,3,4)))
+extern int write_file(const char *path, int fatal, const char *fmt, ...);
 
 /* pager.c */
 extern void setup_pager(void);
diff --git a/wrapper.c b/wrapper.c
index bc1bfb8..9d7b9ac 100644
--- a/wrapper.c
+++ b/wrapper.c
@@ -493,3 +493,34 @@ struct passwd *xgetpwuid_self(void)
 		    errno ? strerror(errno) : _("no such user"));
 	return pw;
 }
+
+int write_file(const char *path, int fatal, const char *fmt, ...)
+{
+	struct strbuf sb = STRBUF_INIT;
+	va_list params;
+	int fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0666);
+	if (fd < 0) {
+		if (fatal)
+			die_errno(_("could not open %s for writing"), path);
+		return -1;
+	}
+	va_start(params, fmt);
+	strbuf_vaddf(&sb, fmt, params);
+	va_end(params);
+	if (write_in_full(fd, sb.buf, sb.len) != sb.len) {
+		int err = errno;
+		close(fd);
+		strbuf_release(&sb);
+		errno = err;
+		if (fatal)
+			die_errno(_("could not write to %s"), path);
+		return -1;
+	}
+	strbuf_release(&sb);
+	if (close(fd)) {
+		if (fatal)
+			die_errno(_("could not close %s"), path);
+		return -1;
+	}
+	return 0;
+}
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 21/32] use new wrapper write_file() for simple file writing
  2014-09-10 22:41 ` [PATCH v2 00/32] nd/multiple-work-trees Nguyễn Thái Ngọc Duy
                     ` (19 preceding siblings ...)
  2014-09-10 22:41   ` [PATCH v2 20/32] wrapper.c: wrapper to open a file, fprintf then close Nguyễn Thái Ngọc Duy
@ 2014-09-10 22:41   ` Nguyễn Thái Ngọc Duy
  2014-09-10 22:41   ` [PATCH v2 22/32] checkout: support checking out into a new working directory Nguyễn Thái Ngọc Duy
                     ` (11 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-10 22:41 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

This fixes common problems in these code about error handling,
forgetting to close the file handle after fprintf() fails, or not
printing out the error string..

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 builtin/branch.c  |  4 +---
 builtin/init-db.c |  7 +------
 daemon.c          | 11 +----------
 submodule.c       |  9 ++-------
 transport.c       |  8 +++-----
 5 files changed, 8 insertions(+), 31 deletions(-)

diff --git a/builtin/branch.c b/builtin/branch.c
index 0591b22..e4265a1 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -754,7 +754,6 @@ static const char edit_description[] = "BRANCH_DESCRIPTION";
 
 static int edit_branch_description(const char *branch_name)
 {
-	FILE *fp;
 	int status;
 	struct strbuf buf = STRBUF_INIT;
 	struct strbuf name = STRBUF_INIT;
@@ -767,8 +766,7 @@ static int edit_branch_description(const char *branch_name)
 		    "  %s\n"
 		    "Lines starting with '%c' will be stripped.\n",
 		    branch_name, comment_line_char);
-	fp = fopen(git_path(edit_description), "w");
-	if ((fwrite(buf.buf, 1, buf.len, fp) < buf.len) || fclose(fp)) {
+	if (write_file(git_path(edit_description), 0, "%s", buf.buf)) {
 		strbuf_release(&buf);
 		return error(_("could not write branch description template: %s"),
 			     strerror(errno));
diff --git a/builtin/init-db.c b/builtin/init-db.c
index 56f85e2..ce8416a 100644
--- a/builtin/init-db.c
+++ b/builtin/init-db.c
@@ -342,7 +342,6 @@ int set_git_dir_init(const char *git_dir, const char *real_git_dir,
 static void separate_git_dir(const char *git_dir)
 {
 	struct stat st;
-	FILE *fp;
 
 	if (!stat(git_link, &st)) {
 		const char *src;
@@ -358,11 +357,7 @@ static void separate_git_dir(const char *git_dir)
 			die_errno(_("unable to move %s to %s"), src, git_dir);
 	}
 
-	fp = fopen(git_link, "w");
-	if (!fp)
-		die(_("Could not create git link %s"), git_link);
-	fprintf(fp, "gitdir: %s\n", git_dir);
-	fclose(fp);
+	write_file(git_link, 1, "gitdir: %s\n", git_dir);
 }
 
 int init_db(const char *template_dir, unsigned int flags)
diff --git a/daemon.c b/daemon.c
index 1eb6631..dd638a9 100644
--- a/daemon.c
+++ b/daemon.c
@@ -1094,15 +1094,6 @@ static struct credentials *prepare_credentials(const char *user_name,
 }
 #endif
 
-static void store_pid(const char *path)
-{
-	FILE *f = fopen(path, "w");
-	if (!f)
-		die_errno("cannot open pid file '%s'", path);
-	if (fprintf(f, "%"PRIuMAX"\n", (uintmax_t) getpid()) < 0 || fclose(f) != 0)
-		die_errno("failed to write pid file '%s'", path);
-}
-
 static int serve(struct string_list *listen_addr, int listen_port,
     struct credentials *cred)
 {
@@ -1313,7 +1304,7 @@ int main(int argc, char **argv)
 		sanitize_stdfds();
 
 	if (pid_file)
-		store_pid(pid_file);
+		write_file(pid_file, 1, "%"PRIuMAX"\n", (uintmax_t) getpid());
 
 	/* prepare argv for serving-processes */
 	cld_argv = xmalloc(sizeof (char *) * (argc + 2));
diff --git a/submodule.c b/submodule.c
index b80ecac..b7b6059 100644
--- a/submodule.c
+++ b/submodule.c
@@ -1112,16 +1112,11 @@ void connect_work_tree_and_git_dir(const char *work_tree, const char *git_dir)
 	struct strbuf file_name = STRBUF_INIT;
 	struct strbuf rel_path = STRBUF_INIT;
 	const char *real_work_tree = xstrdup(real_path(work_tree));
-	FILE *fp;
 
 	/* Update gitfile */
 	strbuf_addf(&file_name, "%s/.git", work_tree);
-	fp = fopen(file_name.buf, "w");
-	if (!fp)
-		die(_("Could not create git link %s"), file_name.buf);
-	fprintf(fp, "gitdir: %s\n", relative_path(git_dir, real_work_tree,
-						  &rel_path));
-	fclose(fp);
+	write_file(file_name.buf, 1, "gitdir: %s\n",
+		   relative_path(git_dir, real_work_tree, &rel_path));
 
 	/* Update core.worktree setting */
 	strbuf_reset(&file_name);
diff --git a/transport.c b/transport.c
index 59c9727..1b779bb 100644
--- a/transport.c
+++ b/transport.c
@@ -296,7 +296,6 @@ static int write_one_ref(const char *name, const unsigned char *sha1,
 {
 	struct strbuf *buf = data;
 	int len = buf->len;
-	FILE *f;
 
 	/* when called via for_each_ref(), flags is non-zero */
 	if (flags && !starts_with(name, "refs/heads/") &&
@@ -305,10 +304,9 @@ static int write_one_ref(const char *name, const unsigned char *sha1,
 
 	strbuf_addstr(buf, name);
 	if (safe_create_leading_directories(buf->buf) ||
-			!(f = fopen(buf->buf, "w")) ||
-			fprintf(f, "%s\n", sha1_to_hex(sha1)) < 0 ||
-			fclose(f))
-		return error("problems writing temporary file %s", buf->buf);
+	    write_file(buf->buf, 0, "%s\n", sha1_to_hex(sha1)))
+		return error("problems writing temporary file %s: %s",
+			     buf->buf, strerror(errno));
 	strbuf_setlen(buf, len);
 	return 0;
 }
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 22/32] checkout: support checking out into a new working directory
  2014-09-10 22:41 ` [PATCH v2 00/32] nd/multiple-work-trees Nguyễn Thái Ngọc Duy
                     ` (20 preceding siblings ...)
  2014-09-10 22:41   ` [PATCH v2 21/32] use new wrapper write_file() for simple file writing Nguyễn Thái Ngọc Duy
@ 2014-09-10 22:41   ` Nguyễn Thái Ngọc Duy
  2014-09-11 15:02     ` Marc Branchaud
  2014-09-10 22:41   ` [PATCH v2 23/32] prune: strategies for linked checkouts Nguyễn Thái Ngọc Duy
                     ` (10 subsequent siblings)
  32 siblings, 1 reply; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-10 22:41 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

"git checkout --to" sets up a new working directory with a .git file
pointing to $GIT_DIR/worktrees/<id>. It then executes "git checkout"
again on the new worktree with the same arguments except "--to" is
taken out. The second checkout execution, which is not contaminated
with any info from the current repository, will actually check out and
everything that normal "git checkout" does.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
---
 Documentation/git-checkout.txt         | 43 +++++++++++++++
 Documentation/git.txt                  |  3 +-
 Documentation/gitrepository-layout.txt |  7 +++
 builtin/checkout.c                     | 95 +++++++++++++++++++++++++++++++++-
 path.c                                 |  2 +-
 t/t2025-checkout-to.sh (new +x)        | 63 ++++++++++++++++++++++
 6 files changed, 209 insertions(+), 4 deletions(-)
 create mode 100755 t/t2025-checkout-to.sh

diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt
index 33ad2ad..bd0fc1d 100644
--- a/Documentation/git-checkout.txt
+++ b/Documentation/git-checkout.txt
@@ -225,6 +225,13 @@ This means that you can use `git checkout -p` to selectively discard
 edits from your current working tree. See the ``Interactive Mode''
 section of linkgit:git-add[1] to learn how to operate the `--patch` mode.
 
+--to=<path>::
+	Check out a branch in a separate working directory at
+	`<path>`. A new working directory is linked to the current
+	repository, sharing everything except working directory
+	specific files such as HEAD, index... See "MULTIPLE CHECKOUT
+	MODE" section for more information.
+
 <branch>::
 	Branch to checkout; if it refers to a branch (i.e., a name that,
 	when prepended with "refs/heads/", is a valid ref), then that
@@ -388,6 +395,42 @@ $ git reflog -2 HEAD # or
 $ git log -g -2 HEAD
 ------------
 
+MULTIPLE CHECKOUT MODE
+----------------------
+Normally a working directory is attached to repository. When "git
+checkout --to" is used, a new working directory is attached to the
+current repository. This new working directory is called "linked
+checkout" as compared to the "main checkout" prepared by "git init" or
+"git clone". A repository has one main checkout and zero or more
+linked checkouts.
+
+Each linked checkout has a private directory in $GIT_DIR/worktrees in
+the main checkout, usually named after the base name of the new
+working directory, optionally with a number added to make it
+unique. For example, the command `git checkout --to ../test-next next`
+running with $GIT_DIR=/path/main may create the directory
+`$GIT_DIR/worktrees/test-next` (or `$GIT_DIR/worktrees/test-next1` if
+`test-next` is already taken).
+
+Within a linked checkout, $GIT_DIR is set to point to this private
+directory (e.g. `/path/main/worktrees/test-next` in the example) and
+$GIT_COMMON_DIR is set to point back to the main checkout's $GIT_DIR
+(e.g. `/path/main`). Setting is done via a .git file located at the
+top directory of the linked checkout.
+
+Path resolution via `git rev-parse --git-path` would use either
+$GIT_DIR or $GIT_COMMON_DIR depending on the path. For example, the
+linked checkout's `$GIT_DIR/HEAD` resolve to
+`/path/main/worktrees/test-next/HEAD` (not `/path/main/HEAD` which is
+the main checkout's HEAD) while `$GIT_DIR/refs/heads/master` would use
+$GIT_COMMON_DIR and resolve to `/path/main/refs/heads/master`, which
+is shared across checkouts.
+
+See linkgit:gitrepository-layout[5] for more information. The rule of
+thumb is do not make any assumption about whether a path belongs to
+$GIT_DIR or $GIT_COMMON_DIR when you need to directly access something
+inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.
+
 EXAMPLES
 --------
 
diff --git a/Documentation/git.txt b/Documentation/git.txt
index 749052f..c0a4940 100644
--- a/Documentation/git.txt
+++ b/Documentation/git.txt
@@ -792,7 +792,8 @@ Git so take care if using Cogito etc.
 	If this variable is set to a path, non-worktree files that are
 	normally in $GIT_DIR will be taken from this path
 	instead. Worktree-specific files such as HEAD or index are
-	taken from $GIT_DIR. See linkgit:gitrepository-layout[5] for
+	taken from $GIT_DIR. See linkgit:gitrepository-layout[5] and
+	the section 'MULTIPLE CHECKOUT MODE' in linkgit:checkout[1]
 	details. This variable has lower precedence than other path
 	variables such as GIT_INDEX_FILE, GIT_OBJECT_DIRECTORY...
 
diff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt
index 1cc33f3..a82780c 100644
--- a/Documentation/gitrepository-layout.txt
+++ b/Documentation/gitrepository-layout.txt
@@ -248,6 +248,13 @@ modules::
 	directory is ignored if $GIT_COMMON_DIR is set and
 	"$GIT_COMMON_DIR/modules" will be used instead.
 
+worktrees::
+	Contains worktree specific information of linked
+	checkouts. Each subdirectory contains the worktree-related
+	part of a linked checkout. This directory is ignored if
+	$GIT_COMMON_DIR is set and "$GIT_COMMON_DIR/worktrees" will be
+	used instead.
+
 SEE ALSO
 --------
 linkgit:git-init[1],
diff --git a/builtin/checkout.c b/builtin/checkout.c
index 8023987..1868f69 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -48,6 +48,10 @@ struct checkout_opts {
 	const char *prefix;
 	struct pathspec pathspec;
 	struct tree *source_tree;
+
+	const char *new_worktree;
+	const char **saved_argv;
+	int new_worktree_mode;
 };
 
 static int post_checkout_hook(struct commit *old, struct commit *new,
@@ -250,6 +254,9 @@ static int checkout_paths(const struct checkout_opts *opts,
 		die(_("Cannot update paths and switch to branch '%s' at the same time."),
 		    opts->new_branch);
 
+	if (opts->new_worktree)
+		die(_("'%s' cannot be used with updating paths"), "--to");
+
 	if (opts->patch_mode)
 		return run_add_interactive(revision, "--patch=checkout",
 					   &opts->pathspec);
@@ -485,7 +492,7 @@ static int merge_working_tree(const struct checkout_opts *opts,
 			topts.dir->flags |= DIR_SHOW_IGNORED;
 			setup_standard_excludes(topts.dir);
 		}
-		tree = parse_tree_indirect(old->commit ?
+		tree = parse_tree_indirect(old->commit && !opts->new_worktree_mode ?
 					   old->commit->object.sha1 :
 					   EMPTY_TREE_SHA1_BIN);
 		init_tree_desc(&trees[0], tree->buffer, tree->size);
@@ -796,7 +803,8 @@ static int switch_branches(const struct checkout_opts *opts,
 		return ret;
 	}
 
-	if (!opts->quiet && !old.path && old.commit && new->commit != old.commit)
+	if (!opts->quiet && !old.path && old.commit &&
+	    new->commit != old.commit && !opts->new_worktree_mode)
 		orphaned_commit_warning(old.commit, new->commit);
 
 	update_refs_for_switch(opts, &old, new);
@@ -806,6 +814,76 @@ static int switch_branches(const struct checkout_opts *opts,
 	return ret || writeout_error;
 }
 
+static int prepare_linked_checkout(const struct checkout_opts *opts,
+				   struct branch_info *new)
+{
+	struct strbuf sb_git = STRBUF_INIT, sb_repo = STRBUF_INIT;
+	struct strbuf sb = STRBUF_INIT;
+	const char *path = opts->new_worktree, *name;
+	struct stat st;
+	struct child_process cp;
+	int counter = 0, len;
+
+	if (!new->commit)
+		die(_("no branch specified"));
+	if (file_exists(path))
+		die(_("'%s' already exists"), path);
+
+	len = strlen(path);
+	while (len && is_dir_sep(path[len - 1]))
+		len--;
+
+	for (name = path + len - 1; name > path; name--)
+		if (is_dir_sep(*name)) {
+			name++;
+			break;
+		}
+	strbuf_addstr(&sb_repo,
+		      git_path("worktrees/%.*s", (int)(path + len - name), name));
+	len = sb_repo.len;
+	if (safe_create_leading_directories_const(sb_repo.buf))
+		die_errno(_("could not create leading directories of '%s'"),
+			  sb_repo.buf);
+	while (!stat(sb_repo.buf, &st)) {
+		counter++;
+		strbuf_setlen(&sb_repo, len);
+		strbuf_addf(&sb_repo, "%d", counter);
+	}
+	name = strrchr(sb_repo.buf, '/') + 1;
+	if (mkdir(sb_repo.buf, 0777))
+		die_errno(_("could not create directory of '%s'"), sb_repo.buf);
+
+	strbuf_addf(&sb_git, "%s/.git", path);
+	if (safe_create_leading_directories_const(sb_git.buf))
+		die_errno(_("could not create leading directories of '%s'"),
+			  sb_git.buf);
+
+	write_file(sb_git.buf, 1, "gitdir: %s/worktrees/%s\n",
+		   real_path(get_git_common_dir()), name);
+	/*
+	 * This is to keep resolve_ref() happy. We need a valid HEAD
+	 * or is_git_directory() will reject the directory. Any valid
+	 * value would do because this value will be ignored and
+	 * replaced at the next (real) checkout.
+	 */
+	strbuf_addf(&sb, "%s/HEAD", sb_repo.buf);
+	write_file(sb.buf, 1, "%s\n", sha1_to_hex(new->commit->object.sha1));
+	strbuf_reset(&sb);
+	strbuf_addf(&sb, "%s/commondir", sb_repo.buf);
+	write_file(sb.buf, 1, "../..\n");
+
+	if (!opts->quiet)
+		fprintf_ln(stderr, _("Enter %s (identifier %s)"), path, name);
+
+	setenv("GIT_CHECKOUT_NEW_WORKTREE", "1", 1);
+	setenv(GIT_DIR_ENVIRONMENT, sb_git.buf, 1);
+	setenv(GIT_WORK_TREE_ENVIRONMENT, path, 1);
+	memset(&cp, 0, sizeof(cp));
+	cp.git_cmd = 1;
+	cp.argv = opts->saved_argv;
+	return run_command(&cp);
+}
+
 static int git_checkout_config(const char *var, const char *value, void *cb)
 {
 	if (!strcmp(var, "diff.ignoresubmodules")) {
@@ -1067,6 +1145,9 @@ static int checkout_branch(struct checkout_opts *opts,
 		die(_("Cannot switch branch to a non-commit '%s'"),
 		    new->name);
 
+	if (opts->new_worktree)
+		return prepare_linked_checkout(opts, new);
+
 	if (!new->commit && opts->new_branch) {
 		unsigned char rev[20];
 		int flag;
@@ -1109,6 +1190,8 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
 			 N_("do not limit pathspecs to sparse entries only")),
 		OPT_HIDDEN_BOOL(0, "guess", &dwim_new_local_branch,
 				N_("second guess 'git checkout no-such-branch'")),
+		OPT_FILENAME(0, "to", &opts.new_worktree,
+			   N_("check a branch out in a separate working directory")),
 		OPT_END(),
 	};
 
@@ -1117,6 +1200,9 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
 	opts.overwrite_ignore = 1;
 	opts.prefix = prefix;
 
+	opts.saved_argv = xmalloc(sizeof(const char *) * (argc + 2));
+	memcpy(opts.saved_argv, argv, sizeof(const char *) * (argc + 1));
+
 	gitmodules_config();
 	git_config(git_checkout_config, &opts);
 
@@ -1125,6 +1211,11 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
 	argc = parse_options(argc, argv, prefix, options, checkout_usage,
 			     PARSE_OPT_KEEP_DASHDASH);
 
+	/* recursive execution from checkout_new_worktree() */
+	opts.new_worktree_mode = getenv("GIT_CHECKOUT_NEW_WORKTREE") != NULL;
+	if (opts.new_worktree_mode)
+		opts.new_worktree = NULL;
+
 	if (conflict_style) {
 		opts.merge = 1; /* implied */
 		git_xmerge_config("merge.conflictstyle", conflict_style, NULL);
diff --git a/path.c b/path.c
index 8a6586c..1fd99f8 100644
--- a/path.c
+++ b/path.c
@@ -92,7 +92,7 @@ static void replace_dir(struct strbuf *buf, int len, const char *newdir)
 
 static const char *common_list[] = {
 	"/branches", "/hooks", "/info", "/logs", "/lost-found", "/modules",
-	"/objects", "/refs", "/remotes", "/rr-cache", "/svn",
+	"/objects", "/refs", "/remotes", "/worktrees", "/rr-cache", "/svn",
 	"config", "gc.pid", "packed-refs", "shallow",
 	NULL
 };
diff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh
new file mode 100755
index 0000000..8c73b18
--- /dev/null
+++ b/t/t2025-checkout-to.sh
@@ -0,0 +1,63 @@
+#!/bin/sh
+
+test_description='test git checkout --to'
+
+. ./test-lib.sh
+
+test_expect_success 'setup' '
+	test_commit init
+'
+
+test_expect_success 'checkout --to not updating paths' '
+	test_must_fail git checkout --to -- init.t
+'
+
+test_expect_success 'checkout --to an existing worktree' '
+	mkdir existing &&
+	test_must_fail git checkout --detach --to existing master
+'
+
+test_expect_success 'checkout --to a new worktree' '
+	git checkout --to here master &&
+	(
+		cd here &&
+		test_cmp ../init.t init.t &&
+		git symbolic-ref HEAD >actual &&
+		echo refs/heads/master >expect &&
+		test_cmp expect actual &&
+		git fsck
+	)
+'
+
+test_expect_success 'checkout --to a new worktree from a subdir' '
+	(
+		mkdir sub &&
+		cd sub &&
+		git checkout --detach --to here master &&
+		cd here &&
+		test_cmp ../../init.t init.t
+	)
+'
+
+test_expect_success 'checkout --to from a linked checkout' '
+	(
+		cd here &&
+		git checkout --to nested-here master
+		cd nested-here &&
+		git fsck
+	)
+'
+
+test_expect_success 'checkout --to a new worktree creating new branch' '
+	git checkout --to there -b newmaster master &&
+	(
+		cd there &&
+		test_cmp ../init.t init.t &&
+		git symbolic-ref HEAD >actual &&
+		echo refs/heads/newmaster >expect &&
+		test_cmp expect actual &&
+		git fsck
+	)
+'
+
+test_done
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 23/32] prune: strategies for linked checkouts
  2014-09-10 22:41 ` [PATCH v2 00/32] nd/multiple-work-trees Nguyễn Thái Ngọc Duy
                     ` (21 preceding siblings ...)
  2014-09-10 22:41   ` [PATCH v2 22/32] checkout: support checking out into a new working directory Nguyễn Thái Ngọc Duy
@ 2014-09-10 22:41   ` Nguyễn Thái Ngọc Duy
  2014-09-11 15:36     ` Marc Branchaud
  2014-09-10 22:42   ` [PATCH v2 24/32] checkout: reject if the branch is already checked out elsewhere Nguyễn Thái Ngọc Duy
                     ` (9 subsequent siblings)
  32 siblings, 1 reply; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-10 22:41 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

(alias R=$GIT_COMMON_DIR/worktrees/<id>)

 - linked checkouts are supposed to keep its location in $R/gitdir up
   to date. The use case is auto fixup after a manual checkout move.

 - linked checkouts are supposed to update mtime of $R/gitdir. If
   $R/gitdir's mtime is older than a limit, and it points to nowhere,
   worktrees/<id> is to be pruned.

 - If $R/locked exists, worktrees/<id> is not supposed to be pruned. If
   $R/locked exists and $R/gitdir's mtime is older than a really long
   limit, warn about old unused repo.

 - "git checkout --to" is supposed to make a hard link named $R/link
   pointing to the .git file on supported file systems to help detect
   the user manually deleting the checkout. If $R/link exists and its
   link count is greated than 1, the repo is kept.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
---
 Documentation/git-checkout.txt             | 18 ++++++
 Documentation/git-prune.txt                |  3 +
 Documentation/gitrepository-layout.txt     | 19 ++++++
 builtin/checkout.c                         | 19 +++++-
 builtin/prune.c                            | 95 ++++++++++++++++++++++++++++++
 setup.c                                    | 13 ++++
 t/t2026-prune-linked-checkouts.sh (new +x) | 84 ++++++++++++++++++++++++++
 7 files changed, 249 insertions(+), 2 deletions(-)
 create mode 100755 t/t2026-prune-linked-checkouts.sh

diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt
index bd0fc1d..a29748e 100644
--- a/Documentation/git-checkout.txt
+++ b/Documentation/git-checkout.txt
@@ -431,6 +431,24 @@ thumb is do not make any assumption about whether a path belongs to
 $GIT_DIR or $GIT_COMMON_DIR when you need to directly access something
 inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.
 
+When you are done, simply deleting the linked working directory would
+suffice. $GIT_DIR/worktrees can be cleaned up using `git prune
+--worktrees`.
+
+After you move a linked working directory to another file system, or
+on a file system that does not support hard link, execute any git
+command (e.g. `git status`) in the new working directory so that it
+could update its location in $GIT_DIR/worktrees and not be
+accidentally pruned.
+
+To stop `git prune --worktrees` from deleting a specific working
+directory (e.g. because it's on a portable device), you could add the
+file 'locked' to $GIT_DIR/worktrees. For example, if `.git` file of
+the new working directory points to `/path/main/worktrees/test-next`,
+the full path of the 'locked' file would be
+`/path/main/worktrees/test-next/locked`. See
+linkgit:gitrepository-layout[5] for details.
+
 EXAMPLES
 --------
 
diff --git a/Documentation/git-prune.txt b/Documentation/git-prune.txt
index 7a493c8..a0ea381 100644
--- a/Documentation/git-prune.txt
+++ b/Documentation/git-prune.txt
@@ -48,6 +48,9 @@ OPTIONS
 --expire <time>::
 	Only expire loose objects older than <time>.
 
+--worktrees::
+	Prune dead worktree information in $GIT_DIR/worktrees.
+
 <head>...::
 	In addition to objects
 	reachable from any of our references, keep objects
diff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt
index a82780c..5ceff51 100644
--- a/Documentation/gitrepository-layout.txt
+++ b/Documentation/gitrepository-layout.txt
@@ -255,6 +255,25 @@ worktrees::
 	$GIT_COMMON_DIR is set and "$GIT_COMMON_DIR/worktrees" will be
 	used instead.
 
+worktrees/<id>/gitdir::
+	A text file containing the absolute path back to the .git file
+	that points to here. This is used to check if the linked
+	repository has been manually removed and there is no need to
+	keep this directory any more. mtime of this file should be
+	updated every time the linked repository is accessed.
+
+worktrees/<id>/locked::
+	If this file exists, the linked repository may be on a
+	portable device and not available. It does not mean that the
+	linked repository is gone and `worktrees/<id>` could be
+	removed. The file's content contains a reason string on why
+	the repository is locked.
+
+worktrees/<id>/link::
+	If this file exists, it is a hard link to the linked .git
+	file. It is used to detect if the linked repository is
+	manually removed.
+
 SEE ALSO
 --------
 linkgit:git-init[1],
diff --git a/builtin/checkout.c b/builtin/checkout.c
index 1868f69..b5ddf88 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -822,7 +822,7 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
 	const char *path = opts->new_worktree, *name;
 	struct stat st;
 	struct child_process cp;
-	int counter = 0, len;
+	int counter = 0, len, ret;
 
 	if (!new->commit)
 		die(_("no branch specified"));
@@ -853,11 +853,21 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
 	if (mkdir(sb_repo.buf, 0777))
 		die_errno(_("could not create directory of '%s'"), sb_repo.buf);
 
+	/*
+	 * lock the incomplete repo so prune won't delete it, unlock
+	 * after the preparation is over.
+	 */
+	strbuf_addf(&sb, "%s/locked", sb_repo.buf);
+	write_file(sb.buf, 1, "initializing\n");
+
 	strbuf_addf(&sb_git, "%s/.git", path);
 	if (safe_create_leading_directories_const(sb_git.buf))
 		die_errno(_("could not create leading directories of '%s'"),
 			  sb_git.buf);
 
+	strbuf_reset(&sb);
+	strbuf_addf(&sb, "%s/gitdir", sb_repo.buf);
+	write_file(sb.buf, 1, "%s\n", real_path(sb_git.buf));
 	write_file(sb_git.buf, 1, "gitdir: %s/worktrees/%s\n",
 		   real_path(get_git_common_dir()), name);
 	/*
@@ -866,6 +876,7 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
 	 * value would do because this value will be ignored and
 	 * replaced at the next (real) checkout.
 	 */
+	strbuf_reset(&sb);
 	strbuf_addf(&sb, "%s/HEAD", sb_repo.buf);
 	write_file(sb.buf, 1, "%s\n", sha1_to_hex(new->commit->object.sha1));
 	strbuf_reset(&sb);
@@ -881,7 +892,11 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
 	memset(&cp, 0, sizeof(cp));
 	cp.git_cmd = 1;
 	cp.argv = opts->saved_argv;
-	return run_command(&cp);
+	ret = run_command(&cp);
+	strbuf_reset(&sb);
+	strbuf_addf(&sb, "%s/locked", sb_repo.buf);
+	unlink_or_warn(sb.buf);
+	return ret;
 }
 
 static int git_checkout_config(const char *var, const char *value, void *cb)
diff --git a/builtin/prune.c b/builtin/prune.c
index 144a3bd..cf56110 100644
--- a/builtin/prune.c
+++ b/builtin/prune.c
@@ -112,6 +112,91 @@ static void prune_object_dir(const char *path)
 	}
 }
 
+static int prune_worktree(const char *id, struct strbuf *reason)
+{
+	struct stat st;
+	char *path;
+	int fd, len;
+
+	if (!is_directory(git_path("worktrees/%s", id))) {
+		strbuf_addf(reason, _("Removing worktrees/%s: not a valid directory"), id);
+		return 1;
+	}
+	if (file_exists(git_path("worktrees/%s/locked", id)))
+		return 0;
+	if (stat(git_path("worktrees/%s/gitdir", id), &st)) {
+		strbuf_addf(reason, _("Removing worktrees/%s: gitdir file does not exist"), id);
+		return 1;
+	}
+	fd = open(git_path("worktrees/%s/gitdir", id), O_RDONLY);
+	if (fd < 0) {
+		strbuf_addf(reason, _("Removing worktrees/%s: unable to read gitdir file (%s)"),
+			    id, strerror(errno));
+		return 1;
+	}
+	len = st.st_size;
+	path = xmalloc(len + 1);
+	read_in_full(fd, path, len);
+	close(fd);
+	while (len && (path[len - 1] == '\n' || path[len - 1] == '\r'))
+		len--;
+	if (!len) {
+		strbuf_addf(reason, _("Removing worktrees/%s: invalid gitdir file"), id);
+		free(path);
+		return 1;
+	}
+	path[len] = '\0';
+	if (!file_exists(path)) {
+		struct stat st_link;
+		free(path);
+		/*
+		 * the repo is moved manually and has not been
+		 * accessed since?
+		 */
+		if (!stat(git_path("worktrees/%s/link", id), &st_link) &&
+		    st_link.st_nlink > 1)
+			return 0;
+		strbuf_addf(reason, _("Removing worktrees/%s: gitdir file points to non-existent location"), id);
+		return 1;
+	}
+	free(path);
+	return st.st_mtime <= expire;
+}
+
+static void prune_worktrees(void)
+{
+	struct strbuf reason = STRBUF_INIT;
+	struct strbuf path = STRBUF_INIT;
+	DIR *dir = opendir(git_path("worktrees"));
+	struct dirent *d;
+	int ret;
+	if (!dir)
+		return;
+	while ((d = readdir(dir)) != NULL) {
+		if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, ".."))
+			continue;
+		strbuf_reset(&reason);
+		if (!prune_worktree(d->d_name, &reason))
+			continue;
+		if (show_only || verbose)
+			printf("%s\n", reason.buf);
+		if (show_only)
+			continue;
+		strbuf_reset(&path);
+		strbuf_addstr(&path, git_path("worktrees/%s", d->d_name));
+		ret = remove_dir_recursively(&path, 0);
+		if (ret < 0 && errno == ENOTDIR)
+			ret = unlink(path.buf);
+		if (ret)
+			error(_("failed to remove: %s"), strerror(errno));
+	}
+	closedir(dir);
+	if (!show_only)
+		rmdir(git_path("worktrees"));
+	strbuf_release(&reason);
+	strbuf_release(&path);
+}
+
 /*
  * Write errors (particularly out of space) can result in
  * failed temporary packs (and more rarely indexes and other
@@ -138,10 +223,12 @@ int cmd_prune(int argc, const char **argv, const char *prefix)
 {
 	struct rev_info revs;
 	struct progress *progress = NULL;
+	int do_prune_worktrees = 0;
 	const struct option options[] = {
 		OPT__DRY_RUN(&show_only, N_("do not remove, show only")),
 		OPT__VERBOSE(&verbose, N_("report pruned objects")),
 		OPT_BOOL(0, "progress", &show_progress, N_("show progress")),
+		OPT_BOOL(0, "worktrees", &do_prune_worktrees, N_("prune .git/worktrees")),
 		OPT_EXPIRY_DATE(0, "expire", &expire,
 				N_("expire objects older than <time>")),
 		OPT_END()
@@ -154,6 +241,14 @@ int cmd_prune(int argc, const char **argv, const char *prefix)
 	init_revisions(&revs, prefix);
 
 	argc = parse_options(argc, argv, prefix, options, prune_usage, 0);
+
+	if (do_prune_worktrees) {
+		if (argc)
+			die(_("--worktrees does not take extra arguments"));
+		prune_worktrees();
+		return 0;
+	}
+
 	while (argc--) {
 		unsigned char sha1[20];
 		const char *name = *argv++;
diff --git a/setup.c b/setup.c
index 8f90bc3..da2d669 100644
--- a/setup.c
+++ b/setup.c
@@ -390,6 +390,17 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)
 	return ret;
 }
 
+static void update_linked_gitdir(const char *gitfile, const char *gitdir)
+{
+	struct strbuf path = STRBUF_INIT;
+	struct stat st;
+
+	strbuf_addf(&path, "%s/gitfile", gitdir);
+	if (stat(path.buf, &st) || st.st_mtime + 24 * 3600 < time(NULL))
+		write_file(path.buf, 0, "%s\n", gitfile);
+	strbuf_release(&path);
+}
+
 /*
  * Try to read the location of the git directory from the .git file,
  * return path to git directory if found.
@@ -438,6 +449,8 @@ const char *read_gitfile(const char *path)
 
 	if (!is_git_directory(dir))
 		die("Not a git repository: %s", dir);
+
+	update_linked_gitdir(path, dir);
 	path = real_path(dir);
 
 	free(buf);
diff --git a/t/t2026-prune-linked-checkouts.sh b/t/t2026-prune-linked-checkouts.sh
new file mode 100755
index 0000000..3622800
--- /dev/null
+++ b/t/t2026-prune-linked-checkouts.sh
@@ -0,0 +1,84 @@
+#!/bin/sh
+
+test_description='prune $GIT_DIR/worktrees'
+
+. ./test-lib.sh
+
+test_expect_success 'prune --worktrees on normal repo' '
+	git prune --worktrees &&
+	test_must_fail git prune --worktrees abc
+'
+
+test_expect_success 'prune files inside $GIT_DIR/worktrees' '
+	mkdir .git/worktrees &&
+	: >.git/worktrees/abc &&
+	git prune --worktrees --verbose >actual &&
+	cat >expect <<EOF &&
+Removing worktrees/abc: not a valid directory
+EOF
+	test_i18ncmp expect actual &&
+	! test -f .git/worktrees/abc &&
+	! test -d .git/worktrees
+'
+
+test_expect_success 'prune directories without gitdir' '
+	mkdir -p .git/worktrees/def/abc &&
+	: >.git/worktrees/def/def &&
+	cat >expect <<EOF &&
+Removing worktrees/def: gitdir file does not exist
+EOF
+	git prune --worktrees --verbose >actual &&
+	test_i18ncmp expect actual &&
+	! test -d .git/worktrees/def &&
+	! test -d .git/worktrees
+'
+
+test_expect_success POSIXPERM 'prune directories with unreadable gitdir' '
+	mkdir -p .git/worktrees/def/abc &&
+	: >.git/worktrees/def/def &&
+	: >.git/worktrees/def/gitdir &&
+	chmod u-r .git/worktrees/def/gitdir &&
+	git prune --worktrees --verbose >actual &&
+	test_i18ngrep "Removing worktrees/def: unable to read gitdir file" actual &&
+	! test -d .git/worktrees/def &&
+	! test -d .git/worktrees
+'
+
+test_expect_success 'prune directories with invalid gitdir' '
+	mkdir -p .git/worktrees/def/abc &&
+	: >.git/worktrees/def/def &&
+	: >.git/worktrees/def/gitdir &&
+	git prune --worktrees --verbose >actual &&
+	test_i18ngrep "Removing worktrees/def: invalid gitdir file" actual &&
+	! test -d .git/worktrees/def &&
+	! test -d .git/worktrees
+'
+
+test_expect_success 'prune directories with gitdir pointing to nowhere' '
+	mkdir -p .git/worktrees/def/abc &&
+	: >.git/worktrees/def/def &&
+	echo "$TRASH_DIRECTORY"/nowhere >.git/worktrees/def/gitdir &&
+	git prune --worktrees --verbose >actual &&
+	test_i18ngrep "Removing worktrees/def: gitdir file points to non-existent location" actual &&
+	! test -d .git/worktrees/def &&
+	! test -d .git/worktrees
+'
+
+test_expect_success 'not prune locked checkout' '
+	test_when_finished rm -r .git/worktrees
+	mkdir -p .git/worktrees/ghi &&
+	: >.git/worktrees/ghi/locked &&
+	git prune --worktrees &&
+	test -d .git/worktrees/ghi
+'
+
+test_expect_success 'not prune recent checkouts' '
+	test_when_finished rm -r .git/worktrees
+	mkdir zz &&
+	mkdir -p .git/worktrees/jlm &&
+	echo "$TRASH_DIRECTORY"/zz >.git/worktrees/jlm/gitdir &&
+	git prune --worktrees --verbose --expire=2.days.ago &&
+	test -d .git/worktrees/jlm
+'
+
+test_done
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 24/32] checkout: reject if the branch is already checked out elsewhere
  2014-09-10 22:41 ` [PATCH v2 00/32] nd/multiple-work-trees Nguyễn Thái Ngọc Duy
                     ` (22 preceding siblings ...)
  2014-09-10 22:41   ` [PATCH v2 23/32] prune: strategies for linked checkouts Nguyễn Thái Ngọc Duy
@ 2014-09-10 22:42   ` Nguyễn Thái Ngọc Duy
  2014-09-10 22:42   ` [PATCH v2 25/32] checkout: clean up half-prepared directories in --to mode Nguyễn Thái Ngọc Duy
                     ` (8 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-10 22:42 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

One branch obviously can't be checked out at two places (but detached
heads are ok). Give the user a choice in this case: --detach, -b
new-branch, switch branch in the other checkout first or simply 'cd'
and continue to work there.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
---
 builtin/checkout.c     | 86 ++++++++++++++++++++++++++++++++++++++++++++++++--
 t/t2025-checkout-to.sh | 25 ++++++++++++---
 2 files changed, 104 insertions(+), 7 deletions(-)

diff --git a/builtin/checkout.c b/builtin/checkout.c
index b5ddf88..6d623fa 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -431,6 +431,11 @@ struct branch_info {
 	const char *name; /* The short name used */
 	const char *path; /* The full name of a real branch */
 	struct commit *commit; /* The named commit */
+	/*
+	 * if not null the branch is detached because it's already
+	 * checked out in this checkout
+	 */
+	char *checkout;
 };
 
 static void setup_branch_path(struct branch_info *branch)
@@ -954,12 +959,78 @@ static const char *unique_tracking_name(const char *name, unsigned char *sha1)
 	return NULL;
 }
 
+static void check_linked_checkout(struct branch_info *new, const char *id)
+{
+	struct strbuf sb = STRBUF_INIT;
+	struct strbuf path = STRBUF_INIT;
+	struct strbuf gitdir = STRBUF_INIT;
+	const char *start, *end;
+
+	if (id)
+		strbuf_addf(&path, "%s/worktrees/%s/HEAD", get_git_common_dir(), id);
+	else
+		strbuf_addf(&path, "%s/HEAD", get_git_common_dir());
+
+	if (strbuf_read_file(&sb, path.buf, 0) < 0 ||
+	    !skip_prefix(sb.buf, "ref:", &start))
+		goto done;
+	while (isspace(*start))
+		start++;
+	end = start;
+	while (*end && !isspace(*end))
+		end++;
+	if (strncmp(start, new->path, end - start) || new->path[end - start] != '\0')
+		goto done;
+	if (id) {
+		strbuf_reset(&path);
+		strbuf_addf(&path, "%s/worktrees/%s/gitdir", get_git_common_dir(), id);
+		if (strbuf_read_file(&gitdir, path.buf, 0) <= 0)
+			goto done;
+		strbuf_rtrim(&gitdir);
+	} else
+		strbuf_addstr(&gitdir, get_git_common_dir());
+	die(_("'%s' is already checked out at '%s'"), new->name, gitdir.buf);
+done:
+	strbuf_release(&path);
+	strbuf_release(&sb);
+	strbuf_release(&gitdir);
+}
+
+static void check_linked_checkouts(struct branch_info *new)
+{
+	struct strbuf path = STRBUF_INIT;
+	DIR *dir;
+	struct dirent *d;
+
+	strbuf_addf(&path, "%s/worktrees", get_git_common_dir());
+	if ((dir = opendir(path.buf)) == NULL) {
+		strbuf_release(&path);
+		return;
+	}
+
+	/*
+	 * $GIT_COMMON_DIR/HEAD is practically outside
+	 * $GIT_DIR so resolve_ref_unsafe() won't work (it
+	 * uses git_path). Parse the ref ourselves.
+	 */
+	check_linked_checkout(new, NULL);
+
+	while ((d = readdir(dir)) != NULL) {
+		if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, ".."))
+			continue;
+		check_linked_checkout(new, d->d_name);
+	}
+	strbuf_release(&path);
+	closedir(dir);
+}
+
 static int parse_branchname_arg(int argc, const char **argv,
 				int dwim_new_local_branch_ok,
 				struct branch_info *new,
 				struct tree **source_tree,
 				unsigned char rev[20],
-				const char **new_branch)
+				const char **new_branch,
+				int force_detach)
 {
 	int argcount = 0;
 	unsigned char branch_rev[20];
@@ -1081,6 +1152,16 @@ static int parse_branchname_arg(int argc, const char **argv,
 	else
 		new->path = NULL; /* not an existing branch */
 
+	if (new->path && !force_detach && !*new_branch) {
+		unsigned char sha1[20];
+		int flag;
+		char *head_ref = resolve_refdup("HEAD", sha1, 0, &flag);
+		if (head_ref &&
+		    (!(flag & REF_ISSYMREF) || strcmp(head_ref, new->path)))
+			check_linked_checkouts(new);
+		free(head_ref);
+	}
+
 	new->commit = lookup_commit_reference_gently(rev, 1);
 	if (!new->commit) {
 		/* not a commit */
@@ -1287,7 +1368,8 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
 			!opts.new_branch;
 		int n = parse_branchname_arg(argc, argv, dwim_ok,
 					     &new, &opts.source_tree,
-					     rev, &opts.new_branch);
+					     rev, &opts.new_branch,
+					     opts.force_detach);
 		argv += n;
 		argc -= n;
 	}
diff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh
index 8c73b18..afa8a62 100755
--- a/t/t2025-checkout-to.sh
+++ b/t/t2025-checkout-to.sh
@@ -18,13 +18,14 @@ test_expect_success 'checkout --to an existing worktree' '
 '
 
 test_expect_success 'checkout --to a new worktree' '
-	git checkout --to here master &&
+	git rev-parse HEAD >expect &&
+	git checkout --detach --to here master &&
 	(
 		cd here &&
 		test_cmp ../init.t init.t &&
-		git symbolic-ref HEAD >actual &&
-		echo refs/heads/master >expect &&
-		test_cmp expect actual &&
+		test_must_fail git symbolic-ref HEAD &&
+		git rev-parse HEAD >actual &&
+		test_cmp ../expect actual &&
 		git fsck
 	)
 '
@@ -42,7 +43,7 @@ test_expect_success 'checkout --to a new worktree from a subdir' '
 test_expect_success 'checkout --to from a linked checkout' '
 	(
 		cd here &&
-		git checkout --to nested-here master
+		git checkout --detach --to nested-here master
 		cd nested-here &&
 		git fsck
 	)
@@ -60,4 +61,18 @@ test_expect_success 'checkout --to a new worktree creating new branch' '
 	)
 '
 
+test_expect_success 'die the same branch is already checked out' '
+	(
+		cd here &&
+		test_must_fail git checkout newmaster
+	)
+'
+
+test_expect_success 'not die on re-checking out current branch' '
+	(
+		cd there &&
+		git checkout newmaster
+	)
+'
+
 test_done
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 25/32] checkout: clean up half-prepared directories in --to mode
  2014-09-10 22:41 ` [PATCH v2 00/32] nd/multiple-work-trees Nguyễn Thái Ngọc Duy
                     ` (23 preceding siblings ...)
  2014-09-10 22:42   ` [PATCH v2 24/32] checkout: reject if the branch is already checked out elsewhere Nguyễn Thái Ngọc Duy
@ 2014-09-10 22:42   ` Nguyễn Thái Ngọc Duy
  2014-09-10 22:42   ` [PATCH v2 26/32] gc: style change -- no SP before closing parenthesis Nguyễn Thái Ngọc Duy
                     ` (7 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-10 22:42 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
---
 builtin/checkout.c     | 48 ++++++++++++++++++++++++++++++++++++++++++++++++
 t/t2025-checkout-to.sh |  6 ++++++
 2 files changed, 54 insertions(+)

diff --git a/builtin/checkout.c b/builtin/checkout.c
index 6d623fa..f419ddf 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -20,6 +20,7 @@
 #include "resolve-undo.h"
 #include "submodule.h"
 #include "argv-array.h"
+#include "sigchain.h"
 
 static const char * const checkout_usage[] = {
 	N_("git checkout [options] <branch>"),
@@ -819,6 +820,35 @@ static int switch_branches(const struct checkout_opts *opts,
 	return ret || writeout_error;
 }
 
+static char *junk_work_tree;
+static char *junk_git_dir;
+static int is_junk;
+static pid_t junk_pid;
+
+static void remove_junk(void)
+{
+	struct strbuf sb = STRBUF_INIT;
+	if (!is_junk || getpid() != junk_pid)
+		return;
+	if (junk_git_dir) {
+		strbuf_addstr(&sb, junk_git_dir);
+		remove_dir_recursively(&sb, 0);
+		strbuf_reset(&sb);
+	}
+	if (junk_work_tree) {
+		strbuf_addstr(&sb, junk_work_tree);
+		remove_dir_recursively(&sb, 0);
+	}
+	strbuf_release(&sb);
+}
+
+static void remove_junk_on_signal(int signo)
+{
+	remove_junk();
+	sigchain_pop(signo);
+	raise(signo);
+}
+
 static int prepare_linked_checkout(const struct checkout_opts *opts,
 				   struct branch_info *new)
 {
@@ -855,8 +885,15 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
 		strbuf_addf(&sb_repo, "%d", counter);
 	}
 	name = strrchr(sb_repo.buf, '/') + 1;
+
+	junk_pid = getpid();
+	atexit(remove_junk);
+	sigchain_push_common(remove_junk_on_signal);
+
 	if (mkdir(sb_repo.buf, 0777))
 		die_errno(_("could not create directory of '%s'"), sb_repo.buf);
+	junk_git_dir = xstrdup(sb_repo.buf);
+	is_junk = 1;
 
 	/*
 	 * lock the incomplete repo so prune won't delete it, unlock
@@ -869,6 +906,7 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
 	if (safe_create_leading_directories_const(sb_git.buf))
 		die_errno(_("could not create leading directories of '%s'"),
 			  sb_git.buf);
+	junk_work_tree = xstrdup(path);
 
 	strbuf_reset(&sb);
 	strbuf_addf(&sb, "%s/gitdir", sb_repo.buf);
@@ -898,9 +936,19 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
 	cp.git_cmd = 1;
 	cp.argv = opts->saved_argv;
 	ret = run_command(&cp);
+	if (!ret) {
+		is_junk = 0;
+		free(junk_work_tree);
+		free(junk_git_dir);
+		junk_work_tree = NULL;
+		junk_git_dir = NULL;
+	}
 	strbuf_reset(&sb);
 	strbuf_addf(&sb, "%s/locked", sb_repo.buf);
 	unlink_or_warn(sb.buf);
+	strbuf_release(&sb);
+	strbuf_release(&sb_repo);
+	strbuf_release(&sb_git);
 	return ret;
 }
 
diff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh
index afa8a62..5d28f9e 100755
--- a/t/t2025-checkout-to.sh
+++ b/t/t2025-checkout-to.sh
@@ -17,6 +17,12 @@ test_expect_success 'checkout --to an existing worktree' '
 	test_must_fail git checkout --detach --to existing master
 '
 
+test_expect_success 'checkout --to refuses to checkout locked branch' '
+	test_must_fail git checkout --to zere master &&
+	! test -d zere &&
+	! test -d .git/worktrees/zere
+'
+
 test_expect_success 'checkout --to a new worktree' '
 	git rev-parse HEAD >expect &&
 	git checkout --detach --to here master &&
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 26/32] gc: style change -- no SP before closing parenthesis
  2014-09-10 22:41 ` [PATCH v2 00/32] nd/multiple-work-trees Nguyễn Thái Ngọc Duy
                     ` (24 preceding siblings ...)
  2014-09-10 22:42   ` [PATCH v2 25/32] checkout: clean up half-prepared directories in --to mode Nguyễn Thái Ngọc Duy
@ 2014-09-10 22:42   ` Nguyễn Thái Ngọc Duy
  2014-09-10 22:42   ` [PATCH v2 27/32] gc: factor out gc.pruneexpire parsing code Nguyễn Thái Ngọc Duy
                     ` (6 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-10 22:42 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 builtin/gc.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/builtin/gc.c b/builtin/gc.c
index 8d219d8..3bfb990 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -298,7 +298,7 @@ int cmd_gc(int argc, const char **argv, const char *prefix)
 	argv_array_pushl(&pack_refs_cmd, "pack-refs", "--all", "--prune", NULL);
 	argv_array_pushl(&reflog, "reflog", "expire", "--all", NULL);
 	argv_array_pushl(&repack, "repack", "-d", "-l", NULL);
-	argv_array_pushl(&prune, "prune", "--expire", NULL );
+	argv_array_pushl(&prune, "prune", "--expire", NULL);
 	argv_array_pushl(&rerere, "rerere", "gc", NULL);
 
 	git_config(gc_config, NULL);
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 27/32] gc: factor out gc.pruneexpire parsing code
  2014-09-10 22:41 ` [PATCH v2 00/32] nd/multiple-work-trees Nguyễn Thái Ngọc Duy
                     ` (25 preceding siblings ...)
  2014-09-10 22:42   ` [PATCH v2 26/32] gc: style change -- no SP before closing parenthesis Nguyễn Thái Ngọc Duy
@ 2014-09-10 22:42   ` Nguyễn Thái Ngọc Duy
  2014-09-10 22:42   ` [PATCH v2 28/32] gc: support prune --worktrees Nguyễn Thái Ngọc Duy
                     ` (5 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-10 22:42 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 builtin/gc.c | 21 +++++++++++++--------
 1 file changed, 13 insertions(+), 8 deletions(-)

diff --git a/builtin/gc.c b/builtin/gc.c
index 3bfb990..e38c902 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -55,6 +55,17 @@ static void remove_pidfile_on_signal(int signo)
 	raise(signo);
 }
 
+static int git_config_date_string(const char **output,
+				  const char *var, const char *value)
+{
+	if (value && strcmp(value, "now")) {
+		unsigned long now = approxidate("now");
+		if (approxidate(value) >= now)
+			return error(_("Invalid %s: '%s'"), var, value);
+	}
+	return git_config_string(output, var, value);
+}
+
 static int gc_config(const char *var, const char *value, void *cb)
 {
 	if (!strcmp(var, "gc.packrefs")) {
@@ -84,14 +95,8 @@ static int gc_config(const char *var, const char *value, void *cb)
 		detach_auto = git_config_bool(var, value);
 		return 0;
 	}
-	if (!strcmp(var, "gc.pruneexpire")) {
-		if (value && strcmp(value, "now")) {
-			unsigned long now = approxidate("now");
-			if (approxidate(value) >= now)
-				return error(_("Invalid %s: '%s'"), var, value);
-		}
-		return git_config_string(&prune_expire, var, value);
-	}
+	if (!strcmp(var, "gc.pruneexpire"))
+		return git_config_date_string(&prune_expire, var, value);
 	return git_default_config(var, value, cb);
 }
 
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 28/32] gc: support prune --worktrees
  2014-09-10 22:41 ` [PATCH v2 00/32] nd/multiple-work-trees Nguyễn Thái Ngọc Duy
                     ` (26 preceding siblings ...)
  2014-09-10 22:42   ` [PATCH v2 27/32] gc: factor out gc.pruneexpire parsing code Nguyễn Thái Ngọc Duy
@ 2014-09-10 22:42   ` Nguyễn Thái Ngọc Duy
  2014-09-21 10:43     ` Duy Nguyen
  2014-09-10 22:42   ` [PATCH v2 29/32] count-objects: report unused files in $GIT_DIR/worktrees/ Nguyễn Thái Ngọc Duy
                     ` (4 subsequent siblings)
  32 siblings, 1 reply; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-10 22:42 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
---
 Documentation/config.txt       |  7 +++++++
 Documentation/git-checkout.txt |  2 +-
 builtin/gc.c                   | 11 +++++++++++
 3 files changed, 19 insertions(+), 1 deletion(-)

diff --git a/Documentation/config.txt b/Documentation/config.txt
index 286e539..a85f684 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -1211,6 +1211,13 @@ gc.pruneexpire::
 	"now" may be used to disable this  grace period and always prune
 	unreachable objects immediately.
 
+gc.pruneworktreesexpire::
+	When 'git gc' is run, it will call
+	'prune --worktrees --expire 3.months.ago'.
+	Override the grace period with this config variable. The value
+	"now" may be used to disable the grace period and prune
+	$GIT_DIR/worktrees immediately.
+
 gc.reflogexpire::
 gc.<pattern>.reflogexpire::
 	'git reflog expire' removes reflog entries older than
diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt
index a29748e..23f0c80 100644
--- a/Documentation/git-checkout.txt
+++ b/Documentation/git-checkout.txt
@@ -433,7 +433,7 @@ inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.
 
 When you are done, simply deleting the linked working directory would
 suffice. $GIT_DIR/worktrees can be cleaned up using `git prune
---worktrees`.
+--worktrees`, which is part of automated garbage collection.
 
 After you move a linked working directory to another file system, or
 on a file system that does not support hard link, execute any git
diff --git a/builtin/gc.c b/builtin/gc.c
index e38c902..00239ca 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -33,11 +33,13 @@ static int gc_auto_threshold = 6700;
 static int gc_auto_pack_limit = 50;
 static int detach_auto = 1;
 static const char *prune_expire = "2.weeks.ago";
+static const char *prune_worktrees_expire = "3.months.ago";
 
 static struct argv_array pack_refs_cmd = ARGV_ARRAY_INIT;
 static struct argv_array reflog = ARGV_ARRAY_INIT;
 static struct argv_array repack = ARGV_ARRAY_INIT;
 static struct argv_array prune = ARGV_ARRAY_INIT;
+static struct argv_array prune_worktrees = ARGV_ARRAY_INIT;
 static struct argv_array rerere = ARGV_ARRAY_INIT;
 
 static char *pidfile;
@@ -97,6 +99,8 @@ static int gc_config(const char *var, const char *value, void *cb)
 	}
 	if (!strcmp(var, "gc.pruneexpire"))
 		return git_config_date_string(&prune_expire, var, value);
+	if (!strcmp(var, "gc.pruneworktreesexpire"))
+		return git_config_date_string(&prune_worktrees_expire, var, value);
 	return git_default_config(var, value, cb);
 }
 
@@ -304,6 +308,7 @@ int cmd_gc(int argc, const char **argv, const char *prefix)
 	argv_array_pushl(&reflog, "reflog", "expire", "--all", NULL);
 	argv_array_pushl(&repack, "repack", "-d", "-l", NULL);
 	argv_array_pushl(&prune, "prune", "--expire", NULL);
+	argv_array_pushl(&prune_worktrees, "prune", "--worktrees", "--expire", NULL);
 	argv_array_pushl(&rerere, "rerere", "gc", NULL);
 
 	git_config(gc_config, NULL);
@@ -373,6 +378,12 @@ int cmd_gc(int argc, const char **argv, const char *prefix)
 			return error(FAILED_RUN, prune.argv[0]);
 	}
 
+	if (prune_worktrees_expire) {
+		argv_array_push(&prune_worktrees, prune_worktrees_expire);
+		if (run_command_v_opt(prune_worktrees.argv, RUN_GIT_CMD))
+			return error(FAILED_RUN, prune_worktrees.argv[0]);
+	}
+
 	if (run_command_v_opt(rerere.argv, RUN_GIT_CMD))
 		return error(FAILED_RUN, rerere.argv[0]);
 
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 29/32] count-objects: report unused files in $GIT_DIR/worktrees/...
  2014-09-10 22:41 ` [PATCH v2 00/32] nd/multiple-work-trees Nguyễn Thái Ngọc Duy
                     ` (27 preceding siblings ...)
  2014-09-10 22:42   ` [PATCH v2 28/32] gc: support prune --worktrees Nguyễn Thái Ngọc Duy
@ 2014-09-10 22:42   ` Nguyễn Thái Ngọc Duy
  2014-09-10 22:42   ` [PATCH v2 30/32] git_path(): keep "info/sparse-checkout" per work-tree Nguyễn Thái Ngọc Duy
                     ` (3 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-10 22:42 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

In linked checkouts, borrowed parts like config is taken from
$GIT_COMMON_DIR. $GIT_DIR/config is never used. Report them as
garbage.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
---
 builtin/count-objects.c |  4 +++-
 cache.h                 |  1 +
 path.c                  | 29 +++++++++++++++++++++++++++--
 3 files changed, 31 insertions(+), 3 deletions(-)

diff --git a/builtin/count-objects.c b/builtin/count-objects.c
index a7f70cb..d3a1620 100644
--- a/builtin/count-objects.c
+++ b/builtin/count-objects.c
@@ -102,8 +102,10 @@ int cmd_count_objects(int argc, const char **argv, const char *prefix)
 	/* we do not take arguments other than flags for now */
 	if (argc)
 		usage_with_options(count_objects_usage, opts);
-	if (verbose)
+	if (verbose) {
 		report_garbage = real_report_garbage;
+		report_linked_checkout_garbage();
+	}
 	memcpy(path, objdir, len);
 	if (len && objdir[len-1] != '/')
 		path[len++] = '/';
diff --git a/cache.h b/cache.h
index bf4d15e..12f0fc0 100644
--- a/cache.h
+++ b/cache.h
@@ -690,6 +690,7 @@ extern const char *mkpath(const char *fmt, ...) __attribute__((format (printf, 1
 extern const char *git_path(const char *fmt, ...) __attribute__((format (printf, 1, 2)));
 extern const char *git_path_submodule(const char *path, const char *fmt, ...)
 	__attribute__((format (printf, 2, 3)));
+extern void report_linked_checkout_garbage(void);
 
 /*
  * Return the name of the file in the local object database that would
diff --git a/path.c b/path.c
index 1fd99f8..d07ae69 100644
--- a/path.c
+++ b/path.c
@@ -4,6 +4,7 @@
 #include "cache.h"
 #include "strbuf.h"
 #include "string-list.h"
+#include "dir.h"
 
 static int get_st_mode_bits(const char *path, int *mode)
 {
@@ -91,9 +92,9 @@ static void replace_dir(struct strbuf *buf, int len, const char *newdir)
 }
 
 static const char *common_list[] = {
-	"/branches", "/hooks", "/info", "/logs", "/lost-found", "/modules",
+	"/branches", "/hooks", "/info", "!/logs", "/lost-found", "/modules",
 	"/objects", "/refs", "/remotes", "/worktrees", "/rr-cache", "/svn",
-	"config", "gc.pid", "packed-refs", "shallow",
+	"config", "!gc.pid", "packed-refs", "shallow",
 	NULL
 };
 
@@ -107,6 +108,8 @@ static void update_common_dir(struct strbuf *buf, int git_dir_len)
 	for (p = common_list; *p; p++) {
 		const char *path = *p;
 		int is_dir = 0;
+		if (*path == '!')
+			path++;
 		if (*path == '/') {
 			path++;
 			is_dir = 1;
@@ -122,6 +125,28 @@ static void update_common_dir(struct strbuf *buf, int git_dir_len)
 	}
 }
 
+void report_linked_checkout_garbage(void)
+{
+	struct strbuf sb = STRBUF_INIT;
+	const char **p;
+	int len;
+
+	if (!git_common_dir_env)
+		return;
+	strbuf_addf(&sb, "%s/", get_git_dir());
+	len = sb.len;
+	for (p = common_list; *p; p++) {
+		const char *path = *p;
+		if (*path == '!')
+			continue;
+		strbuf_setlen(&sb, len);
+		strbuf_addstr(&sb, path);
+		if (file_exists(sb.buf))
+			report_garbage("unused in linked checkout", sb.buf);
+	}
+	strbuf_release(&sb);
+}
+
 static void adjust_git_path(struct strbuf *buf, int git_dir_len)
 {
 	const char *base = buf->buf + git_dir_len;
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 30/32] git_path(): keep "info/sparse-checkout" per work-tree
  2014-09-10 22:41 ` [PATCH v2 00/32] nd/multiple-work-trees Nguyễn Thái Ngọc Duy
                     ` (28 preceding siblings ...)
  2014-09-10 22:42   ` [PATCH v2 29/32] count-objects: report unused files in $GIT_DIR/worktrees/ Nguyễn Thái Ngọc Duy
@ 2014-09-10 22:42   ` Nguyễn Thái Ngọc Duy
  2014-09-10 22:42   ` [PATCH v2 31/32] checkout: don't require a work tree when checking out into a new one Nguyễn Thái Ngọc Duy
                     ` (2 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-10 22:42 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Currently git_path("info/sparse-checkout") resolves to
$GIT_COMMON_DIR/info/sparse-checkout in multiple worktree mode. It
makes more sense for the sparse checkout patterns to be per worktree,
so you can have multiple checkouts with different parts of the tree.

With this, "git checkout --to <new>" on a sparse checkout will create
<new> as a full checkout. Which is expected, it's how a new checkout
is made. The user can reshape the worktree afterwards.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 path.c                | 3 ++-
 t/t0060-path-utils.sh | 1 +
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/path.c b/path.c
index d07ae69..1f1881a 100644
--- a/path.c
+++ b/path.c
@@ -103,7 +103,8 @@ static void update_common_dir(struct strbuf *buf, int git_dir_len)
 	char *base = buf->buf + git_dir_len;
 	const char **p;
 
-	if (is_dir_file(base, "logs", "HEAD"))
+	if (is_dir_file(base, "logs", "HEAD") ||
+	    is_dir_file(base, "info", "sparse-checkout"))
 		return;	/* keep this in $GIT_DIR */
 	for (p = common_list; *p; p++) {
 		const char *path = *p;
diff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh
index f5d6f80..93605f4 100755
--- a/t/t0060-path-utils.sh
+++ b/t/t0060-path-utils.sh
@@ -270,6 +270,7 @@ test_git_path GIT_COMMON_DIR=bar objects                  bar/objects
 test_git_path GIT_COMMON_DIR=bar objects/bar              bar/objects/bar
 test_git_path GIT_COMMON_DIR=bar info/exclude             bar/info/exclude
 test_git_path GIT_COMMON_DIR=bar info/grafts              bar/info/grafts
+test_git_path GIT_COMMON_DIR=bar info/sparse-checkout     .git/info/sparse-checkout
 test_git_path GIT_COMMON_DIR=bar remotes/bar              bar/remotes/bar
 test_git_path GIT_COMMON_DIR=bar branches/bar             bar/branches/bar
 test_git_path GIT_COMMON_DIR=bar logs/refs/heads/master   bar/logs/refs/heads/master
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 31/32] checkout: don't require a work tree when checking out into a new one
  2014-09-10 22:41 ` [PATCH v2 00/32] nd/multiple-work-trees Nguyễn Thái Ngọc Duy
                     ` (29 preceding siblings ...)
  2014-09-10 22:42   ` [PATCH v2 30/32] git_path(): keep "info/sparse-checkout" per work-tree Nguyễn Thái Ngọc Duy
@ 2014-09-10 22:42   ` Nguyễn Thái Ngọc Duy
  2014-09-10 22:42   ` [PATCH v2 32/32] t2025: add a test to make sure grafts is working from a linked checkout Nguyễn Thái Ngọc Duy
  2014-09-25 21:20   ` [PATCH v2 00/32] nd/multiple-work-trees Junio C Hamano
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-10 22:42 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Dennis Kaarsemaker,
	Nguyễn Thái Ngọc Duy

From: Dennis Kaarsemaker <dennis@kaarsemaker.net>

For normal use cases, it does not make sense for 'checkout' to work on
a bare repository, without a worktree. But "checkout --to" is an
exception because it _creates_ a new worktree. Allow this option to
run on bare repositories.

People who check out from a bare repository should remember that
core.logallrefupdates is off by default and it should be turned back
on. `--to` cannot do this automatically behind the user's back because
some user may deliberately want no reflog.

For people interested in repository setup/discovery code,
is_bare_repository_cfg (aka "core.bare") is unchanged by this patch,
which means 'true' by default for bare repos. Fortunately when we get
the repo through a linked checkout, is_bare_repository_cfg is never
used. So all is still good.

[nd: commit message]

Signed-off-by: Dennis Kaarsemaker <dennis@kaarsemaker.net>
Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
---
 builtin/checkout.c     |  3 +++
 git.c                  |  2 +-
 t/t2025-checkout-to.sh | 15 +++++++++++++++
 3 files changed, 19 insertions(+), 1 deletion(-)

diff --git a/builtin/checkout.c b/builtin/checkout.c
index f419ddf..6106b81 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -1360,6 +1360,9 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
 	if (opts.new_worktree_mode)
 		opts.new_worktree = NULL;
 
+	if (!opts.new_worktree)
+		setup_work_tree();
+
 	if (conflict_style) {
 		opts.merge = 1; /* implied */
 		git_xmerge_config("merge.conflictstyle", conflict_style, NULL);
diff --git a/git.c b/git.c
index 5b6c761..7426651 100644
--- a/git.c
+++ b/git.c
@@ -383,7 +383,7 @@ static struct cmd_struct commands[] = {
 	{ "check-ignore", cmd_check_ignore, RUN_SETUP | NEED_WORK_TREE },
 	{ "check-mailmap", cmd_check_mailmap, RUN_SETUP },
 	{ "check-ref-format", cmd_check_ref_format },
-	{ "checkout", cmd_checkout, RUN_SETUP | NEED_WORK_TREE },
+	{ "checkout", cmd_checkout, RUN_SETUP },
 	{ "checkout-index", cmd_checkout_index,
 		RUN_SETUP | NEED_WORK_TREE},
 	{ "cherry", cmd_cherry, RUN_SETUP },
diff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh
index 5d28f9e..2cddbf1 100755
--- a/t/t2025-checkout-to.sh
+++ b/t/t2025-checkout-to.sh
@@ -81,4 +81,19 @@ test_expect_success 'not die on re-checking out current branch' '
 	)
 '
 
+test_expect_success 'checkout --to from a bare repo' '
+	(
+		git clone --bare . bare &&
+		cd bare &&
+		git checkout --to ../there2 -b bare-master master
+	)
+'
+
+test_expect_success 'checkout from a bare repo without --to' '
+	(
+		cd bare &&
+		test_must_fail git checkout master
+	)
+'
+
 test_done
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v2 32/32] t2025: add a test to make sure grafts is working from a linked checkout
  2014-09-10 22:41 ` [PATCH v2 00/32] nd/multiple-work-trees Nguyễn Thái Ngọc Duy
                     ` (30 preceding siblings ...)
  2014-09-10 22:42   ` [PATCH v2 31/32] checkout: don't require a work tree when checking out into a new one Nguyễn Thái Ngọc Duy
@ 2014-09-10 22:42   ` Nguyễn Thái Ngọc Duy
  2014-09-25 21:20   ` [PATCH v2 00/32] nd/multiple-work-trees Junio C Hamano
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-10 22:42 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
---
 t/t2025-checkout-to.sh | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)

diff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh
index 2cddbf1..27384a1 100755
--- a/t/t2025-checkout-to.sh
+++ b/t/t2025-checkout-to.sh
@@ -96,4 +96,22 @@ test_expect_success 'checkout from a bare repo without --to' '
 	)
 '
 
+test_expect_success 'checkout with grafts' '
+	test_when_finished rm .git/info/grafts &&
+	test_commit abc &&
+	SHA1=`git rev-parse HEAD` &&
+	test_commit def &&
+	test_commit xyz &&
+	echo "`git rev-parse HEAD` $SHA1" >.git/info/grafts &&
+	cat >expected <<-\EOF &&
+	xyz
+	abc
+	EOF
+	git log --format=%s -2 >actual &&
+	test_cmp expected actual &&
+	git checkout --detach --to grafted master &&
+	git --git-dir=grafted/.git log --format=%s -2 >actual &&
+	test_cmp expected actual
+'
+
 test_done
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* Re: [PATCH v2 22/32] checkout: support checking out into a new working directory
  2014-09-10 22:41   ` [PATCH v2 22/32] checkout: support checking out into a new working directory Nguyễn Thái Ngọc Duy
@ 2014-09-11 15:02     ` Marc Branchaud
  2014-09-21  2:41       ` Duy Nguyen
  0 siblings, 1 reply; 134+ messages in thread
From: Marc Branchaud @ 2014-09-11 15:02 UTC (permalink / raw)
  To: Nguyễn Thái Ngọc Duy, git; +Cc: Junio C Hamano

On 14-09-10 06:41 PM, Nguyễn Thái Ngọc Duy wrote:
> "git checkout --to" sets up a new working directory with a .git file
> pointing to $GIT_DIR/worktrees/<id>. It then executes "git checkout"
> again on the new worktree with the same arguments except "--to" is
> taken out. The second checkout execution, which is not contaminated
> with any info from the current repository, will actually check out and
> everything that normal "git checkout" does.
> 
> Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
> ---
>  Documentation/git-checkout.txt         | 43 +++++++++++++++
>  Documentation/git.txt                  |  3 +-
>  Documentation/gitrepository-layout.txt |  7 +++
>  builtin/checkout.c                     | 95 +++++++++++++++++++++++++++++++++-
>  path.c                                 |  2 +-
>  t/t2025-checkout-to.sh (new +x)        | 63 ++++++++++++++++++++++
>  6 files changed, 209 insertions(+), 4 deletions(-)
>  create mode 100755 t/t2025-checkout-to.sh
> 
> diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt
> index 33ad2ad..bd0fc1d 100644
> --- a/Documentation/git-checkout.txt
> +++ b/Documentation/git-checkout.txt
> @@ -225,6 +225,13 @@ This means that you can use `git checkout -p` to selectively discard
>  edits from your current working tree. See the ``Interactive Mode''
>  section of linkgit:git-add[1] to learn how to operate the `--patch` mode.
>  
> +--to=<path>::
> +	Check out a branch in a separate working directory at
> +	`<path>`. A new working directory is linked to the current
> +	repository, sharing everything except working directory
> +	specific files such as HEAD, index... See "MULTIPLE CHECKOUT
> +	MODE" section for more information.
> +
>  <branch>::
>  	Branch to checkout; if it refers to a branch (i.e., a name that,
>  	when prepended with "refs/heads/", is a valid ref), then that
> @@ -388,6 +395,42 @@ $ git reflog -2 HEAD # or
>  $ git log -g -2 HEAD
>  ------------
>  
> +MULTIPLE CHECKOUT MODE
> +----------------------
> +Normally a working directory is attached to repository. When "git
> +checkout --to" is used, a new working directory is attached to the
> +current repository. This new working directory is called "linked
> +checkout" as compared to the "main checkout" prepared by "git init" or
> +"git clone". A repository has one main checkout and zero or more
> +linked checkouts.
> +
> +Each linked checkout has a private directory in $GIT_DIR/worktrees in
> +the main checkout, usually named after the base name of the new
> +working directory, optionally with a number added to make it
> +unique. For example, the command `git checkout --to ../test-next next`
> +running with $GIT_DIR=/path/main may create the directory
> +`$GIT_DIR/worktrees/test-next` (or `$GIT_DIR/worktrees/test-next1` if
> +`test-next` is already taken).
> +
> +Within a linked checkout, $GIT_DIR is set to point to this private
> +directory (e.g. `/path/main/worktrees/test-next` in the example) and
> +$GIT_COMMON_DIR is set to point back to the main checkout's $GIT_DIR
> +(e.g. `/path/main`). Setting is done via a .git file located at the
> +top directory of the linked checkout.
> +
> +Path resolution via `git rev-parse --git-path` would use either
> +$GIT_DIR or $GIT_COMMON_DIR depending on the path. For example, the
> +linked checkout's `$GIT_DIR/HEAD` resolve to
> +`/path/main/worktrees/test-next/HEAD` (not `/path/main/HEAD` which is
> +the main checkout's HEAD) while `$GIT_DIR/refs/heads/master` would use
> +$GIT_COMMON_DIR and resolve to `/path/main/refs/heads/master`, which
> +is shared across checkouts.
> +
> +See linkgit:gitrepository-layout[5] for more information. The rule of
> +thumb is do not make any assumption about whether a path belongs to
> +$GIT_DIR or $GIT_COMMON_DIR when you need to directly access something
> +inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.
> +

Um, didn't you say in [1] that you'd use the text I posted in [2]?

[1] http://article.gmane.org/gmane.comp.version-control.git/256446
[2] http://article.gmane.org/gmane.comp.version-control.git/256323

		M.

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH v2 23/32] prune: strategies for linked checkouts
  2014-09-10 22:41   ` [PATCH v2 23/32] prune: strategies for linked checkouts Nguyễn Thái Ngọc Duy
@ 2014-09-11 15:36     ` Marc Branchaud
  2014-09-12  3:06       ` Eric Sunshine
  0 siblings, 1 reply; 134+ messages in thread
From: Marc Branchaud @ 2014-09-11 15:36 UTC (permalink / raw)
  To: Nguyễn Thái Ngọc Duy, git; +Cc: Junio C Hamano

On 14-09-10 06:41 PM, Nguyễn Thái Ngọc Duy wrote:
> (alias R=$GIT_COMMON_DIR/worktrees/<id>)
> 
>  - linked checkouts are supposed to keep its location in $R/gitdir up
>    to date. The use case is auto fixup after a manual checkout move.
> 
>  - linked checkouts are supposed to update mtime of $R/gitdir. If
>    $R/gitdir's mtime is older than a limit, and it points to nowhere,
>    worktrees/<id> is to be pruned.
> 
>  - If $R/locked exists, worktrees/<id> is not supposed to be pruned. If
>    $R/locked exists and $R/gitdir's mtime is older than a really long
>    limit, warn about old unused repo.
> 
>  - "git checkout --to" is supposed to make a hard link named $R/link
>    pointing to the .git file on supported file systems to help detect
>    the user manually deleting the checkout. If $R/link exists and its
>    link count is greated than 1, the repo is kept.
> 
> Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
> ---
>  Documentation/git-checkout.txt             | 18 ++++++
>  Documentation/git-prune.txt                |  3 +
>  Documentation/gitrepository-layout.txt     | 19 ++++++
>  builtin/checkout.c                         | 19 +++++-
>  builtin/prune.c                            | 95 ++++++++++++++++++++++++++++++
>  setup.c                                    | 13 ++++
>  t/t2026-prune-linked-checkouts.sh (new +x) | 84 ++++++++++++++++++++++++++
>  7 files changed, 249 insertions(+), 2 deletions(-)
>  create mode 100755 t/t2026-prune-linked-checkouts.sh
> 
> diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt
> index bd0fc1d..a29748e 100644
> --- a/Documentation/git-checkout.txt
> +++ b/Documentation/git-checkout.txt
> @@ -431,6 +431,24 @@ thumb is do not make any assumption about whether a path belongs to
>  $GIT_DIR or $GIT_COMMON_DIR when you need to directly access something
>  inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.
>  
> +When you are done, simply deleting the linked working directory would
> +suffice. $GIT_DIR/worktrees can be cleaned up using `git prune
> +--worktrees`.

This needs a tweak or two so that it follows more naturally from the previous
paragraph.  How about:

	When you are done with a linked working tree you can simply delete
	it.  You can clean up any stale $GIT_DIR/worktrees entries with
	`git prune --worktrees`.

Then in commit 28, when you add worktrees pruning to gc, you should change
this paragraph again:

	When you are done with a linked working tree you can simply delete
	it.  The working tree's entry in the repository's $GIT_DIR/worktrees
	directory will eventually be removed automatically (see
	`gc.pruneworktreesexpire` in linkgit::git-config[1]), or you can run
	`git prune --worktrees` to clean up any stale entries in
	$GIT_DIR/worktrees.

> +After you move a linked working directory to another file system, or
> +on a file system that does not support hard link, execute any git
> +command (e.g. `git status`) in the new working directory so that it
> +could update its location in $GIT_DIR/worktrees and not be
> +accidentally pruned.

It took me a couple of seconds to parse that.  How about:

	If you move a linked working directory to another file system, or
	within a file system that does not support hard links, you need to
	run at least one git command inside the moved linked working
	directory (e.g. `git status`) in order to update its entry in
	$GIT_DIR/worktrees so that it does not get automatically removed.

> +To stop `git prune --worktrees` from deleting a specific working
> +directory (e.g. because it's on a portable device), you could add the
> +file 'locked' to $GIT_DIR/worktrees. For example, if `.git` file of
> +the new working directory points to `/path/main/worktrees/test-next`,
> +the full path of the 'locked' file would be
> +`/path/main/worktrees/test-next/locked`. See
> +linkgit:gitrepository-layout[5] for details.

Sorry, I can't help rewriting this one too:

	To prevent `git prune --worktrees` from deleting a
	$GIT_DIR/worktrees entry (which can be useful in some situations,
	such as when the entry's working tree is stored on a portable
	device), add a file named 'locked' to the entry's directory.  For
	example, if a linked working tree's `.git` file points to
	`/path/main/.git/worktrees/test-next` then a file named
	`/path/main/.git/worktrees/test-next/locked` will prevent the
	`test-next` entry from being pruned.  See
	linkgit:gitrepository-layout[5] for details.

I also suggest this paragraph get updated in commit 28, but just change the
first clause, before "(which can be ...":

	To prevent a $GIT_DIR/worktrees entry from being pruned (which ...

Thanks for all this work!

		M.

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH v2 23/32] prune: strategies for linked checkouts
  2014-09-11 15:36     ` Marc Branchaud
@ 2014-09-12  3:06       ` Eric Sunshine
  2014-09-12 19:08         ` Marc Branchaud
  2014-09-21  2:54         ` Duy Nguyen
  0 siblings, 2 replies; 134+ messages in thread
From: Eric Sunshine @ 2014-09-12  3:06 UTC (permalink / raw)
  To: Marc Branchaud
  Cc: Nguyễn Thái Ngọc Duy, Git List, Junio C Hamano

On Thu, Sep 11, 2014 at 11:36 AM, Marc Branchaud <marcnarc@xiplink.com> wrote:
> On 14-09-10 06:41 PM, Nguyễn Thái Ngọc Duy wrote:
>> (alias R=$GIT_COMMON_DIR/worktrees/<id>)
>>
>>  - linked checkouts are supposed to keep its location in $R/gitdir up
>>    to date. The use case is auto fixup after a manual checkout move.
>>
>>  - linked checkouts are supposed to update mtime of $R/gitdir. If
>>    $R/gitdir's mtime is older than a limit, and it points to nowhere,
>>    worktrees/<id> is to be pruned.
>>
>>  - If $R/locked exists, worktrees/<id> is not supposed to be pruned. If
>>    $R/locked exists and $R/gitdir's mtime is older than a really long
>>    limit, warn about old unused repo.
>>
>>  - "git checkout --to" is supposed to make a hard link named $R/link
>>    pointing to the .git file on supported file systems to help detect
>>    the user manually deleting the checkout. If $R/link exists and its
>>    link count is greated than 1, the repo is kept.
>>
>> Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
>> ---
>>  Documentation/git-checkout.txt             | 18 ++++++
>>  Documentation/git-prune.txt                |  3 +
>>  Documentation/gitrepository-layout.txt     | 19 ++++++
>>  builtin/checkout.c                         | 19 +++++-
>>  builtin/prune.c                            | 95 ++++++++++++++++++++++++++++++
>>  setup.c                                    | 13 ++++
>>  t/t2026-prune-linked-checkouts.sh (new +x) | 84 ++++++++++++++++++++++++++
>>  7 files changed, 249 insertions(+), 2 deletions(-)
>>  create mode 100755 t/t2026-prune-linked-checkouts.sh
>>
>> diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt
>> index bd0fc1d..a29748e 100644
>> --- a/Documentation/git-checkout.txt
>> +++ b/Documentation/git-checkout.txt
>> @@ -431,6 +431,24 @@ thumb is do not make any assumption about whether a path belongs to
>>  $GIT_DIR or $GIT_COMMON_DIR when you need to directly access something
>>  inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.
>>
>> +When you are done, simply deleting the linked working directory would
>> +suffice. $GIT_DIR/worktrees can be cleaned up using `git prune
>> +--worktrees`.
>
> This needs a tweak or two so that it follows more naturally from the previous
> paragraph.  How about:
>
>         When you are done with a linked working tree you can simply delete
>         it.  You can clean up any stale $GIT_DIR/worktrees entries with
>         `git prune --worktrees`.

Thanks for these rewrites; I was going to provide similar suggestions.

One minor addition for clarification would be to mention that the 'git
prune --worktrees' invocation applies to the main worktree:

    When you are done with a linked working tree, you can simply delete
    it. You can clean up any stale $GIT_DIR/worktrees entries via
    `git prune --worktrees` in the main worktree.

> Then in commit 28, when you add worktrees pruning to gc, you should change
> this paragraph again:
>
>         When you are done with a linked working tree you can simply delete
>         it.  The working tree's entry in the repository's $GIT_DIR/worktrees
>         directory will eventually be removed automatically (see
>         `gc.pruneworktreesexpire` in linkgit::git-config[1]), or you can run
>         `git prune --worktrees` to clean up any stale entries in
>         $GIT_DIR/worktrees.

Ditto about qualifying 'git prune --worktrees' with "in the main work tree".

>> +After you move a linked working directory to another file system, or
>> +on a file system that does not support hard link, execute any git
>> +command (e.g. `git status`) in the new working directory so that it
>> +could update its location in $GIT_DIR/worktrees and not be
>> +accidentally pruned.
>
> It took me a couple of seconds to parse that.  How about:
>
>         If you move a linked working directory to another file system, or
>         within a file system that does not support hard links, you need to
>         run at least one git command inside the moved linked working

I trip over "moved linked" every time I read it. I think there's
sufficient context in the 'if' clause leading to this that "moved" can
be dropped.

>         directory (e.g. `git status`) in order to update its entry in
>         $GIT_DIR/worktrees so that it does not get automatically removed.
>
>> +To stop `git prune --worktrees` from deleting a specific working
>> +directory (e.g. because it's on a portable device), you could add the
>> +file 'locked' to $GIT_DIR/worktrees. For example, if `.git` file of
>> +the new working directory points to `/path/main/worktrees/test-next`,
>> +the full path of the 'locked' file would be
>> +`/path/main/worktrees/test-next/locked`. See
>> +linkgit:gitrepository-layout[5] for details.
>
> Sorry, I can't help rewriting this one too:
>
>         To prevent `git prune --worktrees` from deleting a
>         $GIT_DIR/worktrees entry (which can be useful in some situations,
>         such as when the entry's working tree is stored on a portable
>         device), add a file named 'locked' to the entry's directory.  For
>         example, if a linked working tree's `.git` file points to
>         `/path/main/.git/worktrees/test-next` then a file named
>         `/path/main/.git/worktrees/test-next/locked` will prevent the
>         `test-next` entry from being pruned.  See
>         linkgit:gitrepository-layout[5] for details.

Each time I read this (or Duy's original), the first question that
pops into my head is "should 'locked' be empty or have content, and if
content, what content?" gitrepository-layout.txt does explain the
content (if the reader bothers to chase the link), but perhaps it is
worth an explanatory sentence here?

> I also suggest this paragraph get updated in commit 28, but just change the
> first clause, before "(which can be ...":
>
>         To prevent a $GIT_DIR/worktrees entry from being pruned (which ...
>
> Thanks for all this work!
>
>                 M.

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH v2 23/32] prune: strategies for linked checkouts
  2014-09-12  3:06       ` Eric Sunshine
@ 2014-09-12 19:08         ` Marc Branchaud
  2014-09-21  2:54         ` Duy Nguyen
  1 sibling, 0 replies; 134+ messages in thread
From: Marc Branchaud @ 2014-09-12 19:08 UTC (permalink / raw)
  To: Eric Sunshine
  Cc: Nguyễn Thái Ngọc Duy, Git List, Junio C Hamano

On 14-09-11 11:06 PM, Eric Sunshine wrote:
> On Thu, Sep 11, 2014 at 11:36 AM, Marc Branchaud <marcnarc@xiplink.com> wrote:
>> On 14-09-10 06:41 PM, Nguyễn Thái Ngọc Duy wrote:
>>> (alias R=$GIT_COMMON_DIR/worktrees/<id>)
>>>
>>>  - linked checkouts are supposed to keep its location in $R/gitdir up
>>>    to date. The use case is auto fixup after a manual checkout move.
>>>
>>>  - linked checkouts are supposed to update mtime of $R/gitdir. If
>>>    $R/gitdir's mtime is older than a limit, and it points to nowhere,
>>>    worktrees/<id> is to be pruned.
>>>
>>>  - If $R/locked exists, worktrees/<id> is not supposed to be pruned. If
>>>    $R/locked exists and $R/gitdir's mtime is older than a really long
>>>    limit, warn about old unused repo.
>>>
>>>  - "git checkout --to" is supposed to make a hard link named $R/link
>>>    pointing to the .git file on supported file systems to help detect
>>>    the user manually deleting the checkout. If $R/link exists and its
>>>    link count is greated than 1, the repo is kept.
>>>
>>> Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
>>> ---
>>>  Documentation/git-checkout.txt             | 18 ++++++
>>>  Documentation/git-prune.txt                |  3 +
>>>  Documentation/gitrepository-layout.txt     | 19 ++++++
>>>  builtin/checkout.c                         | 19 +++++-
>>>  builtin/prune.c                            | 95 ++++++++++++++++++++++++++++++
>>>  setup.c                                    | 13 ++++
>>>  t/t2026-prune-linked-checkouts.sh (new +x) | 84 ++++++++++++++++++++++++++
>>>  7 files changed, 249 insertions(+), 2 deletions(-)
>>>  create mode 100755 t/t2026-prune-linked-checkouts.sh
>>>
>>> diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt
>>> index bd0fc1d..a29748e 100644
>>> --- a/Documentation/git-checkout.txt
>>> +++ b/Documentation/git-checkout.txt
>>> @@ -431,6 +431,24 @@ thumb is do not make any assumption about whether a path belongs to
>>>  $GIT_DIR or $GIT_COMMON_DIR when you need to directly access something
>>>  inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.
>>>
>>> +When you are done, simply deleting the linked working directory would
>>> +suffice. $GIT_DIR/worktrees can be cleaned up using `git prune
>>> +--worktrees`.
>>
>> This needs a tweak or two so that it follows more naturally from the previous
>> paragraph.  How about:
>>
>>         When you are done with a linked working tree you can simply delete
>>         it.  You can clean up any stale $GIT_DIR/worktrees entries with
>>         `git prune --worktrees`.
> 
> Thanks for these rewrites; I was going to provide similar suggestions.
> 
> One minor addition for clarification would be to mention that the 'git
> prune --worktrees' invocation applies to the main worktree:
> 
>     When you are done with a linked working tree, you can simply delete
>     it. You can clean up any stale $GIT_DIR/worktrees entries via
>     `git prune --worktrees` in the main worktree.
> 
>> Then in commit 28, when you add worktrees pruning to gc, you should change
>> this paragraph again:
>>
>>         When you are done with a linked working tree you can simply delete
>>         it.  The working tree's entry in the repository's $GIT_DIR/worktrees
>>         directory will eventually be removed automatically (see
>>         `gc.pruneworktreesexpire` in linkgit::git-config[1]), or you can run
>>         `git prune --worktrees` to clean up any stale entries in
>>         $GIT_DIR/worktrees.
> 
> Ditto about qualifying 'git prune --worktrees' with "in the main work tree".
> 
>>> +After you move a linked working directory to another file system, or
>>> +on a file system that does not support hard link, execute any git
>>> +command (e.g. `git status`) in the new working directory so that it
>>> +could update its location in $GIT_DIR/worktrees and not be
>>> +accidentally pruned.
>>
>> It took me a couple of seconds to parse that.  How about:
>>
>>         If you move a linked working directory to another file system, or
>>         within a file system that does not support hard links, you need to
>>         run at least one git command inside the moved linked working
> 
> I trip over "moved linked" every time I read it. I think there's
> sufficient context in the 'if' clause leading to this that "moved" can
> be dropped.

Fine by me, as are your other suggestions.

		M.


>>         directory (e.g. `git status`) in order to update its entry in
>>         $GIT_DIR/worktrees so that it does not get automatically removed.
>>
>>> +To stop `git prune --worktrees` from deleting a specific working
>>> +directory (e.g. because it's on a portable device), you could add the
>>> +file 'locked' to $GIT_DIR/worktrees. For example, if `.git` file of
>>> +the new working directory points to `/path/main/worktrees/test-next`,
>>> +the full path of the 'locked' file would be
>>> +`/path/main/worktrees/test-next/locked`. See
>>> +linkgit:gitrepository-layout[5] for details.
>>
>> Sorry, I can't help rewriting this one too:
>>
>>         To prevent `git prune --worktrees` from deleting a
>>         $GIT_DIR/worktrees entry (which can be useful in some situations,
>>         such as when the entry's working tree is stored on a portable
>>         device), add a file named 'locked' to the entry's directory.  For
>>         example, if a linked working tree's `.git` file points to
>>         `/path/main/.git/worktrees/test-next` then a file named
>>         `/path/main/.git/worktrees/test-next/locked` will prevent the
>>         `test-next` entry from being pruned.  See
>>         linkgit:gitrepository-layout[5] for details.
> 
> Each time I read this (or Duy's original), the first question that
> pops into my head is "should 'locked' be empty or have content, and if
> content, what content?" gitrepository-layout.txt does explain the
> content (if the reader bothers to chase the link), but perhaps it is
> worth an explanatory sentence here?
> 
>> I also suggest this paragraph get updated in commit 28, but just change the
>> first clause, before "(which can be ...":
>>
>>         To prevent a $GIT_DIR/worktrees entry from being pruned (which ...
>>
>> Thanks for all this work!
>>
>>                 M.
> --
> To unsubscribe from this list: send the line "unsubscribe git" in
> the body of a message to majordomo@vger.kernel.org
> More majordomo info at  http://vger.kernel.org/majordomo-info.html
> 

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH v2 22/32] checkout: support checking out into a new working directory
  2014-09-11 15:02     ` Marc Branchaud
@ 2014-09-21  2:41       ` Duy Nguyen
  2014-09-21  3:10         ` Eric Sunshine
  0 siblings, 1 reply; 134+ messages in thread
From: Duy Nguyen @ 2014-09-21  2:41 UTC (permalink / raw)
  To: Marc Branchaud; +Cc: git, Junio C Hamano

On Thu, Sep 11, 2014 at 11:02:36AM -0400, Marc Branchaud wrote:
> Um, didn't you say in [1] that you'd use the text I posted in [2]?
> 
> [1] http://article.gmane.org/gmane.comp.version-control.git/256446
> [2] http://article.gmane.org/gmane.comp.version-control.git/256323

That's the problem with updating a bit one day and a bit another
day. Thanks for checking. Does this look ok?

-- 8< --
Subject: [PATCH] checkout: support checking out into a new working directory

"git checkout --to" sets up a new working directory with a .git file
pointing to $GIT_DIR/worktrees/<id>. It then executes "git checkout"
again on the new worktree with the same arguments except "--to" is
taken out. The second checkout execution, which is not contaminated
with any info from the current repository, will actually check out and
everything that normal "git checkout" does.

Helped-by: Marc Branchaud <marcnarc@xiplink.com>
Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
---
 Documentation/git-checkout.txt         | 46 ++++++++++++++++
 Documentation/git.txt                  |  3 +-
 Documentation/gitrepository-layout.txt |  7 +++
 builtin/checkout.c                     | 95 +++++++++++++++++++++++++++++++++-
 path.c                                 |  2 +-
 t/t2025-checkout-to.sh (new +x)        | 63 ++++++++++++++++++++++
 6 files changed, 212 insertions(+), 4 deletions(-)
 create mode 100755 t/t2025-checkout-to.sh

diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt
index 33ad2ad..c101575 100644
--- a/Documentation/git-checkout.txt
+++ b/Documentation/git-checkout.txt
@@ -225,6 +225,13 @@ This means that you can use `git checkout -p` to selectively discard
 edits from your current working tree. See the ``Interactive Mode''
 section of linkgit:git-add[1] to learn how to operate the `--patch` mode.
 
+--to=<path>::
+	Check out a branch in a separate working directory at
+	`<path>`. A new working directory is linked to the current
+	repository, sharing everything except working directory
+	specific files such as HEAD, index... See "MULTIPLE WORKING
+	TREES" section for more information.
+
 <branch>::
 	Branch to checkout; if it refers to a branch (i.e., a name that,
 	when prepended with "refs/heads/", is a valid ref), then that
@@ -388,6 +395,45 @@ $ git reflog -2 HEAD # or
 $ git log -g -2 HEAD
 ------------
 
+MULTIPLE WORKING TREES
+----------------------
+
+A git repository can support multiple working trees, allowing you to check
+out more than one branch at a time.  With `git checkout --to` a new working
+tree is associated with the repository.  This new working tree is called a
+"linked working tree" as opposed to the "main working tree" prepared by "git
+init" or "git clone".  A repository has one main working tree (if it's not a
+bare repository) and zero or more linked working trees.
+
+Each linked working tree has a private sub-directory in the repository's
+$GIT_DIR/worktrees directory.  The private sub-directory's name is usually
+the base name of the linked working tree's path, possibly appended with a
+number to make it unique.  For example, when `$GIT_DIR=/path/main/.git` the
+command `git checkout --to /path/other/test-next next` creates the linked
+working tree in `/path/other/test-next` and also creates a
+`$GIT_DIR/worktrees/test-next` directory (or `$GIT_DIR/worktrees/test-next1`
+if `test-next` is already taken).
+
+Within a linked working tree, $GIT_DIR is set to point to this private
+directory (e.g. `/path/main/.git/worktrees/test-next` in the example) and
+$GIT_COMMON_DIR is set to point back to the main working tree's $GIT_DIR
+(e.g. `/path/main/.git`). These settings are made in a `.git` file located at
+the top directory of the linked working tree.
+
+Path resolution via `git rev-parse --git-path` uses either
+$GIT_DIR or $GIT_COMMON_DIR depending on the path. For example, in the
+linked working tree `git rev-parse --git-path HEAD` returns
+`/path/main/.git/worktrees/test-next/HEAD` (not
+`/path/other/test-next/.git/HEAD` or `/path/main/.git/HEAD`) while `git
+rev-parse --git-path refs/heads/master` uses
+$GIT_COMMON_DIR and returns `/path/main/.git/refs/heads/master`,
+since refs are shared across all working trees.
+
+See linkgit:gitrepository-layout[5] for more information. The rule of
+thumb is do not make any assumption about whether a path belongs to
+$GIT_DIR or $GIT_COMMON_DIR when you need to directly access something
+inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.
+
 EXAMPLES
 --------
 
diff --git a/Documentation/git.txt b/Documentation/git.txt
index 176d37c..54c9fb6 100644
--- a/Documentation/git.txt
+++ b/Documentation/git.txt
@@ -806,7 +806,8 @@ Git so take care if using Cogito etc.
 	If this variable is set to a path, non-worktree files that are
 	normally in $GIT_DIR will be taken from this path
 	instead. Worktree-specific files such as HEAD or index are
-	taken from $GIT_DIR. See linkgit:gitrepository-layout[5] for
+	taken from $GIT_DIR. See linkgit:gitrepository-layout[5] and
+	the section 'MULTIPLE CHECKOUT MODE' in linkgit:checkout[1]
 	details. This variable has lower precedence than other path
 	variables such as GIT_INDEX_FILE, GIT_OBJECT_DIRECTORY...
 
diff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt
index 2dc5667..8228450 100644
--- a/Documentation/gitrepository-layout.txt
+++ b/Documentation/gitrepository-layout.txt
@@ -252,6 +252,13 @@ modules::
 	directory is ignored if $GIT_COMMON_DIR is set and
 	"$GIT_COMMON_DIR/modules" will be used instead.
 
+worktrees::
+	Contains worktree specific information of linked
+	checkouts. Each subdirectory contains the worktree-related
+	part of a linked checkout. This directory is ignored if
+	$GIT_COMMON_DIR is set and "$GIT_COMMON_DIR/worktrees" will be
+	used instead.
+
 SEE ALSO
 --------
 linkgit:git-init[1],
diff --git a/builtin/checkout.c b/builtin/checkout.c
index 220f80e..ad10f99 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -48,6 +48,10 @@ struct checkout_opts {
 	const char *prefix;
 	struct pathspec pathspec;
 	struct tree *source_tree;
+
+	const char *new_worktree;
+	const char **saved_argv;
+	int new_worktree_mode;
 };
 
 static int post_checkout_hook(struct commit *old, struct commit *new,
@@ -249,6 +253,9 @@ static int checkout_paths(const struct checkout_opts *opts,
 		die(_("Cannot update paths and switch to branch '%s' at the same time."),
 		    opts->new_branch);
 
+	if (opts->new_worktree)
+		die(_("'%s' cannot be used with updating paths"), "--to");
+
 	if (opts->patch_mode)
 		return run_add_interactive(revision, "--patch=checkout",
 					   &opts->pathspec);
@@ -484,7 +491,7 @@ static int merge_working_tree(const struct checkout_opts *opts,
 			topts.dir->flags |= DIR_SHOW_IGNORED;
 			setup_standard_excludes(topts.dir);
 		}
-		tree = parse_tree_indirect(old->commit ?
+		tree = parse_tree_indirect(old->commit && !opts->new_worktree_mode ?
 					   old->commit->object.sha1 :
 					   EMPTY_TREE_SHA1_BIN);
 		init_tree_desc(&trees[0], tree->buffer, tree->size);
@@ -800,7 +807,8 @@ static int switch_branches(const struct checkout_opts *opts,
 		return ret;
 	}
 
-	if (!opts->quiet && !old.path && old.commit && new->commit != old.commit)
+	if (!opts->quiet && !old.path && old.commit &&
+	    new->commit != old.commit && !opts->new_worktree_mode)
 		orphaned_commit_warning(old.commit, new->commit);
 
 	update_refs_for_switch(opts, &old, new);
@@ -810,6 +818,76 @@ static int switch_branches(const struct checkout_opts *opts,
 	return ret || writeout_error;
 }
 
+static int prepare_linked_checkout(const struct checkout_opts *opts,
+				   struct branch_info *new)
+{
+	struct strbuf sb_git = STRBUF_INIT, sb_repo = STRBUF_INIT;
+	struct strbuf sb = STRBUF_INIT;
+	const char *path = opts->new_worktree, *name;
+	struct stat st;
+	struct child_process cp;
+	int counter = 0, len;
+
+	if (!new->commit)
+		die(_("no branch specified"));
+	if (file_exists(path))
+		die(_("'%s' already exists"), path);
+
+	len = strlen(path);
+	while (len && is_dir_sep(path[len - 1]))
+		len--;
+
+	for (name = path + len - 1; name > path; name--)
+		if (is_dir_sep(*name)) {
+			name++;
+			break;
+		}
+	strbuf_addstr(&sb_repo,
+		      git_path("worktrees/%.*s", (int)(path + len - name), name));
+	len = sb_repo.len;
+	if (safe_create_leading_directories_const(sb_repo.buf))
+		die_errno(_("could not create leading directories of '%s'"),
+			  sb_repo.buf);
+	while (!stat(sb_repo.buf, &st)) {
+		counter++;
+		strbuf_setlen(&sb_repo, len);
+		strbuf_addf(&sb_repo, "%d", counter);
+	}
+	name = strrchr(sb_repo.buf, '/') + 1;
+	if (mkdir(sb_repo.buf, 0777))
+		die_errno(_("could not create directory of '%s'"), sb_repo.buf);
+
+	strbuf_addf(&sb_git, "%s/.git", path);
+	if (safe_create_leading_directories_const(sb_git.buf))
+		die_errno(_("could not create leading directories of '%s'"),
+			  sb_git.buf);
+
+	write_file(sb_git.buf, 1, "gitdir: %s/worktrees/%s\n",
+		   real_path(get_git_common_dir()), name);
+	/*
+	 * This is to keep resolve_ref() happy. We need a valid HEAD
+	 * or is_git_directory() will reject the directory. Any valid
+	 * value would do because this value will be ignored and
+	 * replaced at the next (real) checkout.
+	 */
+	strbuf_addf(&sb, "%s/HEAD", sb_repo.buf);
+	write_file(sb.buf, 1, "%s\n", sha1_to_hex(new->commit->object.sha1));
+	strbuf_reset(&sb);
+	strbuf_addf(&sb, "%s/commondir", sb_repo.buf);
+	write_file(sb.buf, 1, "../..\n");
+
+	if (!opts->quiet)
+		fprintf_ln(stderr, _("Enter %s (identifier %s)"), path, name);
+
+	setenv("GIT_CHECKOUT_NEW_WORKTREE", "1", 1);
+	setenv(GIT_DIR_ENVIRONMENT, sb_git.buf, 1);
+	setenv(GIT_WORK_TREE_ENVIRONMENT, path, 1);
+	memset(&cp, 0, sizeof(cp));
+	cp.git_cmd = 1;
+	cp.argv = opts->saved_argv;
+	return run_command(&cp);
+}
+
 static int git_checkout_config(const char *var, const char *value, void *cb)
 {
 	if (!strcmp(var, "diff.ignoresubmodules")) {
@@ -1071,6 +1149,9 @@ static int checkout_branch(struct checkout_opts *opts,
 		die(_("Cannot switch branch to a non-commit '%s'"),
 		    new->name);
 
+	if (opts->new_worktree)
+		return prepare_linked_checkout(opts, new);
+
 	if (!new->commit && opts->new_branch) {
 		unsigned char rev[20];
 		int flag;
@@ -1113,6 +1194,8 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
 			 N_("do not limit pathspecs to sparse entries only")),
 		OPT_HIDDEN_BOOL(0, "guess", &dwim_new_local_branch,
 				N_("second guess 'git checkout no-such-branch'")),
+		OPT_FILENAME(0, "to", &opts.new_worktree,
+			   N_("check a branch out in a separate working directory")),
 		OPT_END(),
 	};
 
@@ -1121,6 +1204,9 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
 	opts.overwrite_ignore = 1;
 	opts.prefix = prefix;
 
+	opts.saved_argv = xmalloc(sizeof(const char *) * (argc + 2));
+	memcpy(opts.saved_argv, argv, sizeof(const char *) * (argc + 1));
+
 	gitmodules_config();
 	git_config(git_checkout_config, &opts);
 
@@ -1129,6 +1215,11 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
 	argc = parse_options(argc, argv, prefix, options, checkout_usage,
 			     PARSE_OPT_KEEP_DASHDASH);
 
+	/* recursive execution from checkout_new_worktree() */
+	opts.new_worktree_mode = getenv("GIT_CHECKOUT_NEW_WORKTREE") != NULL;
+	if (opts.new_worktree_mode)
+		opts.new_worktree = NULL;
+
 	if (conflict_style) {
 		opts.merge = 1; /* implied */
 		git_xmerge_config("merge.conflictstyle", conflict_style, NULL);
diff --git a/path.c b/path.c
index 94db501..72eca6d 100644
--- a/path.c
+++ b/path.c
@@ -92,7 +92,7 @@ static void replace_dir(struct strbuf *buf, int len, const char *newdir)
 
 static const char *common_list[] = {
 	"/branches", "/hooks", "/info", "/logs", "/lost-found", "/modules",
-	"/objects", "/refs", "/remotes", "/rr-cache", "/svn",
+	"/objects", "/refs", "/remotes", "/worktrees", "/rr-cache", "/svn",
 	"config", "gc.pid", "packed-refs", "shallow",
 	NULL
 };
diff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh
new file mode 100755
index 0000000..8c73b18
--- /dev/null
+++ b/t/t2025-checkout-to.sh
@@ -0,0 +1,63 @@
+#!/bin/sh
+
+test_description='test git checkout --to'
+
+. ./test-lib.sh
+
+test_expect_success 'setup' '
+	test_commit init
+'
+
+test_expect_success 'checkout --to not updating paths' '
+	test_must_fail git checkout --to -- init.t
+'
+
+test_expect_success 'checkout --to an existing worktree' '
+	mkdir existing &&
+	test_must_fail git checkout --detach --to existing master
+'
+
+test_expect_success 'checkout --to a new worktree' '
+	git checkout --to here master &&
+	(
+		cd here &&
+		test_cmp ../init.t init.t &&
+		git symbolic-ref HEAD >actual &&
+		echo refs/heads/master >expect &&
+		test_cmp expect actual &&
+		git fsck
+	)
+'
+
+test_expect_success 'checkout --to a new worktree from a subdir' '
+	(
+		mkdir sub &&
+		cd sub &&
+		git checkout --detach --to here master &&
+		cd here &&
+		test_cmp ../../init.t init.t
+	)
+'
+
+test_expect_success 'checkout --to from a linked checkout' '
+	(
+		cd here &&
+		git checkout --to nested-here master
+		cd nested-here &&
+		git fsck
+	)
+'
+
+test_expect_success 'checkout --to a new worktree creating new branch' '
+	git checkout --to there -b newmaster master &&
+	(
+		cd there &&
+		test_cmp ../init.t init.t &&
+		git symbolic-ref HEAD >actual &&
+		echo refs/heads/newmaster >expect &&
+		test_cmp expect actual &&
+		git fsck
+	)
+'
+
+test_done
-- 
2.1.0.rc0.78.gc0d8480
-- 8<--

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* Re: [PATCH v2 23/32] prune: strategies for linked checkouts
  2014-09-12  3:06       ` Eric Sunshine
  2014-09-12 19:08         ` Marc Branchaud
@ 2014-09-21  2:54         ` Duy Nguyen
  2014-09-21  3:01           ` Eric Sunshine
  1 sibling, 1 reply; 134+ messages in thread
From: Duy Nguyen @ 2014-09-21  2:54 UTC (permalink / raw)
  To: Eric Sunshine; +Cc: Marc Branchaud, Git List, Junio C Hamano

On Fri, Sep 12, 2014 at 10:06 AM, Eric Sunshine <sunshine@sunshineco.com> wrote:
> One minor addition for clarification would be to mention that the 'git
> prune --worktrees' invocation applies to the main worktree:
>
>     When you are done with a linked working tree, you can simply delete
>     it. You can clean up any stale $GIT_DIR/worktrees entries via
>     `git prune --worktrees` in the main worktree.


The command should work from any worktree (of course not the worktree
that you just deleted). I try to make all operations work on all
worktrees. How should I write this?

>>> +To stop `git prune --worktrees` from deleting a specific working
>>> +directory (e.g. because it's on a portable device), you could add the
>>> +file 'locked' to $GIT_DIR/worktrees. For example, if `.git` file of
>>> +the new working directory points to `/path/main/worktrees/test-next`,
>>> +the full path of the 'locked' file would be
>>> +`/path/main/worktrees/test-next/locked`. See
>>> +linkgit:gitrepository-layout[5] for details.
>>
>> Sorry, I can't help rewriting this one too:
>>
>>         To prevent `git prune --worktrees` from deleting a
>>         $GIT_DIR/worktrees entry (which can be useful in some situations,
>>         such as when the entry's working tree is stored on a portable
>>         device), add a file named 'locked' to the entry's directory.  For
>>         example, if a linked working tree's `.git` file points to
>>         `/path/main/.git/worktrees/test-next` then a file named
>>         `/path/main/.git/worktrees/test-next/locked` will prevent the
>>         `test-next` entry from being pruned.  See
>>         linkgit:gitrepository-layout[5] for details.
>
> Each time I read this (or Duy's original), the first question that
> pops into my head is "should 'locked' be empty or have content, and if
> content, what content?" gitrepository-layout.txt does explain the
> content (if the reader bothers to chase the link), but perhaps it is
> worth an explanatory sentence here?

Will do.
-- 
Duy

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH v2 23/32] prune: strategies for linked checkouts
  2014-09-21  2:54         ` Duy Nguyen
@ 2014-09-21  3:01           ` Eric Sunshine
  2014-09-21 10:29             ` Duy Nguyen
  0 siblings, 1 reply; 134+ messages in thread
From: Eric Sunshine @ 2014-09-21  3:01 UTC (permalink / raw)
  To: Duy Nguyen; +Cc: Marc Branchaud, Git List, Junio C Hamano

On Sat, Sep 20, 2014 at 10:54 PM, Duy Nguyen <pclouds@gmail.com> wrote:
> On Fri, Sep 12, 2014 at 10:06 AM, Eric Sunshine <sunshine@sunshineco.com> wrote:
>> One minor addition for clarification would be to mention that the 'git
>> prune --worktrees' invocation applies to the main worktree:
>>
>>     When you are done with a linked working tree, you can simply delete
>>     it. You can clean up any stale $GIT_DIR/worktrees entries via
>>     `git prune --worktrees` in the main worktree.
>
>
> The command should work from any worktree (of course not the worktree
> that you just deleted). I try to make all operations work on all
> worktrees. How should I write this?

I realized that a bit after writing the above. You could qualify it as:

    When you are done with a linked working tree, you can simply delete
    it. You can clean up any stale $GIT_DIR/worktrees entries via
    `git prune --worktrees` in the main worktree or any linked tree.

But the "...in the main worktree or any linked tree" is likely
redundant, in which case Marc's rewrite is probably sufficient.

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH v2 22/32] checkout: support checking out into a new working directory
  2014-09-21  2:41       ` Duy Nguyen
@ 2014-09-21  3:10         ` Eric Sunshine
  2014-09-21  9:50           ` Duy Nguyen
  0 siblings, 1 reply; 134+ messages in thread
From: Eric Sunshine @ 2014-09-21  3:10 UTC (permalink / raw)
  To: Duy Nguyen; +Cc: Marc Branchaud, Git List, Junio C Hamano

On Sat, Sep 20, 2014 at 10:41 PM, Duy Nguyen <pclouds@gmail.com> wrote:
> On Thu, Sep 11, 2014 at 11:02:36AM -0400, Marc Branchaud wrote:
>> Um, didn't you say in [1] that you'd use the text I posted in [2]?
>>
>> [1] http://article.gmane.org/gmane.comp.version-control.git/256446
>> [2] http://article.gmane.org/gmane.comp.version-control.git/256323
>
> That's the problem with updating a bit one day and a bit another
> day. Thanks for checking. Does this look ok?
>
> -- 8< --
> Subject: [PATCH] checkout: support checking out into a new working directory
>
> "git checkout --to" sets up a new working directory with a .git file
> pointing to $GIT_DIR/worktrees/<id>. It then executes "git checkout"
> again on the new worktree with the same arguments except "--to" is
> taken out. The second checkout execution, which is not contaminated
> with any info from the current repository, will actually check out and
> everything that normal "git checkout" does.
>
> Helped-by: Marc Branchaud <marcnarc@xiplink.com>
> Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
> ---
> diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt
> index 33ad2ad..c101575 100644
> --- a/Documentation/git-checkout.txt
> +++ b/Documentation/git-checkout.txt
> @@ -388,6 +395,45 @@ $ git reflog -2 HEAD # or
>  $ git log -g -2 HEAD
>  ------------
>
> +MULTIPLE WORKING TREES
> +----------------------
> +
> +A git repository can support multiple working trees, allowing you to check
> +out more than one branch at a time.  With `git checkout --to` a new working
> +tree is associated with the repository.  This new working tree is called a
> +"linked working tree" as opposed to the "main working tree" prepared by "git
> +init" or "git clone".  A repository has one main working tree (if it's not a
> +bare repository) and zero or more linked working trees.
> +
> +Each linked working tree has a private sub-directory in the repository's
> +$GIT_DIR/worktrees directory.  The private sub-directory's name is usually
> +the base name of the linked working tree's path, possibly appended with a
> +number to make it unique.  For example, when `$GIT_DIR=/path/main/.git` the
> +command `git checkout --to /path/other/test-next next` creates the linked
> +working tree in `/path/other/test-next` and also creates a
> +`$GIT_DIR/worktrees/test-next` directory (or `$GIT_DIR/worktrees/test-next1`
> +if `test-next` is already taken).
> +
> +Within a linked working tree, $GIT_DIR is set to point to this private
> +directory (e.g. `/path/main/.git/worktrees/test-next` in the example) and
> +$GIT_COMMON_DIR is set to point back to the main working tree's $GIT_DIR
> +(e.g. `/path/main/.git`). These settings are made in a `.git` file located at
> +the top directory of the linked working tree.
> +
> +Path resolution via `git rev-parse --git-path` uses either
> +$GIT_DIR or $GIT_COMMON_DIR depending on the path. For example, in the
> +linked working tree `git rev-parse --git-path HEAD` returns
> +`/path/main/.git/worktrees/test-next/HEAD` (not
> +`/path/other/test-next/.git/HEAD` or `/path/main/.git/HEAD`) while `git
> +rev-parse --git-path refs/heads/master` uses
> +$GIT_COMMON_DIR and returns `/path/main/.git/refs/heads/master`,
> +since refs are shared across all working trees.
> +
> +See linkgit:gitrepository-layout[5] for more information. The rule of
> +thumb is do not make any assumption about whether a path belongs to
> +$GIT_DIR or $GIT_COMMON_DIR when you need to directly access something
> +inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.

Would it make sense for this "rule of thumb" summary to be presented
first, and then the explanation of that rule after, rather than the
reverse as is currently the case?

> diff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh
> new file mode 100755
> index 0000000..8c73b18
> --- /dev/null
> +++ b/t/t2025-checkout-to.sh
> @@ -0,0 +1,63 @@
> +test_expect_success 'checkout --to from a linked checkout' '
> +       (
> +               cd here &&
> +               git checkout --to nested-here master

Broken &&-chain.

> +               cd nested-here &&
> +               git fsck
> +       )
> +'
> +
> +test_expect_success 'checkout --to a new worktree creating new branch' '
> +       git checkout --to there -b newmaster master &&
> +       (
> +               cd there &&
> +               test_cmp ../init.t init.t &&
> +               git symbolic-ref HEAD >actual &&
> +               echo refs/heads/newmaster >expect &&
> +               test_cmp expect actual &&
> +               git fsck
> +       )
> +'
> +
> +test_done
> --
> 2.1.0.rc0.78.gc0d8480

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH v2 22/32] checkout: support checking out into a new working directory
  2014-09-21  3:10         ` Eric Sunshine
@ 2014-09-21  9:50           ` Duy Nguyen
  2014-09-22 21:00             ` Marc Branchaud
  0 siblings, 1 reply; 134+ messages in thread
From: Duy Nguyen @ 2014-09-21  9:50 UTC (permalink / raw)
  To: Eric Sunshine; +Cc: Marc Branchaud, Git List, Junio C Hamano

On Sun, Sep 21, 2014 at 10:10 AM, Eric Sunshine <sunshine@sunshineco.com> wrote:
> Would it make sense for this "rule of thumb" summary to be presented
> first, and then the explanation of that rule after, rather than the
> reverse as is currently the case?

You mean like this?

diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt
index c101575..fd77631 100644
--- a/Documentation/git-checkout.txt
+++ b/Documentation/git-checkout.txt
@@ -420,6 +420,11 @@ $GIT_COMMON_DIR is set to point back to the main
working tree's $GIT_DIR
 (e.g. `/path/main/.git`). These settings are made in a `.git` file located at
 the top directory of the linked working tree.

+See linkgit:gitrepository-layout[5] for more information. The rule of
+thumb is do not make any assumption about whether a path belongs to
+$GIT_DIR or $GIT_COMMON_DIR when you need to directly access something
+inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.
+
 Path resolution via `git rev-parse --git-path` uses either
 $GIT_DIR or $GIT_COMMON_DIR depending on the path. For example, in the
 linked working tree `git rev-parse --git-path HEAD` returns
@@ -429,11 +434,6 @@ rev-parse --git-path refs/heads/master` uses
 $GIT_COMMON_DIR and returns `/path/main/.git/refs/heads/master`,
 since refs are shared across all working trees.

-See linkgit:gitrepository-layout[5] for more information. The rule of
-thumb is do not make any assumption about whether a path belongs to
-$GIT_DIR or $GIT_COMMON_DIR when you need to directly access something
-inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.
-
 EXAMPLES
 --------



-- 
Duy

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* Re: [PATCH v2 23/32] prune: strategies for linked checkouts
  2014-09-21  3:01           ` Eric Sunshine
@ 2014-09-21 10:29             ` Duy Nguyen
  2014-09-22 21:06               ` Marc Branchaud
  2014-09-22 22:19               ` Eric Sunshine
  0 siblings, 2 replies; 134+ messages in thread
From: Duy Nguyen @ 2014-09-21 10:29 UTC (permalink / raw)
  To: Eric Sunshine, Marc Branchaud; +Cc: Git List, Junio C Hamano

Here we go again. Thanks both for the suggestions.

-- 8< --
Subject: [PATCH] prune: strategies for linked checkouts

(alias R=$GIT_COMMON_DIR/worktrees/<id>)

 - linked checkouts are supposed to keep its location in $R/gitdir up
   to date. The use case is auto fixup after a manual checkout move.

 - linked checkouts are supposed to update mtime of $R/gitdir. If
   $R/gitdir's mtime is older than a limit, and it points to nowhere,
   worktrees/<id> is to be pruned.

 - If $R/locked exists, worktrees/<id> is not supposed to be pruned. If
   $R/locked exists and $R/gitdir's mtime is older than a really long
   limit, warn about old unused repo.

 - "git checkout --to" is supposed to make a hard link named $R/link
   pointing to the .git file on supported file systems to help detect
   the user manually deleting the checkout. If $R/link exists and its
   link count is greated than 1, the repo is kept.

Helped-by: Marc Branchaud <marcnarc@xiplink.com>
Helped-by: Eric Sunshine <sunshine@sunshineco.com>
Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
---
 Documentation/git-checkout.txt             | 20 +++++++
 Documentation/git-prune.txt                |  3 +
 Documentation/gitrepository-layout.txt     | 19 ++++++
 builtin/checkout.c                         | 19 +++++-
 builtin/prune.c                            | 95 ++++++++++++++++++++++++++++++
 setup.c                                    | 13 ++++
 t/t2026-prune-linked-checkouts.sh (new +x) | 84 ++++++++++++++++++++++++++
 7 files changed, 251 insertions(+), 2 deletions(-)
 create mode 100755 t/t2026-prune-linked-checkouts.sh

diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt
index c101575..0fd3bab 100644
--- a/Documentation/git-checkout.txt
+++ b/Documentation/git-checkout.txt
@@ -434,6 +434,26 @@ thumb is do not make any assumption about whether a path belongs to
 $GIT_DIR or $GIT_COMMON_DIR when you need to directly access something
 inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.
 
+When you are done with a linked working tree you can simply delete it.
+You can clean up any stale $GIT_DIR/worktrees entries via `git prune
+--worktrees` in the main worktree or any linked worktree.
+
+If you move a linked working directory to another file system, or
+within a file system that does not support hard links, you need to run
+at least one git command inside the linked working directory
+(e.g. `git status`) in order to update its entry in $GIT_DIR/worktrees
+so that it does not get automatically removed.
+
+To prevent `git prune --worktrees` from deleting a $GIT_DIR/worktrees
+entry (which can be useful in some situations, such as when the
+entry's working tree is stored on a portable device), add a file named
+'locked' to the entry's directory. The file contains the reason in
+plain text. For example, if a linked working tree's `.git` file points
+to `/path/main/.git/worktrees/test-next` then a file named
+`/path/main/.git/worktrees/test-next/locked` will prevent the
+`test-next` entry from being pruned.  See
+linkgit:gitrepository-layout[5] for details.
+
 EXAMPLES
 --------
 
diff --git a/Documentation/git-prune.txt b/Documentation/git-prune.txt
index 7a493c8..a0ea381 100644
--- a/Documentation/git-prune.txt
+++ b/Documentation/git-prune.txt
@@ -48,6 +48,9 @@ OPTIONS
 --expire <time>::
 	Only expire loose objects older than <time>.
 
+--worktrees::
+	Prune stale worktree information in $GIT_DIR/worktrees.
+
 <head>...::
 	In addition to objects
 	reachable from any of our references, keep objects
diff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt
index 8228450..2b30a92 100644
--- a/Documentation/gitrepository-layout.txt
+++ b/Documentation/gitrepository-layout.txt
@@ -259,6 +259,25 @@ worktrees::
 	$GIT_COMMON_DIR is set and "$GIT_COMMON_DIR/worktrees" will be
 	used instead.
 
+worktrees/<id>/gitdir::
+	A text file containing the absolute path back to the .git file
+	that points to here. This is used to check if the linked
+	repository has been manually removed and there is no need to
+	keep this directory any more. mtime of this file should be
+	updated every time the linked repository is accessed.
+
+worktrees/<id>/locked::
+	If this file exists, the linked repository may be on a
+	portable device and not available. It does not mean that the
+	linked repository is gone and `worktrees/<id>` could be
+	removed. The file's content contains a reason string on why
+	the repository is locked.
+
+worktrees/<id>/link::
+	If this file exists, it is a hard link to the linked .git
+	file. It is used to detect if the linked repository is
+	manually removed.
+
 SEE ALSO
 --------
 linkgit:git-init[1],
diff --git a/builtin/checkout.c b/builtin/checkout.c
index ad10f99..ab46af9 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -826,7 +826,7 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
 	const char *path = opts->new_worktree, *name;
 	struct stat st;
 	struct child_process cp;
-	int counter = 0, len;
+	int counter = 0, len, ret;
 
 	if (!new->commit)
 		die(_("no branch specified"));
@@ -857,11 +857,21 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
 	if (mkdir(sb_repo.buf, 0777))
 		die_errno(_("could not create directory of '%s'"), sb_repo.buf);
 
+	/*
+	 * lock the incomplete repo so prune won't delete it, unlock
+	 * after the preparation is over.
+	 */
+	strbuf_addf(&sb, "%s/locked", sb_repo.buf);
+	write_file(sb.buf, 1, "initializing\n");
+
 	strbuf_addf(&sb_git, "%s/.git", path);
 	if (safe_create_leading_directories_const(sb_git.buf))
 		die_errno(_("could not create leading directories of '%s'"),
 			  sb_git.buf);
 
+	strbuf_reset(&sb);
+	strbuf_addf(&sb, "%s/gitdir", sb_repo.buf);
+	write_file(sb.buf, 1, "%s\n", real_path(sb_git.buf));
 	write_file(sb_git.buf, 1, "gitdir: %s/worktrees/%s\n",
 		   real_path(get_git_common_dir()), name);
 	/*
@@ -870,6 +880,7 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
 	 * value would do because this value will be ignored and
 	 * replaced at the next (real) checkout.
 	 */
+	strbuf_reset(&sb);
 	strbuf_addf(&sb, "%s/HEAD", sb_repo.buf);
 	write_file(sb.buf, 1, "%s\n", sha1_to_hex(new->commit->object.sha1));
 	strbuf_reset(&sb);
@@ -885,7 +896,11 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
 	memset(&cp, 0, sizeof(cp));
 	cp.git_cmd = 1;
 	cp.argv = opts->saved_argv;
-	return run_command(&cp);
+	ret = run_command(&cp);
+	strbuf_reset(&sb);
+	strbuf_addf(&sb, "%s/locked", sb_repo.buf);
+	unlink_or_warn(sb.buf);
+	return ret;
 }
 
 static int git_checkout_config(const char *var, const char *value, void *cb)
diff --git a/builtin/prune.c b/builtin/prune.c
index 144a3bd..cf56110 100644
--- a/builtin/prune.c
+++ b/builtin/prune.c
@@ -112,6 +112,91 @@ static void prune_object_dir(const char *path)
 	}
 }
 
+static int prune_worktree(const char *id, struct strbuf *reason)
+{
+	struct stat st;
+	char *path;
+	int fd, len;
+
+	if (!is_directory(git_path("worktrees/%s", id))) {
+		strbuf_addf(reason, _("Removing worktrees/%s: not a valid directory"), id);
+		return 1;
+	}
+	if (file_exists(git_path("worktrees/%s/locked", id)))
+		return 0;
+	if (stat(git_path("worktrees/%s/gitdir", id), &st)) {
+		strbuf_addf(reason, _("Removing worktrees/%s: gitdir file does not exist"), id);
+		return 1;
+	}
+	fd = open(git_path("worktrees/%s/gitdir", id), O_RDONLY);
+	if (fd < 0) {
+		strbuf_addf(reason, _("Removing worktrees/%s: unable to read gitdir file (%s)"),
+			    id, strerror(errno));
+		return 1;
+	}
+	len = st.st_size;
+	path = xmalloc(len + 1);
+	read_in_full(fd, path, len);
+	close(fd);
+	while (len && (path[len - 1] == '\n' || path[len - 1] == '\r'))
+		len--;
+	if (!len) {
+		strbuf_addf(reason, _("Removing worktrees/%s: invalid gitdir file"), id);
+		free(path);
+		return 1;
+	}
+	path[len] = '\0';
+	if (!file_exists(path)) {
+		struct stat st_link;
+		free(path);
+		/*
+		 * the repo is moved manually and has not been
+		 * accessed since?
+		 */
+		if (!stat(git_path("worktrees/%s/link", id), &st_link) &&
+		    st_link.st_nlink > 1)
+			return 0;
+		strbuf_addf(reason, _("Removing worktrees/%s: gitdir file points to non-existent location"), id);
+		return 1;
+	}
+	free(path);
+	return st.st_mtime <= expire;
+}
+
+static void prune_worktrees(void)
+{
+	struct strbuf reason = STRBUF_INIT;
+	struct strbuf path = STRBUF_INIT;
+	DIR *dir = opendir(git_path("worktrees"));
+	struct dirent *d;
+	int ret;
+	if (!dir)
+		return;
+	while ((d = readdir(dir)) != NULL) {
+		if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, ".."))
+			continue;
+		strbuf_reset(&reason);
+		if (!prune_worktree(d->d_name, &reason))
+			continue;
+		if (show_only || verbose)
+			printf("%s\n", reason.buf);
+		if (show_only)
+			continue;
+		strbuf_reset(&path);
+		strbuf_addstr(&path, git_path("worktrees/%s", d->d_name));
+		ret = remove_dir_recursively(&path, 0);
+		if (ret < 0 && errno == ENOTDIR)
+			ret = unlink(path.buf);
+		if (ret)
+			error(_("failed to remove: %s"), strerror(errno));
+	}
+	closedir(dir);
+	if (!show_only)
+		rmdir(git_path("worktrees"));
+	strbuf_release(&reason);
+	strbuf_release(&path);
+}
+
 /*
  * Write errors (particularly out of space) can result in
  * failed temporary packs (and more rarely indexes and other
@@ -138,10 +223,12 @@ int cmd_prune(int argc, const char **argv, const char *prefix)
 {
 	struct rev_info revs;
 	struct progress *progress = NULL;
+	int do_prune_worktrees = 0;
 	const struct option options[] = {
 		OPT__DRY_RUN(&show_only, N_("do not remove, show only")),
 		OPT__VERBOSE(&verbose, N_("report pruned objects")),
 		OPT_BOOL(0, "progress", &show_progress, N_("show progress")),
+		OPT_BOOL(0, "worktrees", &do_prune_worktrees, N_("prune .git/worktrees")),
 		OPT_EXPIRY_DATE(0, "expire", &expire,
 				N_("expire objects older than <time>")),
 		OPT_END()
@@ -154,6 +241,14 @@ int cmd_prune(int argc, const char **argv, const char *prefix)
 	init_revisions(&revs, prefix);
 
 	argc = parse_options(argc, argv, prefix, options, prune_usage, 0);
+
+	if (do_prune_worktrees) {
+		if (argc)
+			die(_("--worktrees does not take extra arguments"));
+		prune_worktrees();
+		return 0;
+	}
+
 	while (argc--) {
 		unsigned char sha1[20];
 		const char *name = *argv++;
diff --git a/setup.c b/setup.c
index b99bdd3..fb61860 100644
--- a/setup.c
+++ b/setup.c
@@ -390,6 +390,17 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)
 	return ret;
 }
 
+static void update_linked_gitdir(const char *gitfile, const char *gitdir)
+{
+	struct strbuf path = STRBUF_INIT;
+	struct stat st;
+
+	strbuf_addf(&path, "%s/gitfile", gitdir);
+	if (stat(path.buf, &st) || st.st_mtime + 24 * 3600 < time(NULL))
+		write_file(path.buf, 0, "%s\n", gitfile);
+	strbuf_release(&path);
+}
+
 /*
  * Try to read the location of the git directory from the .git file,
  * return path to git directory if found.
@@ -438,6 +449,8 @@ const char *read_gitfile(const char *path)
 
 	if (!is_git_directory(dir))
 		die("Not a git repository: %s", dir);
+
+	update_linked_gitdir(path, dir);
 	path = real_path(dir);
 
 	free(buf);
diff --git a/t/t2026-prune-linked-checkouts.sh b/t/t2026-prune-linked-checkouts.sh
new file mode 100755
index 0000000..3622800
--- /dev/null
+++ b/t/t2026-prune-linked-checkouts.sh
@@ -0,0 +1,84 @@
+#!/bin/sh
+
+test_description='prune $GIT_DIR/worktrees'
+
+. ./test-lib.sh
+
+test_expect_success 'prune --worktrees on normal repo' '
+	git prune --worktrees &&
+	test_must_fail git prune --worktrees abc
+'
+
+test_expect_success 'prune files inside $GIT_DIR/worktrees' '
+	mkdir .git/worktrees &&
+	: >.git/worktrees/abc &&
+	git prune --worktrees --verbose >actual &&
+	cat >expect <<EOF &&
+Removing worktrees/abc: not a valid directory
+EOF
+	test_i18ncmp expect actual &&
+	! test -f .git/worktrees/abc &&
+	! test -d .git/worktrees
+'
+
+test_expect_success 'prune directories without gitdir' '
+	mkdir -p .git/worktrees/def/abc &&
+	: >.git/worktrees/def/def &&
+	cat >expect <<EOF &&
+Removing worktrees/def: gitdir file does not exist
+EOF
+	git prune --worktrees --verbose >actual &&
+	test_i18ncmp expect actual &&
+	! test -d .git/worktrees/def &&
+	! test -d .git/worktrees
+'
+
+test_expect_success POSIXPERM 'prune directories with unreadable gitdir' '
+	mkdir -p .git/worktrees/def/abc &&
+	: >.git/worktrees/def/def &&
+	: >.git/worktrees/def/gitdir &&
+	chmod u-r .git/worktrees/def/gitdir &&
+	git prune --worktrees --verbose >actual &&
+	test_i18ngrep "Removing worktrees/def: unable to read gitdir file" actual &&
+	! test -d .git/worktrees/def &&
+	! test -d .git/worktrees
+'
+
+test_expect_success 'prune directories with invalid gitdir' '
+	mkdir -p .git/worktrees/def/abc &&
+	: >.git/worktrees/def/def &&
+	: >.git/worktrees/def/gitdir &&
+	git prune --worktrees --verbose >actual &&
+	test_i18ngrep "Removing worktrees/def: invalid gitdir file" actual &&
+	! test -d .git/worktrees/def &&
+	! test -d .git/worktrees
+'
+
+test_expect_success 'prune directories with gitdir pointing to nowhere' '
+	mkdir -p .git/worktrees/def/abc &&
+	: >.git/worktrees/def/def &&
+	echo "$TRASH_DIRECTORY"/nowhere >.git/worktrees/def/gitdir &&
+	git prune --worktrees --verbose >actual &&
+	test_i18ngrep "Removing worktrees/def: gitdir file points to non-existent location" actual &&
+	! test -d .git/worktrees/def &&
+	! test -d .git/worktrees
+'
+
+test_expect_success 'not prune locked checkout' '
+	test_when_finished rm -r .git/worktrees
+	mkdir -p .git/worktrees/ghi &&
+	: >.git/worktrees/ghi/locked &&
+	git prune --worktrees &&
+	test -d .git/worktrees/ghi
+'
+
+test_expect_success 'not prune recent checkouts' '
+	test_when_finished rm -r .git/worktrees
+	mkdir zz &&
+	mkdir -p .git/worktrees/jlm &&
+	echo "$TRASH_DIRECTORY"/zz >.git/worktrees/jlm/gitdir &&
+	git prune --worktrees --verbose --expire=2.days.ago &&
+	test -d .git/worktrees/jlm
+'
+
+test_done
-- 
2.1.0.rc0.78.gc0d8480

-- 8< --

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* Re: [PATCH v2 28/32] gc: support prune --worktrees
  2014-09-10 22:42   ` [PATCH v2 28/32] gc: support prune --worktrees Nguyễn Thái Ngọc Duy
@ 2014-09-21 10:43     ` Duy Nguyen
  2014-09-22 21:06       ` Marc Branchaud
  0 siblings, 1 reply; 134+ messages in thread
From: Duy Nguyen @ 2014-09-21 10:43 UTC (permalink / raw)
  To: Marc Branchaud; +Cc: Junio C Hamano, git, Eric Sunshine

And this is the update as suggested in 23/32 [1]

[1] http://thread.gmane.org/gmane.comp.version-control.git/256210/focus=256849

-- 8< --
Subject: [PATCH] gc: support prune --worktrees

Helped-by: Marc Branchaud <marcnarc@xiplink.com>
Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
---
 Documentation/config.txt       |  7 +++++++
 Documentation/git-checkout.txt | 11 +++++++----
 builtin/gc.c                   | 10 ++++++++++
 3 files changed, 24 insertions(+), 4 deletions(-)

diff --git a/Documentation/config.txt b/Documentation/config.txt
index 98b8ef0..8351c8a 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -1212,6 +1212,13 @@ gc.pruneexpire::
 	"now" may be used to disable this  grace period and always prune
 	unreachable objects immediately.
 
+gc.pruneworktreesexpire::
+	When 'git gc' is run, it will call
+	'prune --worktrees --expire 3.months.ago'.
+	Override the grace period with this config variable. The value
+	"now" may be used to disable the grace period and prune
+	$GIT_DIR/worktrees immediately.
+
 gc.reflogexpire::
 gc.<pattern>.reflogexpire::
 	'git reflog expire' removes reflog entries older than
diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt
index 0fd3bab..6c14710 100644
--- a/Documentation/git-checkout.txt
+++ b/Documentation/git-checkout.txt
@@ -435,8 +435,11 @@ $GIT_DIR or $GIT_COMMON_DIR when you need to directly access something
 inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.
 
 When you are done with a linked working tree you can simply delete it.
-You can clean up any stale $GIT_DIR/worktrees entries via `git prune
---worktrees` in the main worktree or any linked worktree.
+The working tree's entry in the repository's $GIT_DIR/worktrees
+directory will eventually be removed automatically (see
+`gc.pruneworktreesexpire` in linkgit::git-config[1]), or you can run
+`git prune --worktrees` in the main worktree or any linked worktree to
+clean up any stale entries in $GIT_DIR/worktrees.
 
 If you move a linked working directory to another file system, or
 within a file system that does not support hard links, you need to run
@@ -444,8 +447,8 @@ at least one git command inside the linked working directory
 (e.g. `git status`) in order to update its entry in $GIT_DIR/worktrees
 so that it does not get automatically removed.
 
-To prevent `git prune --worktrees` from deleting a $GIT_DIR/worktrees
-entry (which can be useful in some situations, such as when the
+To prevent a $GIT_DIR/worktrees entry from from being pruned (which
+can be useful in some situations, such as when the
 entry's working tree is stored on a portable device), add a file named
 'locked' to the entry's directory. The file contains the reason in
 plain text. For example, if a linked working tree's `.git` file points
diff --git a/builtin/gc.c b/builtin/gc.c
index 849a87c..35542f3 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -33,11 +33,13 @@ static int gc_auto_threshold = 6700;
 static int gc_auto_pack_limit = 50;
 static int detach_auto = 1;
 static const char *prune_expire = "2.weeks.ago";
+static const char *prune_worktrees_expire = "3.months.ago";
 
 static struct argv_array pack_refs_cmd = ARGV_ARRAY_INIT;
 static struct argv_array reflog = ARGV_ARRAY_INIT;
 static struct argv_array repack = ARGV_ARRAY_INIT;
 static struct argv_array prune = ARGV_ARRAY_INIT;
+static struct argv_array prune_worktrees = ARGV_ARRAY_INIT;
 static struct argv_array rerere = ARGV_ARRAY_INIT;
 
 static char *pidfile;
@@ -83,6 +85,7 @@ static void gc_config(void)
 	git_config_get_int("gc.autopacklimit", &gc_auto_pack_limit);
 	git_config_get_bool("gc.autodetach", &detach_auto);
 	git_config_date_string("gc.pruneexpire", &prune_expire);
+	git_config_date_string("gc.pruneworktreesexpire", &prune_worktrees_expire);
 	git_config(git_default_config, NULL);
 }
 
@@ -290,6 +293,7 @@ int cmd_gc(int argc, const char **argv, const char *prefix)
 	argv_array_pushl(&reflog, "reflog", "expire", "--all", NULL);
 	argv_array_pushl(&repack, "repack", "-d", "-l", NULL);
 	argv_array_pushl(&prune, "prune", "--expire", NULL);
+	argv_array_pushl(&prune_worktrees, "prune", "--worktrees", "--expire", NULL);
 	argv_array_pushl(&rerere, "rerere", "gc", NULL);
 
 	gc_config();
@@ -359,6 +363,12 @@ int cmd_gc(int argc, const char **argv, const char *prefix)
 			return error(FAILED_RUN, prune.argv[0]);
 	}
 
+	if (prune_worktrees_expire) {
+		argv_array_push(&prune_worktrees, prune_worktrees_expire);
+		if (run_command_v_opt(prune_worktrees.argv, RUN_GIT_CMD))
+			return error(FAILED_RUN, prune_worktrees.argv[0]);
+	}
+
 	if (run_command_v_opt(rerere.argv, RUN_GIT_CMD))
 		return error(FAILED_RUN, rerere.argv[0]);
 
-- 
2.1.0.rc0.78.gc0d8480



-- 8< --

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* Re: [PATCH v2 22/32] checkout: support checking out into a new working directory
  2014-09-21  9:50           ` Duy Nguyen
@ 2014-09-22 21:00             ` Marc Branchaud
  2014-09-22 23:01               ` Eric Sunshine
  0 siblings, 1 reply; 134+ messages in thread
From: Marc Branchaud @ 2014-09-22 21:00 UTC (permalink / raw)
  To: Duy Nguyen, Eric Sunshine; +Cc: Git List, Junio C Hamano

On 14-09-21 05:50 AM, Duy Nguyen wrote:
> On Sun, Sep 21, 2014 at 10:10 AM, Eric Sunshine <sunshine@sunshineco.com> wrote:
>> Would it make sense for this "rule of thumb" summary to be presented
>> first, and then the explanation of that rule after, rather than the
>> reverse as is currently the case?
> 
> You mean like this?

*shrug*  To me it seems better to leave it at the end here.

If we really want people to follow this advice, it should appear where
GIT_DIR is officially documented.  I think this is the Environment Variables
section of the Documentation/git.txt file.  Probably GIT_COMMON_DIR should
also be documented there.

(Sorry, I don't have time to whip up a patch right now.)

		M.


> diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt
> index c101575..fd77631 100644
> --- a/Documentation/git-checkout.txt
> +++ b/Documentation/git-checkout.txt
> @@ -420,6 +420,11 @@ $GIT_COMMON_DIR is set to point back to the main
> working tree's $GIT_DIR
>  (e.g. `/path/main/.git`). These settings are made in a `.git` file located at
>  the top directory of the linked working tree.
> 
> +See linkgit:gitrepository-layout[5] for more information. The rule of
> +thumb is do not make any assumption about whether a path belongs to
> +$GIT_DIR or $GIT_COMMON_DIR when you need to directly access something
> +inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.
> +
>  Path resolution via `git rev-parse --git-path` uses either
>  $GIT_DIR or $GIT_COMMON_DIR depending on the path. For example, in the
>  linked working tree `git rev-parse --git-path HEAD` returns
> @@ -429,11 +434,6 @@ rev-parse --git-path refs/heads/master` uses
>  $GIT_COMMON_DIR and returns `/path/main/.git/refs/heads/master`,
>  since refs are shared across all working trees.
> 
> -See linkgit:gitrepository-layout[5] for more information. The rule of
> -thumb is do not make any assumption about whether a path belongs to
> -$GIT_DIR or $GIT_COMMON_DIR when you need to directly access something
> -inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.
> -
>  EXAMPLES
>  --------
> 
> 
> 

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH v2 23/32] prune: strategies for linked checkouts
  2014-09-21 10:29             ` Duy Nguyen
@ 2014-09-22 21:06               ` Marc Branchaud
  2014-09-22 22:19               ` Eric Sunshine
  1 sibling, 0 replies; 134+ messages in thread
From: Marc Branchaud @ 2014-09-22 21:06 UTC (permalink / raw)
  To: Duy Nguyen, Eric Sunshine; +Cc: Git List, Junio C Hamano

On 14-09-21 06:29 AM, Duy Nguyen wrote:
> Here we go again. Thanks both for the suggestions.

The documentation looks good to me.

FWIW:
Signed-off-by: Marc Branchaud <marcnarc@xiplink.com>

		M.


> -- 8< --
> Subject: [PATCH] prune: strategies for linked checkouts
> 
> (alias R=$GIT_COMMON_DIR/worktrees/<id>)
> 
>  - linked checkouts are supposed to keep its location in $R/gitdir up
>    to date. The use case is auto fixup after a manual checkout move.
> 
>  - linked checkouts are supposed to update mtime of $R/gitdir. If
>    $R/gitdir's mtime is older than a limit, and it points to nowhere,
>    worktrees/<id> is to be pruned.
> 
>  - If $R/locked exists, worktrees/<id> is not supposed to be pruned. If
>    $R/locked exists and $R/gitdir's mtime is older than a really long
>    limit, warn about old unused repo.
> 
>  - "git checkout --to" is supposed to make a hard link named $R/link
>    pointing to the .git file on supported file systems to help detect
>    the user manually deleting the checkout. If $R/link exists and its
>    link count is greated than 1, the repo is kept.
> 
> Helped-by: Marc Branchaud <marcnarc@xiplink.com>
> Helped-by: Eric Sunshine <sunshine@sunshineco.com>
> Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
> ---
>  Documentation/git-checkout.txt             | 20 +++++++
>  Documentation/git-prune.txt                |  3 +
>  Documentation/gitrepository-layout.txt     | 19 ++++++
>  builtin/checkout.c                         | 19 +++++-
>  builtin/prune.c                            | 95 ++++++++++++++++++++++++++++++
>  setup.c                                    | 13 ++++
>  t/t2026-prune-linked-checkouts.sh (new +x) | 84 ++++++++++++++++++++++++++
>  7 files changed, 251 insertions(+), 2 deletions(-)
>  create mode 100755 t/t2026-prune-linked-checkouts.sh
> 
> diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt
> index c101575..0fd3bab 100644
> --- a/Documentation/git-checkout.txt
> +++ b/Documentation/git-checkout.txt
> @@ -434,6 +434,26 @@ thumb is do not make any assumption about whether a path belongs to
>  $GIT_DIR or $GIT_COMMON_DIR when you need to directly access something
>  inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.
>  
> +When you are done with a linked working tree you can simply delete it.
> +You can clean up any stale $GIT_DIR/worktrees entries via `git prune
> +--worktrees` in the main worktree or any linked worktree.
> +
> +If you move a linked working directory to another file system, or
> +within a file system that does not support hard links, you need to run
> +at least one git command inside the linked working directory
> +(e.g. `git status`) in order to update its entry in $GIT_DIR/worktrees
> +so that it does not get automatically removed.
> +
> +To prevent `git prune --worktrees` from deleting a $GIT_DIR/worktrees
> +entry (which can be useful in some situations, such as when the
> +entry's working tree is stored on a portable device), add a file named
> +'locked' to the entry's directory. The file contains the reason in
> +plain text. For example, if a linked working tree's `.git` file points
> +to `/path/main/.git/worktrees/test-next` then a file named
> +`/path/main/.git/worktrees/test-next/locked` will prevent the
> +`test-next` entry from being pruned.  See
> +linkgit:gitrepository-layout[5] for details.
> +
>  EXAMPLES
>  --------
>  
> diff --git a/Documentation/git-prune.txt b/Documentation/git-prune.txt
> index 7a493c8..a0ea381 100644
> --- a/Documentation/git-prune.txt
> +++ b/Documentation/git-prune.txt
> @@ -48,6 +48,9 @@ OPTIONS
>  --expire <time>::
>  	Only expire loose objects older than <time>.
>  
> +--worktrees::
> +	Prune stale worktree information in $GIT_DIR/worktrees.
> +
>  <head>...::
>  	In addition to objects
>  	reachable from any of our references, keep objects
> diff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt
> index 8228450..2b30a92 100644
> --- a/Documentation/gitrepository-layout.txt
> +++ b/Documentation/gitrepository-layout.txt
> @@ -259,6 +259,25 @@ worktrees::
>  	$GIT_COMMON_DIR is set and "$GIT_COMMON_DIR/worktrees" will be
>  	used instead.
>  
> +worktrees/<id>/gitdir::
> +	A text file containing the absolute path back to the .git file
> +	that points to here. This is used to check if the linked
> +	repository has been manually removed and there is no need to
> +	keep this directory any more. mtime of this file should be
> +	updated every time the linked repository is accessed.
> +
> +worktrees/<id>/locked::
> +	If this file exists, the linked repository may be on a
> +	portable device and not available. It does not mean that the
> +	linked repository is gone and `worktrees/<id>` could be
> +	removed. The file's content contains a reason string on why
> +	the repository is locked.
> +
> +worktrees/<id>/link::
> +	If this file exists, it is a hard link to the linked .git
> +	file. It is used to detect if the linked repository is
> +	manually removed.
> +
>  SEE ALSO
>  --------
>  linkgit:git-init[1],
> diff --git a/builtin/checkout.c b/builtin/checkout.c
> index ad10f99..ab46af9 100644
> --- a/builtin/checkout.c
> +++ b/builtin/checkout.c
> @@ -826,7 +826,7 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
>  	const char *path = opts->new_worktree, *name;
>  	struct stat st;
>  	struct child_process cp;
> -	int counter = 0, len;
> +	int counter = 0, len, ret;
>  
>  	if (!new->commit)
>  		die(_("no branch specified"));
> @@ -857,11 +857,21 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
>  	if (mkdir(sb_repo.buf, 0777))
>  		die_errno(_("could not create directory of '%s'"), sb_repo.buf);
>  
> +	/*
> +	 * lock the incomplete repo so prune won't delete it, unlock
> +	 * after the preparation is over.
> +	 */
> +	strbuf_addf(&sb, "%s/locked", sb_repo.buf);
> +	write_file(sb.buf, 1, "initializing\n");
> +
>  	strbuf_addf(&sb_git, "%s/.git", path);
>  	if (safe_create_leading_directories_const(sb_git.buf))
>  		die_errno(_("could not create leading directories of '%s'"),
>  			  sb_git.buf);
>  
> +	strbuf_reset(&sb);
> +	strbuf_addf(&sb, "%s/gitdir", sb_repo.buf);
> +	write_file(sb.buf, 1, "%s\n", real_path(sb_git.buf));
>  	write_file(sb_git.buf, 1, "gitdir: %s/worktrees/%s\n",
>  		   real_path(get_git_common_dir()), name);
>  	/*
> @@ -870,6 +880,7 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
>  	 * value would do because this value will be ignored and
>  	 * replaced at the next (real) checkout.
>  	 */
> +	strbuf_reset(&sb);
>  	strbuf_addf(&sb, "%s/HEAD", sb_repo.buf);
>  	write_file(sb.buf, 1, "%s\n", sha1_to_hex(new->commit->object.sha1));
>  	strbuf_reset(&sb);
> @@ -885,7 +896,11 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
>  	memset(&cp, 0, sizeof(cp));
>  	cp.git_cmd = 1;
>  	cp.argv = opts->saved_argv;
> -	return run_command(&cp);
> +	ret = run_command(&cp);
> +	strbuf_reset(&sb);
> +	strbuf_addf(&sb, "%s/locked", sb_repo.buf);
> +	unlink_or_warn(sb.buf);
> +	return ret;
>  }
>  
>  static int git_checkout_config(const char *var, const char *value, void *cb)
> diff --git a/builtin/prune.c b/builtin/prune.c
> index 144a3bd..cf56110 100644
> --- a/builtin/prune.c
> +++ b/builtin/prune.c
> @@ -112,6 +112,91 @@ static void prune_object_dir(const char *path)
>  	}
>  }
>  
> +static int prune_worktree(const char *id, struct strbuf *reason)
> +{
> +	struct stat st;
> +	char *path;
> +	int fd, len;
> +
> +	if (!is_directory(git_path("worktrees/%s", id))) {
> +		strbuf_addf(reason, _("Removing worktrees/%s: not a valid directory"), id);
> +		return 1;
> +	}
> +	if (file_exists(git_path("worktrees/%s/locked", id)))
> +		return 0;
> +	if (stat(git_path("worktrees/%s/gitdir", id), &st)) {
> +		strbuf_addf(reason, _("Removing worktrees/%s: gitdir file does not exist"), id);
> +		return 1;
> +	}
> +	fd = open(git_path("worktrees/%s/gitdir", id), O_RDONLY);
> +	if (fd < 0) {
> +		strbuf_addf(reason, _("Removing worktrees/%s: unable to read gitdir file (%s)"),
> +			    id, strerror(errno));
> +		return 1;
> +	}
> +	len = st.st_size;
> +	path = xmalloc(len + 1);
> +	read_in_full(fd, path, len);
> +	close(fd);
> +	while (len && (path[len - 1] == '\n' || path[len - 1] == '\r'))
> +		len--;
> +	if (!len) {
> +		strbuf_addf(reason, _("Removing worktrees/%s: invalid gitdir file"), id);
> +		free(path);
> +		return 1;
> +	}
> +	path[len] = '\0';
> +	if (!file_exists(path)) {
> +		struct stat st_link;
> +		free(path);
> +		/*
> +		 * the repo is moved manually and has not been
> +		 * accessed since?
> +		 */
> +		if (!stat(git_path("worktrees/%s/link", id), &st_link) &&
> +		    st_link.st_nlink > 1)
> +			return 0;
> +		strbuf_addf(reason, _("Removing worktrees/%s: gitdir file points to non-existent location"), id);
> +		return 1;
> +	}
> +	free(path);
> +	return st.st_mtime <= expire;
> +}
> +
> +static void prune_worktrees(void)
> +{
> +	struct strbuf reason = STRBUF_INIT;
> +	struct strbuf path = STRBUF_INIT;
> +	DIR *dir = opendir(git_path("worktrees"));
> +	struct dirent *d;
> +	int ret;
> +	if (!dir)
> +		return;
> +	while ((d = readdir(dir)) != NULL) {
> +		if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, ".."))
> +			continue;
> +		strbuf_reset(&reason);
> +		if (!prune_worktree(d->d_name, &reason))
> +			continue;
> +		if (show_only || verbose)
> +			printf("%s\n", reason.buf);
> +		if (show_only)
> +			continue;
> +		strbuf_reset(&path);
> +		strbuf_addstr(&path, git_path("worktrees/%s", d->d_name));
> +		ret = remove_dir_recursively(&path, 0);
> +		if (ret < 0 && errno == ENOTDIR)
> +			ret = unlink(path.buf);
> +		if (ret)
> +			error(_("failed to remove: %s"), strerror(errno));
> +	}
> +	closedir(dir);
> +	if (!show_only)
> +		rmdir(git_path("worktrees"));
> +	strbuf_release(&reason);
> +	strbuf_release(&path);
> +}
> +
>  /*
>   * Write errors (particularly out of space) can result in
>   * failed temporary packs (and more rarely indexes and other
> @@ -138,10 +223,12 @@ int cmd_prune(int argc, const char **argv, const char *prefix)
>  {
>  	struct rev_info revs;
>  	struct progress *progress = NULL;
> +	int do_prune_worktrees = 0;
>  	const struct option options[] = {
>  		OPT__DRY_RUN(&show_only, N_("do not remove, show only")),
>  		OPT__VERBOSE(&verbose, N_("report pruned objects")),
>  		OPT_BOOL(0, "progress", &show_progress, N_("show progress")),
> +		OPT_BOOL(0, "worktrees", &do_prune_worktrees, N_("prune .git/worktrees")),
>  		OPT_EXPIRY_DATE(0, "expire", &expire,
>  				N_("expire objects older than <time>")),
>  		OPT_END()
> @@ -154,6 +241,14 @@ int cmd_prune(int argc, const char **argv, const char *prefix)
>  	init_revisions(&revs, prefix);
>  
>  	argc = parse_options(argc, argv, prefix, options, prune_usage, 0);
> +
> +	if (do_prune_worktrees) {
> +		if (argc)
> +			die(_("--worktrees does not take extra arguments"));
> +		prune_worktrees();
> +		return 0;
> +	}
> +
>  	while (argc--) {
>  		unsigned char sha1[20];
>  		const char *name = *argv++;
> diff --git a/setup.c b/setup.c
> index b99bdd3..fb61860 100644
> --- a/setup.c
> +++ b/setup.c
> @@ -390,6 +390,17 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)
>  	return ret;
>  }
>  
> +static void update_linked_gitdir(const char *gitfile, const char *gitdir)
> +{
> +	struct strbuf path = STRBUF_INIT;
> +	struct stat st;
> +
> +	strbuf_addf(&path, "%s/gitfile", gitdir);
> +	if (stat(path.buf, &st) || st.st_mtime + 24 * 3600 < time(NULL))
> +		write_file(path.buf, 0, "%s\n", gitfile);
> +	strbuf_release(&path);
> +}
> +
>  /*
>   * Try to read the location of the git directory from the .git file,
>   * return path to git directory if found.
> @@ -438,6 +449,8 @@ const char *read_gitfile(const char *path)
>  
>  	if (!is_git_directory(dir))
>  		die("Not a git repository: %s", dir);
> +
> +	update_linked_gitdir(path, dir);
>  	path = real_path(dir);
>  
>  	free(buf);
> diff --git a/t/t2026-prune-linked-checkouts.sh b/t/t2026-prune-linked-checkouts.sh
> new file mode 100755
> index 0000000..3622800
> --- /dev/null
> +++ b/t/t2026-prune-linked-checkouts.sh
> @@ -0,0 +1,84 @@
> +#!/bin/sh
> +
> +test_description='prune $GIT_DIR/worktrees'
> +
> +. ./test-lib.sh
> +
> +test_expect_success 'prune --worktrees on normal repo' '
> +	git prune --worktrees &&
> +	test_must_fail git prune --worktrees abc
> +'
> +
> +test_expect_success 'prune files inside $GIT_DIR/worktrees' '
> +	mkdir .git/worktrees &&
> +	: >.git/worktrees/abc &&
> +	git prune --worktrees --verbose >actual &&
> +	cat >expect <<EOF &&
> +Removing worktrees/abc: not a valid directory
> +EOF
> +	test_i18ncmp expect actual &&
> +	! test -f .git/worktrees/abc &&
> +	! test -d .git/worktrees
> +'
> +
> +test_expect_success 'prune directories without gitdir' '
> +	mkdir -p .git/worktrees/def/abc &&
> +	: >.git/worktrees/def/def &&
> +	cat >expect <<EOF &&
> +Removing worktrees/def: gitdir file does not exist
> +EOF
> +	git prune --worktrees --verbose >actual &&
> +	test_i18ncmp expect actual &&
> +	! test -d .git/worktrees/def &&
> +	! test -d .git/worktrees
> +'
> +
> +test_expect_success POSIXPERM 'prune directories with unreadable gitdir' '
> +	mkdir -p .git/worktrees/def/abc &&
> +	: >.git/worktrees/def/def &&
> +	: >.git/worktrees/def/gitdir &&
> +	chmod u-r .git/worktrees/def/gitdir &&
> +	git prune --worktrees --verbose >actual &&
> +	test_i18ngrep "Removing worktrees/def: unable to read gitdir file" actual &&
> +	! test -d .git/worktrees/def &&
> +	! test -d .git/worktrees
> +'
> +
> +test_expect_success 'prune directories with invalid gitdir' '
> +	mkdir -p .git/worktrees/def/abc &&
> +	: >.git/worktrees/def/def &&
> +	: >.git/worktrees/def/gitdir &&
> +	git prune --worktrees --verbose >actual &&
> +	test_i18ngrep "Removing worktrees/def: invalid gitdir file" actual &&
> +	! test -d .git/worktrees/def &&
> +	! test -d .git/worktrees
> +'
> +
> +test_expect_success 'prune directories with gitdir pointing to nowhere' '
> +	mkdir -p .git/worktrees/def/abc &&
> +	: >.git/worktrees/def/def &&
> +	echo "$TRASH_DIRECTORY"/nowhere >.git/worktrees/def/gitdir &&
> +	git prune --worktrees --verbose >actual &&
> +	test_i18ngrep "Removing worktrees/def: gitdir file points to non-existent location" actual &&
> +	! test -d .git/worktrees/def &&
> +	! test -d .git/worktrees
> +'
> +
> +test_expect_success 'not prune locked checkout' '
> +	test_when_finished rm -r .git/worktrees
> +	mkdir -p .git/worktrees/ghi &&
> +	: >.git/worktrees/ghi/locked &&
> +	git prune --worktrees &&
> +	test -d .git/worktrees/ghi
> +'
> +
> +test_expect_success 'not prune recent checkouts' '
> +	test_when_finished rm -r .git/worktrees
> +	mkdir zz &&
> +	mkdir -p .git/worktrees/jlm &&
> +	echo "$TRASH_DIRECTORY"/zz >.git/worktrees/jlm/gitdir &&
> +	git prune --worktrees --verbose --expire=2.days.ago &&
> +	test -d .git/worktrees/jlm
> +'
> +
> +test_done
> 

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH v2 28/32] gc: support prune --worktrees
  2014-09-21 10:43     ` Duy Nguyen
@ 2014-09-22 21:06       ` Marc Branchaud
  0 siblings, 0 replies; 134+ messages in thread
From: Marc Branchaud @ 2014-09-22 21:06 UTC (permalink / raw)
  To: Duy Nguyen; +Cc: Junio C Hamano, git, Eric Sunshine

On 14-09-21 06:43 AM, Duy Nguyen wrote:
> And this is the update as suggested in 23/32 [1]
> 
> [1] http://thread.gmane.org/gmane.comp.version-control.git/256210/focus=256849

Looks good!

FWIW:
Signed-off-by: Marc Branchaud <marcnarc@xiplink.com>

		M.


> -- 8< --
> Subject: [PATCH] gc: support prune --worktrees
> 
> Helped-by: Marc Branchaud <marcnarc@xiplink.com>
> Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
> ---
>  Documentation/config.txt       |  7 +++++++
>  Documentation/git-checkout.txt | 11 +++++++----
>  builtin/gc.c                   | 10 ++++++++++
>  3 files changed, 24 insertions(+), 4 deletions(-)
> 
> diff --git a/Documentation/config.txt b/Documentation/config.txt
> index 98b8ef0..8351c8a 100644
> --- a/Documentation/config.txt
> +++ b/Documentation/config.txt
> @@ -1212,6 +1212,13 @@ gc.pruneexpire::
>  	"now" may be used to disable this  grace period and always prune
>  	unreachable objects immediately.
>  
> +gc.pruneworktreesexpire::
> +	When 'git gc' is run, it will call
> +	'prune --worktrees --expire 3.months.ago'.
> +	Override the grace period with this config variable. The value
> +	"now" may be used to disable the grace period and prune
> +	$GIT_DIR/worktrees immediately.
> +
>  gc.reflogexpire::
>  gc.<pattern>.reflogexpire::
>  	'git reflog expire' removes reflog entries older than
> diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt
> index 0fd3bab..6c14710 100644
> --- a/Documentation/git-checkout.txt
> +++ b/Documentation/git-checkout.txt
> @@ -435,8 +435,11 @@ $GIT_DIR or $GIT_COMMON_DIR when you need to directly access something
>  inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.
>  
>  When you are done with a linked working tree you can simply delete it.
> -You can clean up any stale $GIT_DIR/worktrees entries via `git prune
> ---worktrees` in the main worktree or any linked worktree.
> +The working tree's entry in the repository's $GIT_DIR/worktrees
> +directory will eventually be removed automatically (see
> +`gc.pruneworktreesexpire` in linkgit::git-config[1]), or you can run
> +`git prune --worktrees` in the main worktree or any linked worktree to
> +clean up any stale entries in $GIT_DIR/worktrees.
>  
>  If you move a linked working directory to another file system, or
>  within a file system that does not support hard links, you need to run
> @@ -444,8 +447,8 @@ at least one git command inside the linked working directory
>  (e.g. `git status`) in order to update its entry in $GIT_DIR/worktrees
>  so that it does not get automatically removed.
>  
> -To prevent `git prune --worktrees` from deleting a $GIT_DIR/worktrees
> -entry (which can be useful in some situations, such as when the
> +To prevent a $GIT_DIR/worktrees entry from from being pruned (which
> +can be useful in some situations, such as when the
>  entry's working tree is stored on a portable device), add a file named
>  'locked' to the entry's directory. The file contains the reason in
>  plain text. For example, if a linked working tree's `.git` file points
> diff --git a/builtin/gc.c b/builtin/gc.c
> index 849a87c..35542f3 100644
> --- a/builtin/gc.c
> +++ b/builtin/gc.c
> @@ -33,11 +33,13 @@ static int gc_auto_threshold = 6700;
>  static int gc_auto_pack_limit = 50;
>  static int detach_auto = 1;
>  static const char *prune_expire = "2.weeks.ago";
> +static const char *prune_worktrees_expire = "3.months.ago";
>  
>  static struct argv_array pack_refs_cmd = ARGV_ARRAY_INIT;
>  static struct argv_array reflog = ARGV_ARRAY_INIT;
>  static struct argv_array repack = ARGV_ARRAY_INIT;
>  static struct argv_array prune = ARGV_ARRAY_INIT;
> +static struct argv_array prune_worktrees = ARGV_ARRAY_INIT;
>  static struct argv_array rerere = ARGV_ARRAY_INIT;
>  
>  static char *pidfile;
> @@ -83,6 +85,7 @@ static void gc_config(void)
>  	git_config_get_int("gc.autopacklimit", &gc_auto_pack_limit);
>  	git_config_get_bool("gc.autodetach", &detach_auto);
>  	git_config_date_string("gc.pruneexpire", &prune_expire);
> +	git_config_date_string("gc.pruneworktreesexpire", &prune_worktrees_expire);
>  	git_config(git_default_config, NULL);
>  }
>  
> @@ -290,6 +293,7 @@ int cmd_gc(int argc, const char **argv, const char *prefix)
>  	argv_array_pushl(&reflog, "reflog", "expire", "--all", NULL);
>  	argv_array_pushl(&repack, "repack", "-d", "-l", NULL);
>  	argv_array_pushl(&prune, "prune", "--expire", NULL);
> +	argv_array_pushl(&prune_worktrees, "prune", "--worktrees", "--expire", NULL);
>  	argv_array_pushl(&rerere, "rerere", "gc", NULL);
>  
>  	gc_config();
> @@ -359,6 +363,12 @@ int cmd_gc(int argc, const char **argv, const char *prefix)
>  			return error(FAILED_RUN, prune.argv[0]);
>  	}
>  
> +	if (prune_worktrees_expire) {
> +		argv_array_push(&prune_worktrees, prune_worktrees_expire);
> +		if (run_command_v_opt(prune_worktrees.argv, RUN_GIT_CMD))
> +			return error(FAILED_RUN, prune_worktrees.argv[0]);
> +	}
> +
>  	if (run_command_v_opt(rerere.argv, RUN_GIT_CMD))
>  		return error(FAILED_RUN, rerere.argv[0]);
>  
> 

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH v2 23/32] prune: strategies for linked checkouts
  2014-09-21 10:29             ` Duy Nguyen
  2014-09-22 21:06               ` Marc Branchaud
@ 2014-09-22 22:19               ` Eric Sunshine
  1 sibling, 0 replies; 134+ messages in thread
From: Eric Sunshine @ 2014-09-22 22:19 UTC (permalink / raw)
  To: Duy Nguyen; +Cc: Marc Branchaud, Git List, Junio C Hamano

On Sun, Sep 21, 2014 at 6:29 AM, Duy Nguyen <pclouds@gmail.com> wrote:
> Here we go again. Thanks both for the suggestions.
>
> -- 8< --
> Subject: [PATCH] prune: strategies for linked checkouts
>
> (alias R=$GIT_COMMON_DIR/worktrees/<id>)
>
>  - linked checkouts are supposed to keep its location in $R/gitdir up
>    to date. The use case is auto fixup after a manual checkout move.
>
>  - linked checkouts are supposed to update mtime of $R/gitdir. If
>    $R/gitdir's mtime is older than a limit, and it points to nowhere,
>    worktrees/<id> is to be pruned.
>
>  - If $R/locked exists, worktrees/<id> is not supposed to be pruned. If
>    $R/locked exists and $R/gitdir's mtime is older than a really long
>    limit, warn about old unused repo.
>
>  - "git checkout --to" is supposed to make a hard link named $R/link
>    pointing to the .git file on supported file systems to help detect
>    the user manually deleting the checkout. If $R/link exists and its
>    link count is greated than 1, the repo is kept.
>
> Helped-by: Marc Branchaud <marcnarc@xiplink.com>
> Helped-by: Eric Sunshine <sunshine@sunshineco.com>
> Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
> ---
>  Documentation/git-checkout.txt             | 20 +++++++
>  Documentation/git-prune.txt                |  3 +
>  Documentation/gitrepository-layout.txt     | 19 ++++++
>  builtin/checkout.c                         | 19 +++++-
>  builtin/prune.c                            | 95 ++++++++++++++++++++++++++++++
>  setup.c                                    | 13 ++++
>  t/t2026-prune-linked-checkouts.sh (new +x) | 84 ++++++++++++++++++++++++++
>  7 files changed, 251 insertions(+), 2 deletions(-)
>  create mode 100755 t/t2026-prune-linked-checkouts.sh
>
> diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt
> index c101575..0fd3bab 100644
> --- a/Documentation/git-checkout.txt
> +++ b/Documentation/git-checkout.txt
> @@ -434,6 +434,26 @@ thumb is do not make any assumption about whether a path belongs to
>  $GIT_DIR or $GIT_COMMON_DIR when you need to directly access something
>  inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.
>
> +When you are done with a linked working tree you can simply delete it.
> +You can clean up any stale $GIT_DIR/worktrees entries via `git prune
> +--worktrees` in the main worktree or any linked work tree.

Nit: In the rest of the documentation, you have consistently spelled
out "working tree", so the (poor) example you copied from my email,
which says "worktree", is an anomaly. My bad. Plus, you have a mix of
"worktree" and "work tree". You probably want:

    ... in the main or any linked working tree.

> +If you move a linked working directory to another file system, or
> +within a file system that does not support hard links, you need to run
> +at least one git command inside the linked working directory
> +(e.g. `git status`) in order to update its entry in $GIT_DIR/worktrees
> +so that it does not get automatically removed.
> +
> +To prevent `git prune --worktrees` from deleting a $GIT_DIR/worktrees
> +entry (which can be useful in some situations, such as when the
> +entry's working tree is stored on a portable device), add a file named
> +'locked' to the entry's directory. The file contains the reason in
> +plain text. For example, if a linked working tree's `.git` file points
> +to `/path/main/.git/worktrees/test-next` then a file named
> +`/path/main/.git/worktrees/test-next/locked` will prevent the
> +`test-next` entry from being pruned.  See
> +linkgit:gitrepository-layout[5] for details.
> +
>  EXAMPLES
>  --------
>
> diff --git a/Documentation/git-prune.txt b/Documentation/git-prune.txt
> index 7a493c8..a0ea381 100644
> --- a/Documentation/git-prune.txt
> +++ b/Documentation/git-prune.txt
> @@ -48,6 +48,9 @@ OPTIONS
>  --expire <time>::
>         Only expire loose objects older than <time>.
>
> +--worktrees::
> +       Prune stale worktree information in $GIT_DIR/worktrees.
> +
>  <head>...::
>         In addition to objects
>         reachable from any of our references, keep objects
> diff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt
> index 8228450..2b30a92 100644
> --- a/Documentation/gitrepository-layout.txt
> +++ b/Documentation/gitrepository-layout.txt
> @@ -259,6 +259,25 @@ worktrees::
>         $GIT_COMMON_DIR is set and "$GIT_COMMON_DIR/worktrees" will be
>         used instead.
>
> +worktrees/<id>/gitdir::
> +       A text file containing the absolute path back to the .git file
> +       that points to here. This is used to check if the linked
> +       repository has been manually removed and there is no need to
> +       keep this directory any more. mtime of this file should be
> +       updated every time the linked repository is accessed.
> +
> +worktrees/<id>/locked::
> +       If this file exists, the linked repository may be on a
> +       portable device and not available. It does not mean that the
> +       linked repository is gone and `worktrees/<id>` could be
> +       removed. The file's content contains a reason string on why
> +       the repository is locked.
> +
> +worktrees/<id>/link::
> +       If this file exists, it is a hard link to the linked .git
> +       file. It is used to detect if the linked repository is
> +       manually removed.
> +
>  SEE ALSO
>  --------
>  linkgit:git-init[1],
> diff --git a/builtin/checkout.c b/builtin/checkout.c
> index ad10f99..ab46af9 100644
> --- a/builtin/checkout.c
> +++ b/builtin/checkout.c
> @@ -826,7 +826,7 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
>         const char *path = opts->new_worktree, *name;
>         struct stat st;
>         struct child_process cp;
> -       int counter = 0, len;
> +       int counter = 0, len, ret;
>
>         if (!new->commit)
>                 die(_("no branch specified"));
> @@ -857,11 +857,21 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
>         if (mkdir(sb_repo.buf, 0777))
>                 die_errno(_("could not create directory of '%s'"), sb_repo.buf);
>
> +       /*
> +        * lock the incomplete repo so prune won't delete it, unlock
> +        * after the preparation is over.
> +        */
> +       strbuf_addf(&sb, "%s/locked", sb_repo.buf);
> +       write_file(sb.buf, 1, "initializing\n");
> +
>         strbuf_addf(&sb_git, "%s/.git", path);
>         if (safe_create_leading_directories_const(sb_git.buf))
>                 die_errno(_("could not create leading directories of '%s'"),
>                           sb_git.buf);
>
> +       strbuf_reset(&sb);
> +       strbuf_addf(&sb, "%s/gitdir", sb_repo.buf);
> +       write_file(sb.buf, 1, "%s\n", real_path(sb_git.buf));
>         write_file(sb_git.buf, 1, "gitdir: %s/worktrees/%s\n",
>                    real_path(get_git_common_dir()), name);
>         /*
> @@ -870,6 +880,7 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
>          * value would do because this value will be ignored and
>          * replaced at the next (real) checkout.
>          */
> +       strbuf_reset(&sb);
>         strbuf_addf(&sb, "%s/HEAD", sb_repo.buf);
>         write_file(sb.buf, 1, "%s\n", sha1_to_hex(new->commit->object.sha1));
>         strbuf_reset(&sb);
> @@ -885,7 +896,11 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
>         memset(&cp, 0, sizeof(cp));
>         cp.git_cmd = 1;
>         cp.argv = opts->saved_argv;
> -       return run_command(&cp);
> +       ret = run_command(&cp);
> +       strbuf_reset(&sb);
> +       strbuf_addf(&sb, "%s/locked", sb_repo.buf);
> +       unlink_or_warn(sb.buf);
> +       return ret;
>  }
>
>  static int git_checkout_config(const char *var, const char *value, void *cb)
> diff --git a/builtin/prune.c b/builtin/prune.c
> index 144a3bd..cf56110 100644
> --- a/builtin/prune.c
> +++ b/builtin/prune.c
> @@ -112,6 +112,91 @@ static void prune_object_dir(const char *path)
>         }
>  }
>
> +static int prune_worktree(const char *id, struct strbuf *reason)
> +{
> +       struct stat st;
> +       char *path;
> +       int fd, len;
> +
> +       if (!is_directory(git_path("worktrees/%s", id))) {
> +               strbuf_addf(reason, _("Removing worktrees/%s: not a valid directory"), id);
> +               return 1;
> +       }
> +       if (file_exists(git_path("worktrees/%s/locked", id)))
> +               return 0;
> +       if (stat(git_path("worktrees/%s/gitdir", id), &st)) {
> +               strbuf_addf(reason, _("Removing worktrees/%s: gitdir file does not exist"), id);
> +               return 1;
> +       }
> +       fd = open(git_path("worktrees/%s/gitdir", id), O_RDONLY);
> +       if (fd < 0) {
> +               strbuf_addf(reason, _("Removing worktrees/%s: unable to read gitdir file (%s)"),
> +                           id, strerror(errno));
> +               return 1;
> +       }
> +       len = st.st_size;
> +       path = xmalloc(len + 1);
> +       read_in_full(fd, path, len);
> +       close(fd);
> +       while (len && (path[len - 1] == '\n' || path[len - 1] == '\r'))
> +               len--;
> +       if (!len) {
> +               strbuf_addf(reason, _("Removing worktrees/%s: invalid gitdir file"), id);
> +               free(path);
> +               return 1;
> +       }
> +       path[len] = '\0';
> +       if (!file_exists(path)) {
> +               struct stat st_link;
> +               free(path);
> +               /*
> +                * the repo is moved manually and has not been
> +                * accessed since?
> +                */
> +               if (!stat(git_path("worktrees/%s/link", id), &st_link) &&
> +                   st_link.st_nlink > 1)
> +                       return 0;
> +               strbuf_addf(reason, _("Removing worktrees/%s: gitdir file points to non-existent location"), id);
> +               return 1;
> +       }
> +       free(path);
> +       return st.st_mtime <= expire;
> +}
> +
> +static void prune_worktrees(void)
> +{
> +       struct strbuf reason = STRBUF_INIT;
> +       struct strbuf path = STRBUF_INIT;
> +       DIR *dir = opendir(git_path("worktrees"));
> +       struct dirent *d;
> +       int ret;
> +       if (!dir)
> +               return;
> +       while ((d = readdir(dir)) != NULL) {
> +               if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, ".."))
> +                       continue;
> +               strbuf_reset(&reason);
> +               if (!prune_worktree(d->d_name, &reason))
> +                       continue;
> +               if (show_only || verbose)
> +                       printf("%s\n", reason.buf);
> +               if (show_only)
> +                       continue;
> +               strbuf_reset(&path);
> +               strbuf_addstr(&path, git_path("worktrees/%s", d->d_name));
> +               ret = remove_dir_recursively(&path, 0);
> +               if (ret < 0 && errno == ENOTDIR)
> +                       ret = unlink(path.buf);
> +               if (ret)
> +                       error(_("failed to remove: %s"), strerror(errno));
> +       }
> +       closedir(dir);
> +       if (!show_only)
> +               rmdir(git_path("worktrees"));
> +       strbuf_release(&reason);
> +       strbuf_release(&path);
> +}
> +
>  /*
>   * Write errors (particularly out of space) can result in
>   * failed temporary packs (and more rarely indexes and other
> @@ -138,10 +223,12 @@ int cmd_prune(int argc, const char **argv, const char *prefix)
>  {
>         struct rev_info revs;
>         struct progress *progress = NULL;
> +       int do_prune_worktrees = 0;
>         const struct option options[] = {
>                 OPT__DRY_RUN(&show_only, N_("do not remove, show only")),
>                 OPT__VERBOSE(&verbose, N_("report pruned objects")),
>                 OPT_BOOL(0, "progress", &show_progress, N_("show progress")),
> +               OPT_BOOL(0, "worktrees", &do_prune_worktrees, N_("prune .git/worktrees")),
>                 OPT_EXPIRY_DATE(0, "expire", &expire,
>                                 N_("expire objects older than <time>")),
>                 OPT_END()
> @@ -154,6 +241,14 @@ int cmd_prune(int argc, const char **argv, const char *prefix)
>         init_revisions(&revs, prefix);
>
>         argc = parse_options(argc, argv, prefix, options, prune_usage, 0);
> +
> +       if (do_prune_worktrees) {
> +               if (argc)
> +                       die(_("--worktrees does not take extra arguments"));
> +               prune_worktrees();
> +               return 0;
> +       }
> +
>         while (argc--) {
>                 unsigned char sha1[20];
>                 const char *name = *argv++;
> diff --git a/setup.c b/setup.c
> index b99bdd3..fb61860 100644
> --- a/setup.c
> +++ b/setup.c
> @@ -390,6 +390,17 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)
>         return ret;
>  }
>
> +static void update_linked_gitdir(const char *gitfile, const char *gitdir)
> +{
> +       struct strbuf path = STRBUF_INIT;
> +       struct stat st;
> +
> +       strbuf_addf(&path, "%s/gitfile", gitdir);
> +       if (stat(path.buf, &st) || st.st_mtime + 24 * 3600 < time(NULL))
> +               write_file(path.buf, 0, "%s\n", gitfile);
> +       strbuf_release(&path);
> +}
> +
>  /*
>   * Try to read the location of the git directory from the .git file,
>   * return path to git directory if found.
> @@ -438,6 +449,8 @@ const char *read_gitfile(const char *path)
>
>         if (!is_git_directory(dir))
>                 die("Not a git repository: %s", dir);
> +
> +       update_linked_gitdir(path, dir);
>         path = real_path(dir);
>
>         free(buf);
> diff --git a/t/t2026-prune-linked-checkouts.sh b/t/t2026-prune-linked-checkouts.sh
> new file mode 100755
> index 0000000..3622800
> --- /dev/null
> +++ b/t/t2026-prune-linked-checkouts.sh
> @@ -0,0 +1,84 @@
> +#!/bin/sh
> +
> +test_description='prune $GIT_DIR/worktrees'
> +
> +. ./test-lib.sh
> +
> +test_expect_success 'prune --worktrees on normal repo' '
> +       git prune --worktrees &&
> +       test_must_fail git prune --worktrees abc
> +'
> +
> +test_expect_success 'prune files inside $GIT_DIR/worktrees' '
> +       mkdir .git/worktrees &&
> +       : >.git/worktrees/abc &&
> +       git prune --worktrees --verbose >actual &&
> +       cat >expect <<EOF &&
> +Removing worktrees/abc: not a valid directory
> +EOF
> +       test_i18ncmp expect actual &&
> +       ! test -f .git/worktrees/abc &&
> +       ! test -d .git/worktrees
> +'
> +
> +test_expect_success 'prune directories without gitdir' '
> +       mkdir -p .git/worktrees/def/abc &&
> +       : >.git/worktrees/def/def &&
> +       cat >expect <<EOF &&
> +Removing worktrees/def: gitdir file does not exist
> +EOF
> +       git prune --worktrees --verbose >actual &&
> +       test_i18ncmp expect actual &&
> +       ! test -d .git/worktrees/def &&
> +       ! test -d .git/worktrees
> +'
> +
> +test_expect_success POSIXPERM 'prune directories with unreadable gitdir' '
> +       mkdir -p .git/worktrees/def/abc &&
> +       : >.git/worktrees/def/def &&
> +       : >.git/worktrees/def/gitdir &&
> +       chmod u-r .git/worktrees/def/gitdir &&
> +       git prune --worktrees --verbose >actual &&
> +       test_i18ngrep "Removing worktrees/def: unable to read gitdir file" actual &&
> +       ! test -d .git/worktrees/def &&
> +       ! test -d .git/worktrees
> +'
> +
> +test_expect_success 'prune directories with invalid gitdir' '
> +       mkdir -p .git/worktrees/def/abc &&
> +       : >.git/worktrees/def/def &&
> +       : >.git/worktrees/def/gitdir &&
> +       git prune --worktrees --verbose >actual &&
> +       test_i18ngrep "Removing worktrees/def: invalid gitdir file" actual &&
> +       ! test -d .git/worktrees/def &&
> +       ! test -d .git/worktrees
> +'
> +
> +test_expect_success 'prune directories with gitdir pointing to nowhere' '
> +       mkdir -p .git/worktrees/def/abc &&
> +       : >.git/worktrees/def/def &&
> +       echo "$TRASH_DIRECTORY"/nowhere >.git/worktrees/def/gitdir &&
> +       git prune --worktrees --verbose >actual &&
> +       test_i18ngrep "Removing worktrees/def: gitdir file points to non-existent location" actual &&
> +       ! test -d .git/worktrees/def &&
> +       ! test -d .git/worktrees
> +'
> +
> +test_expect_success 'not prune locked checkout' '
> +       test_when_finished rm -r .git/worktrees
> +       mkdir -p .git/worktrees/ghi &&
> +       : >.git/worktrees/ghi/locked &&
> +       git prune --worktrees &&
> +       test -d .git/worktrees/ghi
> +'
> +
> +test_expect_success 'not prune recent checkouts' '
> +       test_when_finished rm -r .git/worktrees
> +       mkdir zz &&
> +       mkdir -p .git/worktrees/jlm &&
> +       echo "$TRASH_DIRECTORY"/zz >.git/worktrees/jlm/gitdir &&
> +       git prune --worktrees --verbose --expire=2.days.ago &&
> +       test -d .git/worktrees/jlm
> +'
> +
> +test_done
> --
> 2.1.0.rc0.78.gc0d8480
>
> -- 8< --

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH v2 22/32] checkout: support checking out into a new working directory
  2014-09-22 21:00             ` Marc Branchaud
@ 2014-09-22 23:01               ` Eric Sunshine
  0 siblings, 0 replies; 134+ messages in thread
From: Eric Sunshine @ 2014-09-22 23:01 UTC (permalink / raw)
  To: Marc Branchaud; +Cc: Duy Nguyen, Git List, Junio C Hamano

On Mon, Sep 22, 2014 at 5:00 PM, Marc Branchaud <marcnarc@xiplink.com> wrote:
> On 14-09-21 05:50 AM, Duy Nguyen wrote:
>> On Sun, Sep 21, 2014 at 10:10 AM, Eric Sunshine <sunshine@sunshineco.com> wrote:
>>> Would it make sense for this "rule of thumb" summary to be presented
>>> first, and then the explanation of that rule after, rather than the
>>> reverse as is currently the case?
>>
>> You mean like this?

Not quite. It was a genuine question rather than an outright
suggestion for change. It is typical to present a high-level overview
and then the low-level details. This case reverses that and presents
the details first and then the overview (in the form of --git-path).
The reader wades through several paragraphs of detailed explanation of
the low-level structure of the .git directory, only to be told at the
end to use --git-path and not worry about those details. Thus, the
organization struck me as a bit odd.

> *shrug*  To me it seems better to leave it at the end here.

The text does work as-is, and presents the necessary information; and
I don't want to turn this into a bike-shedding session, so feel free
to ignore my question.

^ permalink raw reply	[flat|nested] 134+ messages in thread

* Re: [PATCH v2 00/32] nd/multiple-work-trees
  2014-09-10 22:41 ` [PATCH v2 00/32] nd/multiple-work-trees Nguyễn Thái Ngọc Duy
                     ` (31 preceding siblings ...)
  2014-09-10 22:42   ` [PATCH v2 32/32] t2025: add a test to make sure grafts is working from a linked checkout Nguyễn Thái Ngọc Duy
@ 2014-09-25 21:20   ` Junio C Hamano
  2014-09-28  1:22     ` [PATCH v3 " Nguyễn Thái Ngọc Duy
  32 siblings, 1 reply; 134+ messages in thread
From: Junio C Hamano @ 2014-09-25 21:20 UTC (permalink / raw)
  To: Nguyễn Thái Ngọc Duy; +Cc: git

It has been a while since the last review exchanges were seen.  Will
it be time for v3 soon?

^ permalink raw reply	[flat|nested] 134+ messages in thread

* [PATCH v3 00/32] nd/multiple-work-trees
  2014-09-25 21:20   ` [PATCH v2 00/32] nd/multiple-work-trees Junio C Hamano
@ 2014-09-28  1:22     ` Nguyễn Thái Ngọc Duy
  2014-09-28  1:22       ` [PATCH v3 01/32] path.c: make get_pathname() return strbuf instead of static buffer Nguyễn Thái Ngọc Duy
                         ` (32 more replies)
  0 siblings, 33 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-28  1:22 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

On Fri, Sep 26, 2014 at 4:20 AM, Junio C Hamano <gitster@pobox.com> wrote:
> It has been a while since the last review exchanges were seen.  Will
> it be time for v3 soon?

Sorry I've been slow on picking up feedback from v2. v3 is rebased on
latest master. Other changes are mostly *.txt, and one broken &&
chain.

Dennis Kaarsemaker (1):
  checkout: don't require a work tree when checking out into a new one

Nguyễn Thái Ngọc Duy (31):
  path.c: make get_pathname() return strbuf instead of static buffer
  path.c: make get_pathname() call sites return const char *
  git_snpath(): retire and replace with strbuf_git_path()
  path.c: rename vsnpath() to do_git_path()
  path.c: group git_path(), git_pathdup() and strbuf_git_path() together
  git_path(): be aware of file relocation in $GIT_DIR
  *.sh: respect $GIT_INDEX_FILE
  reflog: avoid constructing .lock path with git_path
  fast-import: use git_path() for accessing .git dir instead of get_git_dir()
  commit: use SEQ_DIR instead of hardcoding "sequencer"
  $GIT_COMMON_DIR: a new environment variable
  git-sh-setup.sh: use rev-parse --git-path to get $GIT_DIR/objects
  *.sh: avoid hardcoding $GIT_DIR/hooks/...
  git-stash: avoid hardcoding $GIT_DIR/logs/....
  setup.c: convert is_git_directory() to use strbuf
  setup.c: detect $GIT_COMMON_DIR in is_git_directory()
  setup.c: convert check_repository_format_gently to use strbuf
  setup.c: detect $GIT_COMMON_DIR check_repository_format_gently()
  setup.c: support multi-checkout repo setup
  wrapper.c: wrapper to open a file, fprintf then close
  use new wrapper write_file() for simple file writing
  checkout: support checking out into a new working directory
  prune: strategies for linked checkouts
  checkout: reject if the branch is already checked out elsewhere
  checkout: clean up half-prepared directories in --to mode
  gc: style change -- no SP before closing parenthesis
  gc: factor out gc.pruneexpire parsing code
  gc: support prune --worktrees
  count-objects: report unused files in $GIT_DIR/worktrees/...
  git_path(): keep "info/sparse-checkout" per work-tree
  t2025: add a test to make sure grafts is working from a linked checkout

 Documentation/config.txt                   |   9 +
 Documentation/git-checkout.txt             |  69 ++++++++
 Documentation/git-prune.txt                |   3 +
 Documentation/git-rev-parse.txt            |  10 ++
 Documentation/git.txt                      |   9 +
 Documentation/gitrepository-layout.txt     |  78 +++++++--
 builtin/branch.c                           |   4 +-
 builtin/checkout.c                         | 260 ++++++++++++++++++++++++++++-
 builtin/clone.c                            |   9 +-
 builtin/commit.c                           |   2 +-
 builtin/count-objects.c                    |   4 +-
 builtin/fetch.c                            |   5 +-
 builtin/fsck.c                             |   4 +-
 builtin/gc.c                               |  34 ++--
 builtin/init-db.c                          |   7 +-
 builtin/prune.c                            |  95 +++++++++++
 builtin/receive-pack.c                     |   2 +-
 builtin/reflog.c                           |   2 +-
 builtin/remote.c                           |   2 +-
 builtin/repack.c                           |   8 +-
 builtin/rev-parse.c                        |  11 ++
 cache.h                                    |  17 +-
 daemon.c                                   |  11 +-
 environment.c                              |  33 +++-
 fast-import.c                              |   7 +-
 git-am.sh                                  |  22 +--
 git-pull.sh                                |   2 +-
 git-rebase--interactive.sh                 |   6 +-
 git-rebase--merge.sh                       |   6 +-
 git-rebase.sh                              |   4 +-
 git-sh-setup.sh                            |   2 +-
 git-stash.sh                               |   6 +-
 git.c                                      |   2 +-
 notes-merge.c                              |   6 +-
 path.c                                     | 234 +++++++++++++++++---------
 refs.c                                     |  86 ++++++----
 refs.h                                     |   2 +-
 run-command.c                              |   4 +-
 run-command.h                              |   2 +-
 setup.c                                    | 124 +++++++++++---
 sha1_file.c                                |   2 +-
 submodule.c                                |   9 +-
 t/t0060-path-utils.sh                      |  36 ++++
 t/t1501-worktree.sh                        |  76 +++++++++
 t/t1510-repo-setup.sh                      |   1 +
 t/t2025-checkout-to.sh (new +x)            | 117 +++++++++++++
 t/t2026-prune-linked-checkouts.sh (new +x) |  84 ++++++++++
 templates/hooks--applypatch-msg.sample     |   4 +-
 templates/hooks--pre-applypatch.sample     |   4 +-
 trace.c                                    |   1 +
 transport.c                                |   8 +-
 wrapper.c                                  |  31 ++++
 52 files changed, 1306 insertions(+), 270 deletions(-)
 create mode 100755 t/t2025-checkout-to.sh
 create mode 100755 t/t2026-prune-linked-checkouts.sh

-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply	[flat|nested] 134+ messages in thread

* [PATCH v3 01/32] path.c: make get_pathname() return strbuf instead of static buffer
  2014-09-28  1:22     ` [PATCH v3 " Nguyễn Thái Ngọc Duy
@ 2014-09-28  1:22       ` Nguyễn Thái Ngọc Duy
  2014-09-28  1:22       ` [PATCH v3 02/32] path.c: make get_pathname() call sites return const char * Nguyễn Thái Ngọc Duy
                         ` (31 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-28  1:22 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

We've been avoiding PATH_MAX whenever possible. This patch makes
get_pathname() return a strbuf and updates the callers to take
advantage of this. The code is simplified as we no longer need to
worry about buffer overflow.

vsnpath() behavior is changed slightly: previously it always clears
the buffer before writing, now it just appends. Fortunately this is a
static function and all of its callers prepare the buffer properly:
git_path() gets the buffer from get_pathname() which resets the
buffer, the remaining call sites start with STRBUF_INIT'd buffer.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 path.c | 120 ++++++++++++++++++++++++++++-------------------------------------
 1 file changed, 51 insertions(+), 69 deletions(-)

diff --git a/path.c b/path.c
index f68df0c..015c0e4 100644
--- a/path.c
+++ b/path.c
@@ -16,11 +16,15 @@ static int get_st_mode_bits(const char *path, int *mode)
 
 static char bad_path[] = "/bad-path/";
 
-static char *get_pathname(void)
+static struct strbuf *get_pathname(void)
 {
-	static char pathname_array[4][PATH_MAX];
+	static struct strbuf pathname_array[4] = {
+		STRBUF_INIT, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT
+	};
 	static int index;
-	return pathname_array[3 & ++index];
+	struct strbuf *sb = &pathname_array[3 & ++index];
+	strbuf_reset(sb);
+	return sb;
 }
 
 static char *cleanup_path(char *path)
@@ -34,6 +38,13 @@ static char *cleanup_path(char *path)
 	return path;
 }
 
+static void strbuf_cleanup_path(struct strbuf *sb)
+{
+	char *path = cleanup_path(sb->buf);
+	if (path > sb->buf)
+		strbuf_remove(sb, 0, path - sb->buf);
+}
+
 char *mksnpath(char *buf, size_t n, const char *fmt, ...)
 {
 	va_list args;
@@ -49,85 +60,70 @@ char *mksnpath(char *buf, size_t n, const char *fmt, ...)
 	return cleanup_path(buf);
 }
 
-static char *vsnpath(char *buf, size_t n, const char *fmt, va_list args)
+static void vsnpath(struct strbuf *buf, const char *fmt, va_list args)
 {
 	const char *git_dir = get_git_dir();
-	size_t len;
-
-	len = strlen(git_dir);
-	if (n < len + 1)
-		goto bad;
-	memcpy(buf, git_dir, len);
-	if (len && !is_dir_sep(git_dir[len-1]))
-		buf[len++] = '/';
-	len += vsnprintf(buf + len, n - len, fmt, args);
-	if (len >= n)
-		goto bad;
-	return cleanup_path(buf);
-bad:
-	strlcpy(buf, bad_path, n);
-	return buf;
+	strbuf_addstr(buf, git_dir);
+	if (buf->len && !is_dir_sep(buf->buf[buf->len - 1]))
+		strbuf_addch(buf, '/');
+	strbuf_vaddf(buf, fmt, args);
+	strbuf_cleanup_path(buf);
 }
 
 char *git_snpath(char *buf, size_t n, const char *fmt, ...)
 {
-	char *ret;
+	struct strbuf sb = STRBUF_INIT;
 	va_list args;
 	va_start(args, fmt);
-	ret = vsnpath(buf, n, fmt, args);
+	vsnpath(&sb, fmt, args);
 	va_end(args);
-	return ret;
+	if (sb.len >= n)
+		strlcpy(buf, bad_path, n);
+	else
+		memcpy(buf, sb.buf, sb.len + 1);
+	strbuf_release(&sb);
+	return buf;
 }
 
 char *git_pathdup(const char *fmt, ...)
 {
-	char path[PATH_MAX], *ret;
+	struct strbuf path = STRBUF_INIT;
 	va_list args;
 	va_start(args, fmt);
-	ret = vsnpath(path, sizeof(path), fmt, args);
+	vsnpath(&path, fmt, args);
 	va_end(args);
-	return xstrdup(ret);
+	return strbuf_detach(&path, NULL);
 }
 
 char *mkpathdup(const char *fmt, ...)
 {
-	char *path;
 	struct strbuf sb = STRBUF_INIT;
 	va_list args;
-
 	va_start(args, fmt);
 	strbuf_vaddf(&sb, fmt, args);
 	va_end(args);
-	path = xstrdup(cleanup_path(sb.buf));
-
-	strbuf_release(&sb);
-	return path;
+	strbuf_cleanup_path(&sb);
+	return strbuf_detach(&sb, NULL);
 }
 
 char *mkpath(const char *fmt, ...)
 {
 	va_list args;
-	unsigned len;
-	char *pathname = get_pathname();
-
+	struct strbuf *pathname = get_pathname();
 	va_start(args, fmt);
-	len = vsnprintf(pathname, PATH_MAX, fmt, args);
+	strbuf_vaddf(pathname, fmt, args);
 	va_end(args);
-	if (len >= PATH_MAX)
-		return bad_path;
-	return cleanup_path(pathname);
+	return cleanup_path(pathname->buf);
 }
 
 char *git_path(const char *fmt, ...)
 {
-	char *pathname = get_pathname();
+	struct strbuf *pathname = get_pathname();
 	va_list args;
-	char *ret;
-
 	va_start(args, fmt);
-	ret = vsnpath(pathname, PATH_MAX, fmt, args);
+	vsnpath(pathname, fmt, args);
 	va_end(args);
-	return ret;
+	return pathname->buf;
 }
 
 void home_config_paths(char **global, char **xdg, char *file)
@@ -160,41 +156,27 @@ void home_config_paths(char **global, char **xdg, char *file)
 
 char *git_path_submodule(const char *path, const char *fmt, ...)
 {
-	char *pathname = get_pathname();
-	struct strbuf buf = STRBUF_INIT;
+	struct strbuf *buf = get_pathname();
 	const char *git_dir;
 	va_list args;
-	unsigned len;
-
-	len = strlen(path);
-	if (len > PATH_MAX-100)
-		return bad_path;
 
-	strbuf_addstr(&buf, path);
-	if (len && path[len-1] != '/')
-		strbuf_addch(&buf, '/');
-	strbuf_addstr(&buf, ".git");
+	strbuf_addstr(buf, path);
+	if (buf->len && buf->buf[buf->len - 1] != '/')
+		strbuf_addch(buf, '/');
+	strbuf_addstr(buf, ".git");
 
-	git_dir = read_gitfile(buf.buf);
+	git_dir = read_gitfile(buf->buf);
 	if (git_dir) {
-		strbuf_reset(&buf);
-		strbuf_addstr(&buf, git_dir);
+		strbuf_reset(buf);
+		strbuf_addstr(buf, git_dir);
 	}
-	strbuf_addch(&buf, '/');
-
-	if (buf.len >= PATH_MAX)
-		return bad_path;
-	memcpy(pathname, buf.buf, buf.len + 1);
-
-	strbuf_release(&buf);
-	len = strlen(pathname);
+	strbuf_addch(buf, '/');
 
 	va_start(args, fmt);
-	len += vsnprintf(pathname + len, PATH_MAX - len, fmt, args);
+	strbuf_vaddf(buf, fmt, args);
 	va_end(args);
-	if (len >= PATH_MAX)
-		return bad_path;
-	return cleanup_path(pathname);
+	strbuf_cleanup_path(buf);
+	return buf->buf;
 }
 
 int validate_headref(const char *path)
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 02/32] path.c: make get_pathname() call sites return const char *
  2014-09-28  1:22     ` [PATCH v3 " Nguyễn Thái Ngọc Duy
  2014-09-28  1:22       ` [PATCH v3 01/32] path.c: make get_pathname() return strbuf instead of static buffer Nguyễn Thái Ngọc Duy
@ 2014-09-28  1:22       ` Nguyễn Thái Ngọc Duy
  2014-09-28  1:22       ` [PATCH v3 03/32] git_snpath(): retire and replace with strbuf_git_path() Nguyễn Thái Ngọc Duy
                         ` (30 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-28  1:22 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Before the previous commit, get_pathname returns an array of PATH_MAX
length. Even if git_path() and similar functions does not use the
whole array, git_path() caller can, in theory.

After the commit, get_pathname() may return a buffer that has just
enough room for the returned string and git_path() caller should never
write beyond that.

Make git_path(), mkpath() and git_path_submodule() return a const
buffer to make sure callers do not write in it at all.

This could have been part of the previous commit, but the "const"
conversion is too much distraction from the core changes in path.c.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 builtin/checkout.c     | 2 +-
 builtin/clone.c        | 9 +++++----
 builtin/fetch.c        | 5 +++--
 builtin/fsck.c         | 4 ++--
 builtin/receive-pack.c | 2 +-
 builtin/remote.c       | 2 +-
 builtin/repack.c       | 8 +++++---
 cache.h                | 6 +++---
 fast-import.c          | 2 +-
 notes-merge.c          | 6 +++---
 path.c                 | 6 +++---
 refs.c                 | 8 ++++----
 run-command.c          | 4 ++--
 run-command.h          | 2 +-
 sha1_file.c            | 2 +-
 15 files changed, 36 insertions(+), 32 deletions(-)

diff --git a/builtin/checkout.c b/builtin/checkout.c
index 8afdf2b..d402d7a 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -589,7 +589,7 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
 			if (opts->new_branch_log && !log_all_ref_updates) {
 				int temp;
 				char log_file[PATH_MAX];
-				char *ref_name = mkpath("refs/heads/%s", opts->new_orphan_branch);
+				const char *ref_name = mkpath("refs/heads/%s", opts->new_orphan_branch);
 
 				temp = log_all_ref_updates;
 				log_all_ref_updates = 1;
diff --git a/builtin/clone.c b/builtin/clone.c
index 3927edf..180bc89 100644
--- a/builtin/clone.c
+++ b/builtin/clone.c
@@ -289,16 +289,17 @@ static void copy_alternates(struct strbuf *src, struct strbuf *dst,
 	struct strbuf line = STRBUF_INIT;
 
 	while (strbuf_getline(&line, in, '\n') != EOF) {
-		char *abs_path, abs_buf[PATH_MAX];
+		char *abs_path;
 		if (!line.len || line.buf[0] == '#')
 			continue;
 		if (is_absolute_path(line.buf)) {
 			add_to_alternates_file(line.buf);
 			continue;
 		}
-		abs_path = mkpath("%s/objects/%s", src_repo, line.buf);
-		normalize_path_copy(abs_buf, abs_path);
-		add_to_alternates_file(abs_buf);
+		abs_path = mkpathdup("%s/objects/%s", src_repo, line.buf);
+		normalize_path_copy(abs_path, abs_path);
+		add_to_alternates_file(abs_path);
+		free(abs_path);
 	}
 	strbuf_release(&line);
 	fclose(in);
diff --git a/builtin/fetch.c b/builtin/fetch.c
index 159fb7e..3f8c8f5 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -573,7 +573,8 @@ static int store_updated_refs(const char *raw_url, const char *remote_name,
 	struct strbuf note = STRBUF_INIT;
 	const char *what, *kind;
 	struct ref *rm;
-	char *url, *filename = dry_run ? "/dev/null" : git_path("FETCH_HEAD");
+	char *url;
+	const char *filename = dry_run ? "/dev/null" : git_path("FETCH_HEAD");
 	int want_status;
 
 	fp = fopen(filename, "a");
@@ -807,7 +808,7 @@ static void check_not_current_branch(struct ref *ref_map)
 
 static int truncate_fetch_head(void)
 {
-	char *filename = git_path("FETCH_HEAD");
+	const char *filename = git_path("FETCH_HEAD");
 	FILE *fp = fopen(filename, "w");
 
 	if (!fp)
diff --git a/builtin/fsck.c b/builtin/fsck.c
index e9ba576..0082a0e 100644
--- a/builtin/fsck.c
+++ b/builtin/fsck.c
@@ -225,12 +225,12 @@ static void check_unreachable_object(struct object *obj)
 			printf("dangling %s %s\n", typename(obj->type),
 			       sha1_to_hex(obj->sha1));
 		if (write_lost_and_found) {
-			char *filename = git_path("lost-found/%s/%s",
+			const char *filename = git_path("lost-found/%s/%s",
 				obj->type == OBJ_COMMIT ? "commit" : "other",
 				sha1_to_hex(obj->sha1));
 			FILE *f;
 
-			if (safe_create_leading_directories(filename)) {
+			if (safe_create_leading_directories_const(filename)) {
 				error("Could not create lost-found");
 				return;
 			}
diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index daf0600..3a49d1a 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -610,7 +610,7 @@ static void run_update_post_hook(struct command *commands)
 	int argc;
 	const char **argv;
 	struct child_process proc = CHILD_PROCESS_INIT;
-	char *hook;
+	const char *hook;
 
 	hook = find_hook("post-update");
 	for (argc = 0, cmd = commands; cmd; cmd = cmd->next) {
diff --git a/builtin/remote.c b/builtin/remote.c
index 9a4640d..d5324ed 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -581,7 +581,7 @@ static int migrate_file(struct remote *remote)
 {
 	struct strbuf buf = STRBUF_INIT;
 	int i;
-	char *path = NULL;
+	const char *path = NULL;
 
 	strbuf_addf(&buf, "remote.%s.url", remote->name);
 	for (i = 0; i < remote->url_nr; i++)
diff --git a/builtin/repack.c b/builtin/repack.c
index 2aae05d..614851b 100644
--- a/builtin/repack.c
+++ b/builtin/repack.c
@@ -283,7 +283,8 @@ int cmd_repack(int argc, const char **argv, const char *prefix)
 	failed = 0;
 	for_each_string_list_item(item, &names) {
 		for (ext = 0; ext < ARRAY_SIZE(exts); ext++) {
-			char *fname, *fname_old;
+			const char *fname_old;
+			char *fname;
 			fname = mkpathdup("%s/pack-%s%s", packdir,
 						item->string, exts[ext].name);
 			if (!file_exists(fname)) {
@@ -311,7 +312,8 @@ int cmd_repack(int argc, const char **argv, const char *prefix)
 	if (failed) {
 		struct string_list rollback_failure = STRING_LIST_INIT_DUP;
 		for_each_string_list_item(item, &rollback) {
-			char *fname, *fname_old;
+			const char *fname_old;
+			char *fname;
 			fname = mkpathdup("%s/%s", packdir, item->string);
 			fname_old = mkpath("%s/old-%s", packdir, item->string);
 			if (rename(fname_old, fname))
@@ -364,7 +366,7 @@ int cmd_repack(int argc, const char **argv, const char *prefix)
 	/* Remove the "old-" files */
 	for_each_string_list_item(item, &names) {
 		for (ext = 0; ext < ARRAY_SIZE(exts); ext++) {
-			char *fname;
+			const char *fname;
 			fname = mkpath("%s/old-%s%s",
 					packdir,
 					item->string,
diff --git a/cache.h b/cache.h
index 8206039..2e4d88f 100644
--- a/cache.h
+++ b/cache.h
@@ -705,9 +705,9 @@ extern char *mkpathdup(const char *fmt, ...)
 	__attribute__((format (printf, 1, 2)));
 
 /* Return a statically allocated filename matching the sha1 signature */
-extern char *mkpath(const char *fmt, ...) __attribute__((format (printf, 1, 2)));
-extern char *git_path(const char *fmt, ...) __attribute__((format (printf, 1, 2)));
-extern char *git_path_submodule(const char *path, const char *fmt, ...)
+extern const char *mkpath(const char *fmt, ...) __attribute__((format (printf, 1, 2)));
+extern const char *git_path(const char *fmt, ...) __attribute__((format (printf, 1, 2)));
+extern const char *git_path_submodule(const char *path, const char *fmt, ...)
 	__attribute__((format (printf, 2, 3)));
 
 /*
diff --git a/fast-import.c b/fast-import.c
index 96b0f42..9ddcd3b 100644
--- a/fast-import.c
+++ b/fast-import.c
@@ -404,7 +404,7 @@ static void dump_marks_helper(FILE *, uintmax_t, struct mark_set *);
 
 static void write_crash_report(const char *err)
 {
-	char *loc = git_path("fast_import_crash_%"PRIuMAX, (uintmax_t) getpid());
+	const char *loc = git_path("fast_import_crash_%"PRIuMAX, (uintmax_t) getpid());
 	FILE *rpt = fopen(loc, "w");
 	struct branch *b;
 	unsigned long lu;
diff --git a/notes-merge.c b/notes-merge.c
index fd5fae2..a9e6b15 100644
--- a/notes-merge.c
+++ b/notes-merge.c
@@ -280,7 +280,7 @@ static void check_notes_merge_worktree(struct notes_merge_options *o)
 				    "(%s exists).", git_path("NOTES_MERGE_*"));
 		}
 
-		if (safe_create_leading_directories(git_path(
+		if (safe_create_leading_directories_const(git_path(
 				NOTES_MERGE_WORKTREE "/.test")))
 			die_errno("unable to create directory %s",
 				  git_path(NOTES_MERGE_WORKTREE));
@@ -295,8 +295,8 @@ static void write_buf_to_worktree(const unsigned char *obj,
 				  const char *buf, unsigned long size)
 {
 	int fd;
-	char *path = git_path(NOTES_MERGE_WORKTREE "/%s", sha1_to_hex(obj));
-	if (safe_create_leading_directories(path))
+	const char *path = git_path(NOTES_MERGE_WORKTREE "/%s", sha1_to_hex(obj));
+	if (safe_create_leading_directories_const(path))
 		die_errno("unable to create directory for '%s'", path);
 	if (file_exists(path))
 		die("found existing file at '%s'", path);
diff --git a/path.c b/path.c
index 015c0e4..a7ceea2 100644
--- a/path.c
+++ b/path.c
@@ -106,7 +106,7 @@ char *mkpathdup(const char *fmt, ...)
 	return strbuf_detach(&sb, NULL);
 }
 
-char *mkpath(const char *fmt, ...)
+const char *mkpath(const char *fmt, ...)
 {
 	va_list args;
 	struct strbuf *pathname = get_pathname();
@@ -116,7 +116,7 @@ char *mkpath(const char *fmt, ...)
 	return cleanup_path(pathname->buf);
 }
 
-char *git_path(const char *fmt, ...)
+const char *git_path(const char *fmt, ...)
 {
 	struct strbuf *pathname = get_pathname();
 	va_list args;
@@ -154,7 +154,7 @@ void home_config_paths(char **global, char **xdg, char *file)
 	free(to_free);
 }
 
-char *git_path_submodule(const char *path, const char *fmt, ...)
+const char *git_path_submodule(const char *path, const char *fmt, ...)
 {
 	struct strbuf *buf = get_pathname();
 	const char *git_dir;
diff --git a/refs.c b/refs.c
index 311a6b5..f616adc 100644
--- a/refs.c
+++ b/refs.c
@@ -1306,7 +1306,7 @@ static int resolve_gitlink_ref_recursive(struct ref_cache *refs,
 {
 	int fd, len;
 	char buffer[128], *p;
-	char *path;
+	const char *path;
 
 	if (recursion > MAXDEPTH || strlen(refname) > MAXREFLEN)
 		return -1;
@@ -2139,7 +2139,7 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname,
 					    const unsigned char *old_sha1,
 					    int flags, int *type_p)
 {
-	char *ref_file;
+	const char *ref_file;
 	const char *orig_refname = refname;
 	struct ref_lock *lock;
 	int last_errno = 0;
@@ -2202,7 +2202,7 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname,
 		lock->force_write = 1;
 
  retry:
-	switch (safe_create_leading_directories(ref_file)) {
+	switch (safe_create_leading_directories_const(ref_file)) {
 	case SCLD_OK:
 		break; /* success */
 	case SCLD_VANISHED:
@@ -2647,7 +2647,7 @@ static int rename_tmp_log(const char *newrefname)
 	int attempts_remaining = 4;
 
  retry:
-	switch (safe_create_leading_directories(git_path("logs/%s", newrefname))) {
+	switch (safe_create_leading_directories_const(git_path("logs/%s", newrefname))) {
 	case SCLD_OK:
 		break; /* success */
 	case SCLD_VANISHED:
diff --git a/run-command.c b/run-command.c
index 761f0fd..1591534 100644
--- a/run-command.c
+++ b/run-command.c
@@ -748,9 +748,9 @@ int finish_async(struct async *async)
 #endif
 }
 
-char *find_hook(const char *name)
+const char *find_hook(const char *name)
 {
-	char *path = git_path("hooks/%s", name);
+	const char *path = git_path("hooks/%s", name);
 	if (access(path, X_OK) < 0)
 		path = NULL;
 
diff --git a/run-command.h b/run-command.h
index 1b135d1..ad938ec 100644
--- a/run-command.h
+++ b/run-command.h
@@ -51,7 +51,7 @@ int start_command(struct child_process *);
 int finish_command(struct child_process *);
 int run_command(struct child_process *);
 
-extern char *find_hook(const char *name);
+extern const char *find_hook(const char *name);
 LAST_ARG_MUST_BE_NULL
 extern int run_hook_le(const char *const *env, const char *name, ...);
 extern int run_hook_ve(const char *const *env, const char *name, va_list args);
diff --git a/sha1_file.c b/sha1_file.c
index c08c0cb..50c9600 100644
--- a/sha1_file.c
+++ b/sha1_file.c
@@ -404,7 +404,7 @@ void add_to_alternates_file(const char *reference)
 {
 	struct lock_file *lock = xcalloc(1, sizeof(struct lock_file));
 	int fd = hold_lock_file_for_append(lock, git_path("objects/info/alternates"), LOCK_DIE_ON_ERROR);
-	char *alt = mkpath("%s\n", reference);
+	const char *alt = mkpath("%s\n", reference);
 	write_or_die(fd, alt, strlen(alt));
 	if (commit_lock_file(lock))
 		die("could not close alternates file");
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 03/32] git_snpath(): retire and replace with strbuf_git_path()
  2014-09-28  1:22     ` [PATCH v3 " Nguyễn Thái Ngọc Duy
  2014-09-28  1:22       ` [PATCH v3 01/32] path.c: make get_pathname() return strbuf instead of static buffer Nguyễn Thái Ngọc Duy
  2014-09-28  1:22       ` [PATCH v3 02/32] path.c: make get_pathname() call sites return const char * Nguyễn Thái Ngọc Duy
@ 2014-09-28  1:22       ` Nguyễn Thái Ngọc Duy
  2014-09-28  1:22       ` [PATCH v3 04/32] path.c: rename vsnpath() to do_git_path() Nguyễn Thái Ngọc Duy
                         ` (29 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-28  1:22 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

In the previous patch, git_snpath() is modified to allocate a new
strbuf buffer because vsnpath() needs that. But that makes it
awkward because git_snpath() receives a pre-allocated buffer from
outside and has to copy data back. Rename it to strbuf_git_path()
and make it receive strbuf directly.

Using git_path() in update_refs_for_switch() which used to call
git_snpath() is safe because that function and all of its callers do
not keep any pointer to the round-robin buffer pool allocated by
get_pathname().

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 builtin/checkout.c | 13 +++++----
 cache.h            |  4 +--
 path.c             | 11 ++------
 refs.c             | 78 +++++++++++++++++++++++++++++++++---------------------
 refs.h             |  2 +-
 5 files changed, 61 insertions(+), 47 deletions(-)

diff --git a/builtin/checkout.c b/builtin/checkout.c
index d402d7a..220f80e 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -588,18 +588,21 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
 		if (opts->new_orphan_branch) {
 			if (opts->new_branch_log && !log_all_ref_updates) {
 				int temp;
-				char log_file[PATH_MAX];
-				const char *ref_name = mkpath("refs/heads/%s", opts->new_orphan_branch);
+				struct strbuf log_file = STRBUF_INIT;
+				int ret;
+				const char *ref_name;
 
+				ref_name = mkpath("refs/heads/%s", opts->new_orphan_branch);
 				temp = log_all_ref_updates;
 				log_all_ref_updates = 1;
-				if (log_ref_setup(ref_name, log_file, sizeof(log_file))) {
+				ret = log_ref_setup(ref_name, &log_file);
+				log_all_ref_updates = temp;
+				strbuf_release(&log_file);
+				if (ret) {
 					fprintf(stderr, _("Can not do reflog for '%s'\n"),
 					    opts->new_orphan_branch);
-					log_all_ref_updates = temp;
 					return;
 				}
-				log_all_ref_updates = temp;
 			}
 		}
 		else
diff --git a/cache.h b/cache.h
index 2e4d88f..f487c0e 100644
--- a/cache.h
+++ b/cache.h
@@ -697,8 +697,8 @@ extern int check_repository_format(void);
 
 extern char *mksnpath(char *buf, size_t n, const char *fmt, ...)
 	__attribute__((format (printf, 3, 4)));
-extern char *git_snpath(char *buf, size_t n, const char *fmt, ...)
-	__attribute__((format (printf, 3, 4)));
+extern void strbuf_git_path(struct strbuf *sb, const char *fmt, ...)
+	__attribute__((format (printf, 2, 3)));
 extern char *git_pathdup(const char *fmt, ...)
 	__attribute__((format (printf, 1, 2)));
 extern char *mkpathdup(const char *fmt, ...)
diff --git a/path.c b/path.c
index a7ceea2..47753aa 100644
--- a/path.c
+++ b/path.c
@@ -70,19 +70,12 @@ static void vsnpath(struct strbuf *buf, const char *fmt, va_list args)
 	strbuf_cleanup_path(buf);
 }
 
-char *git_snpath(char *buf, size_t n, const char *fmt, ...)
+void strbuf_git_path(struct strbuf *sb, const char *fmt, ...)
 {
-	struct strbuf sb = STRBUF_INIT;
 	va_list args;
 	va_start(args, fmt);
-	vsnpath(&sb, fmt, args);
+	vsnpath(sb, fmt, args);
 	va_end(args);
-	if (sb.len >= n)
-		strlcpy(buf, bad_path, n);
-	else
-		memcpy(buf, sb.buf, sb.len + 1);
-	strbuf_release(&sb);
-	return buf;
 }
 
 char *git_pathdup(const char *fmt, ...)
diff --git a/refs.c b/refs.c
index f616adc..3cefbd3 100644
--- a/refs.c
+++ b/refs.c
@@ -1400,10 +1400,12 @@ static const char *handle_missing_loose_ref(const char *refname,
 /* This function needs to return a meaningful errno on failure */
 const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int reading, int *flag)
 {
+	struct strbuf sb_path = STRBUF_INIT;
 	int depth = MAXDEPTH;
 	ssize_t len;
 	char buffer[256];
 	static char refname_buffer[256];
+	const char *ret;
 
 	if (flag)
 		*flag = 0;
@@ -1414,17 +1416,19 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea
 	}
 
 	for (;;) {
-		char path[PATH_MAX];
+		const char *path;
 		struct stat st;
 		char *buf;
 		int fd;
 
 		if (--depth < 0) {
 			errno = ELOOP;
-			return NULL;
+			goto fail;
 		}
 
-		git_snpath(path, sizeof(path), "%s", refname);
+		strbuf_reset(&sb_path);
+		strbuf_git_path(&sb_path, "%s", refname);
+		path = sb_path.buf;
 
 		/*
 		 * We might have to loop back here to avoid a race
@@ -1438,10 +1442,11 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea
 	stat_ref:
 		if (lstat(path, &st) < 0) {
 			if (errno == ENOENT)
-				return handle_missing_loose_ref(refname, sha1,
-								reading, flag);
+				ret = handle_missing_loose_ref(refname, sha1,
+							       reading, flag);
 			else
-				return NULL;
+				ret = NULL;
+			goto done;
 		}
 
 		/* Follow "normalized" - ie "refs/.." symlinks by hand */
@@ -1452,7 +1457,7 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea
 					/* inconsistent with lstat; retry */
 					goto stat_ref;
 				else
-					return NULL;
+					goto fail;
 			}
 			buffer[len] = 0;
 			if (starts_with(buffer, "refs/") &&
@@ -1468,7 +1473,7 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea
 		/* Is it a directory? */
 		if (S_ISDIR(st.st_mode)) {
 			errno = EISDIR;
-			return NULL;
+			goto fail;
 		}
 
 		/*
@@ -1481,14 +1486,15 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea
 				/* inconsistent with lstat; retry */
 				goto stat_ref;
 			else
-				return NULL;
+				goto fail;
 		}
+
 		len = read_in_full(fd, buffer, sizeof(buffer)-1);
 		if (len < 0) {
 			int save_errno = errno;
 			close(fd);
 			errno = save_errno;
-			return NULL;
+			goto fail;
 		}
 		close(fd);
 		while (len && isspace(buffer[len-1]))
@@ -1508,9 +1514,10 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea
 				if (flag)
 					*flag |= REF_ISBROKEN;
 				errno = EINVAL;
-				return NULL;
+				goto fail;
 			}
-			return refname;
+			ret = refname;
+			goto done;
 		}
 		if (flag)
 			*flag |= REF_ISSYMREF;
@@ -1521,10 +1528,15 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea
 			if (flag)
 				*flag |= REF_ISBROKEN;
 			errno = EINVAL;
-			return NULL;
+			goto fail;
 		}
 		refname = strcpy(refname_buffer, buf);
 	}
+fail:
+	ret = NULL;
+done:
+	strbuf_release(&sb_path);
+	return ret;
 }
 
 char *resolve_refdup(const char *ref, unsigned char *sha1, int reading, int *flag)
@@ -2832,51 +2844,51 @@ static int copy_msg(char *buf, const char *msg)
 }
 
 /* This function must set a meaningful errno on failure */
-int log_ref_setup(const char *refname, char *logfile, int bufsize)
+int log_ref_setup(const char *refname, struct strbuf *logfile)
 {
 	int logfd, oflags = O_APPEND | O_WRONLY;
 
-	git_snpath(logfile, bufsize, "logs/%s", refname);
+	strbuf_git_path(logfile, "logs/%s", refname);
 	if (log_all_ref_updates &&
 	    (starts_with(refname, "refs/heads/") ||
 	     starts_with(refname, "refs/remotes/") ||
 	     starts_with(refname, "refs/notes/") ||
 	     !strcmp(refname, "HEAD"))) {
-		if (safe_create_leading_directories(logfile) < 0) {
+		if (safe_create_leading_directories(logfile->buf) < 0) {
 			int save_errno = errno;
-			error("unable to create directory for %s", logfile);
+			error("unable to create directory for %s", logfile->buf);
 			errno = save_errno;
 			return -1;
 		}
 		oflags |= O_CREAT;
 	}
 
-	logfd = open(logfile, oflags, 0666);
+	logfd = open(logfile->buf, oflags, 0666);
 	if (logfd < 0) {
 		if (!(oflags & O_CREAT) && errno == ENOENT)
 			return 0;
 
 		if ((oflags & O_CREAT) && errno == EISDIR) {
-			if (remove_empty_directories(logfile)) {
+			if (remove_empty_directories(logfile->buf)) {
 				int save_errno = errno;
 				error("There are still logs under '%s'",
-				      logfile);
+				      logfile->buf);
 				errno = save_errno;
 				return -1;
 			}
-			logfd = open(logfile, oflags, 0666);
+			logfd = open(logfile->buf, oflags, 0666);
 		}
 
 		if (logfd < 0) {
 			int save_errno = errno;
-			error("Unable to append to %s: %s", logfile,
+			error("Unable to append to %s: %s", logfile->buf,
 			      strerror(errno));
 			errno = save_errno;
 			return -1;
 		}
 	}
 
-	adjust_shared_perm(logfile);
+	adjust_shared_perm(logfile->buf);
 	close(logfd);
 	return 0;
 }
@@ -2887,20 +2899,22 @@ static int log_ref_write(const char *refname, const unsigned char *old_sha1,
 	int logfd, result, written, oflags = O_APPEND | O_WRONLY;
 	unsigned maxlen, len;
 	int msglen;
-	char log_file[PATH_MAX];
+	struct strbuf sb_log_file = STRBUF_INIT;
+	const char *log_file;
 	char *logrec;
 	const char *committer;
 
 	if (log_all_ref_updates < 0)
 		log_all_ref_updates = !is_bare_repository();
 
-	result = log_ref_setup(refname, log_file, sizeof(log_file));
+	result = log_ref_setup(refname, &sb_log_file);
 	if (result)
-		return result;
+		goto done;
+	log_file = sb_log_file.buf;
 
 	logfd = open(log_file, oflags);
 	if (logfd < 0)
-		return 0;
+		goto done;
 	msglen = msg ? strlen(msg) : 0;
 	committer = git_committer_info(0);
 	maxlen = strlen(committer) + msglen + 100;
@@ -2918,15 +2932,19 @@ static int log_ref_write(const char *refname, const unsigned char *old_sha1,
 		close(logfd);
 		error("Unable to append to %s", log_file);
 		errno = save_errno;
-		return -1;
+		result = -1;
+		goto done;
 	}
 	if (close(logfd)) {
 		int save_errno = errno;
 		error("Unable to append to %s", log_file);
 		errno = save_errno;
-		return -1;
+		result = -1;
+		goto done;
 	}
-	return 0;
+done:
+	strbuf_release(&sb_log_file);
+	return result;
 }
 
 int is_branch(const char *refname)
diff --git a/refs.h b/refs.h
index 10fc3a2..c990a85 100644
--- a/refs.h
+++ b/refs.h
@@ -203,7 +203,7 @@ extern int write_ref_sha1(struct ref_lock *lock, const unsigned char *sha1, cons
 /*
  * Setup reflog before using. Set errno to something meaningful on failure.
  */
-int log_ref_setup(const char *refname, char *logfile, int bufsize);
+int log_ref_setup(const char *refname, struct strbuf *logfile);
 
 /** Reads log for the value of ref during at_time. **/
 extern int read_ref_at(const char *refname, unsigned long at_time, int cnt,
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 04/32] path.c: rename vsnpath() to do_git_path()
  2014-09-28  1:22     ` [PATCH v3 " Nguyễn Thái Ngọc Duy
                         ` (2 preceding siblings ...)
  2014-09-28  1:22       ` [PATCH v3 03/32] git_snpath(): retire and replace with strbuf_git_path() Nguyễn Thái Ngọc Duy
@ 2014-09-28  1:22       ` Nguyễn Thái Ngọc Duy
  2014-09-28  1:22       ` [PATCH v3 05/32] path.c: group git_path(), git_pathdup() and strbuf_git_path() together Nguyễn Thái Ngọc Duy
                         ` (28 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-28  1:22 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

The name vsnpath() gives an impression that this is general path
handling function. It's not. This is the underlying implementation of
git_path(), git_pathdup() and strbuf_git_path() which will prefix
$GIT_DIR in the result string.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 path.c | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/path.c b/path.c
index 47753aa..6991103 100644
--- a/path.c
+++ b/path.c
@@ -60,7 +60,7 @@ char *mksnpath(char *buf, size_t n, const char *fmt, ...)
 	return cleanup_path(buf);
 }
 
-static void vsnpath(struct strbuf *buf, const char *fmt, va_list args)
+static void do_git_path(struct strbuf *buf, const char *fmt, va_list args)
 {
 	const char *git_dir = get_git_dir();
 	strbuf_addstr(buf, git_dir);
@@ -74,7 +74,7 @@ void strbuf_git_path(struct strbuf *sb, const char *fmt, ...)
 {
 	va_list args;
 	va_start(args, fmt);
-	vsnpath(sb, fmt, args);
+	do_git_path(sb, fmt, args);
 	va_end(args);
 }
 
@@ -83,7 +83,7 @@ char *git_pathdup(const char *fmt, ...)
 	struct strbuf path = STRBUF_INIT;
 	va_list args;
 	va_start(args, fmt);
-	vsnpath(&path, fmt, args);
+	do_git_path(&path, fmt, args);
 	va_end(args);
 	return strbuf_detach(&path, NULL);
 }
@@ -114,7 +114,7 @@ const char *git_path(const char *fmt, ...)
 	struct strbuf *pathname = get_pathname();
 	va_list args;
 	va_start(args, fmt);
-	vsnpath(pathname, fmt, args);
+	do_git_path(pathname, fmt, args);
 	va_end(args);
 	return pathname->buf;
 }
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 05/32] path.c: group git_path(), git_pathdup() and strbuf_git_path() together
  2014-09-28  1:22     ` [PATCH v3 " Nguyễn Thái Ngọc Duy
                         ` (3 preceding siblings ...)
  2014-09-28  1:22       ` [PATCH v3 04/32] path.c: rename vsnpath() to do_git_path() Nguyễn Thái Ngọc Duy
@ 2014-09-28  1:22       ` Nguyễn Thái Ngọc Duy
  2014-09-28  1:22       ` [PATCH v3 06/32] git_path(): be aware of file relocation in $GIT_DIR Nguyễn Thái Ngọc Duy
                         ` (27 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-28  1:22 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 path.c | 20 ++++++++++----------
 1 file changed, 10 insertions(+), 10 deletions(-)

diff --git a/path.c b/path.c
index 6991103..df0f75b 100644
--- a/path.c
+++ b/path.c
@@ -78,6 +78,16 @@ void strbuf_git_path(struct strbuf *sb, const char *fmt, ...)
 	va_end(args);
 }
 
+const char *git_path(const char *fmt, ...)
+{
+	struct strbuf *pathname = get_pathname();
+	va_list args;
+	va_start(args, fmt);
+	do_git_path(pathname, fmt, args);
+	va_end(args);
+	return pathname->buf;
+}
+
 char *git_pathdup(const char *fmt, ...)
 {
 	struct strbuf path = STRBUF_INIT;
@@ -109,16 +119,6 @@ const char *mkpath(const char *fmt, ...)
 	return cleanup_path(pathname->buf);
 }
 
-const char *git_path(const char *fmt, ...)
-{
-	struct strbuf *pathname = get_pathname();
-	va_list args;
-	va_start(args, fmt);
-	do_git_path(pathname, fmt, args);
-	va_end(args);
-	return pathname->buf;
-}
-
 void home_config_paths(char **global, char **xdg, char *file)
 {
 	char *xdg_home = getenv("XDG_CONFIG_HOME");
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 06/32] git_path(): be aware of file relocation in $GIT_DIR
  2014-09-28  1:22     ` [PATCH v3 " Nguyễn Thái Ngọc Duy
                         ` (4 preceding siblings ...)
  2014-09-28  1:22       ` [PATCH v3 05/32] path.c: group git_path(), git_pathdup() and strbuf_git_path() together Nguyễn Thái Ngọc Duy
@ 2014-09-28  1:22       ` Nguyễn Thái Ngọc Duy
  2014-09-28  1:22       ` [PATCH v3 07/32] *.sh: respect $GIT_INDEX_FILE Nguyễn Thái Ngọc Duy
                         ` (26 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-28  1:22 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

We allow the user to relocate certain paths out of $GIT_DIR via
environment variables, e.g. GIT_OBJECT_DIRECTORY, GIT_INDEX_FILE and
GIT_GRAFT_FILE. Callers are not supposed to use git_path() or
git_pathdup() to get those paths. Instead they must use
get_object_directory(), get_index_file() and get_graft_file()
respectively. This is inconvenient and could be missed in review (for
example, there's git_path("objects/info/alternates") somewhere in
sha1_file.c).

This patch makes git_path() and git_pathdup() understand those
environment variables. So if you set GIT_OBJECT_DIRECTORY to /foo/bar,
git_path("objects/abc") should return /foo/bar/abc. The same is done
for the two remaining env variables.

"git rev-parse --git-path" is the wrapper for script use.

This patch kinda reverts a0279e1 (setup_git_env: use git_pathdup
instead of xmalloc + sprintf - 2014-06-19) because using git_pathdup
here would result in infinite recursion:

  setup_git_env() -> git_pathdup("objects") -> .. -> adjust_git_path()
  -> get_object_directory() -> oops, git_object_directory is NOT set
  yet -> setup_git_env()

I wanted to make git_pathdup_literal() that skips adjust_git_path().
But that won't work because later on when $GIT_COMMON_DIR is
introduced, git_pathdup_literal("objects") needs adjust_git_path() to
replace $GIT_DIR with $GIT_COMMON_DIR.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 Documentation/git-rev-parse.txt |  7 ++++++
 builtin/rev-parse.c             |  7 ++++++
 cache.h                         |  1 +
 environment.c                   | 19 +++++++++++-----
 path.c                          | 49 +++++++++++++++++++++++++++++++++++++++--
 t/t0060-path-utils.sh           | 19 ++++++++++++++++
 6 files changed, 95 insertions(+), 7 deletions(-)

diff --git a/Documentation/git-rev-parse.txt b/Documentation/git-rev-parse.txt
index 0b84769..0fed5a4 100644
--- a/Documentation/git-rev-parse.txt
+++ b/Documentation/git-rev-parse.txt
@@ -232,6 +232,13 @@ print a message to stderr and exit with nonzero status.
 	repository.  If <path> is a gitfile then the resolved path
 	to the real repository is printed.
 
+--git-path <path>::
+	Resolve "$GIT_DIR/<path>" and takes other path relocation
+	variables such as $GIT_OBJECT_DIRECTORY,
+	$GIT_INDEX_FILE... into account. For example, if
+	$GIT_OBJECT_DIRECTORY is set to /foo/bar then "git rev-parse
+	--git-path objects/abc" returns /foo/bar/abc.
+
 --show-cdup::
 	When the command is invoked from a subdirectory, show the
 	path of the top-level directory relative to the current
diff --git a/builtin/rev-parse.c b/builtin/rev-parse.c
index c911b45..a6f4697 100644
--- a/builtin/rev-parse.c
+++ b/builtin/rev-parse.c
@@ -531,6 +531,13 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)
 	for (i = 1; i < argc; i++) {
 		const char *arg = argv[i];
 
+		if (!strcmp(arg, "--git-path")) {
+			if (!argv[i + 1])
+				die("--git-path requires an argument");
+			puts(git_path("%s", argv[i + 1]));
+			i++;
+			continue;
+		}
 		if (as_is) {
 			if (show_file(arg, output_prefix) && as_is < 2)
 				verify_filename(prefix, arg, 0);
diff --git a/cache.h b/cache.h
index f487c0e..161dbd5 100644
--- a/cache.h
+++ b/cache.h
@@ -635,6 +635,7 @@ extern int fsync_object_files;
 extern int core_preload_index;
 extern int core_apply_sparse_checkout;
 extern int precomposed_unicode;
+extern int git_db_env, git_index_env, git_graft_env;
 
 /*
  * The character that begins a commented line in user-editable file
diff --git a/environment.c b/environment.c
index 565f652..fee12a6 100644
--- a/environment.c
+++ b/environment.c
@@ -83,6 +83,7 @@ static size_t namespace_len;
 
 static const char *git_dir;
 static char *git_object_dir, *git_index_file, *git_graft_file;
+int git_db_env, git_index_env, git_graft_env;
 
 /*
  * Repository-local GIT_* environment variables; see cache.h for details.
@@ -124,10 +125,18 @@ static char *expand_namespace(const char *raw_namespace)
 	return strbuf_detach(&buf, NULL);
 }
 
-static char *git_path_from_env(const char *envvar, const char *path)
+static char *git_path_from_env(const char *envvar, const char *path,
+			       int *fromenv)
 {
 	const char *value = getenv(envvar);
-	return value ? xstrdup(value) : git_pathdup("%s", path);
+	if (!value) {
+		char *buf = xmalloc(strlen(git_dir) + strlen(path) + 2);
+		sprintf(buf, "%s/%s", git_dir, path);
+		return buf;
+	}
+	if (fromenv)
+		*fromenv = 1;
+	return xstrdup(value);
 }
 
 static void setup_git_env(void)
@@ -140,9 +149,9 @@ static void setup_git_env(void)
 		git_dir = DEFAULT_GIT_DIR_ENVIRONMENT;
 	gitfile = read_gitfile(git_dir);
 	git_dir = xstrdup(gitfile ? gitfile : git_dir);
-	git_object_dir = git_path_from_env(DB_ENVIRONMENT, "objects");
-	git_index_file = git_path_from_env(INDEX_ENVIRONMENT, "index");
-	git_graft_file = git_path_from_env(GRAFT_ENVIRONMENT, "info/grafts");
+	git_object_dir = git_path_from_env(DB_ENVIRONMENT, "objects", &git_db_env);
+	git_index_file = git_path_from_env(INDEX_ENVIRONMENT, "index", &git_index_env);
+	git_graft_file = git_path_from_env(GRAFT_ENVIRONMENT, "info/grafts", &git_graft_env);
 	if (getenv(NO_REPLACE_OBJECTS_ENVIRONMENT))
 		check_replace_refs = 0;
 	namespace = expand_namespace(getenv(GIT_NAMESPACE_ENVIRONMENT));
diff --git a/path.c b/path.c
index df0f75b..4910783 100644
--- a/path.c
+++ b/path.c
@@ -60,13 +60,58 @@ char *mksnpath(char *buf, size_t n, const char *fmt, ...)
 	return cleanup_path(buf);
 }
 
+static int dir_prefix(const char *buf, const char *dir)
+{
+	int len = strlen(dir);
+	return !strncmp(buf, dir, len) &&
+		(is_dir_sep(buf[len]) || buf[len] == '\0');
+}
+
+/* $buf =~ m|$dir/+$file| but without regex */
+static int is_dir_file(const char *buf, const char *dir, const char *file)
+{
+	int len = strlen(dir);
+	if (strncmp(buf, dir, len) || !is_dir_sep(buf[len]))
+		return 0;
+	while (is_dir_sep(buf[len]))
+		len++;
+	return !strcmp(buf + len, file);
+}
+
+static void replace_dir(struct strbuf *buf, int len, const char *newdir)
+{
+	int newlen = strlen(newdir);
+	int need_sep = (buf->buf[len] && !is_dir_sep(buf->buf[len])) &&
+		!is_dir_sep(newdir[newlen - 1]);
+	if (need_sep)
+		len--;	 /* keep one char, to be replaced with '/'  */
+	strbuf_splice(buf, 0, len, newdir, newlen);
+	if (need_sep)
+		buf->buf[newlen] = '/';
+}
+
+static void adjust_git_path(struct strbuf *buf, int git_dir_len)
+{
+	const char *base = buf->buf + git_dir_len;
+	if (git_graft_env && is_dir_file(base, "info", "grafts"))
+		strbuf_splice(buf, 0, buf->len,
+			      get_graft_file(), strlen(get_graft_file()));
+	else if (git_index_env && !strcmp(base, "index"))
+		strbuf_splice(buf, 0, buf->len,
+			      get_index_file(), strlen(get_index_file()));
+	else if (git_db_env && dir_prefix(base, "objects"))
+		replace_dir(buf, git_dir_len + 7, get_object_directory());
+}
+
 static void do_git_path(struct strbuf *buf, const char *fmt, va_list args)
 {
-	const char *git_dir = get_git_dir();
-	strbuf_addstr(buf, git_dir);
+	int gitdir_len;
+	strbuf_addstr(buf, get_git_dir());
 	if (buf->len && !is_dir_sep(buf->buf[buf->len - 1]))
 		strbuf_addch(buf, '/');
+	gitdir_len = buf->len;
 	strbuf_vaddf(buf, fmt, args);
+	adjust_git_path(buf, gitdir_len);
 	strbuf_cleanup_path(buf);
 }
 
diff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh
index c0143a0..33d2818 100755
--- a/t/t0060-path-utils.sh
+++ b/t/t0060-path-utils.sh
@@ -19,6 +19,14 @@ relative_path() {
 	"test \"\$(test-path-utils relative_path '$1' '$2')\" = '$expected'"
 }
 
+test_git_path() {
+	test_expect_success "git-path $1 $2 => $3" "
+		$1 git rev-parse --git-path $2 >actual &&
+		echo $3 >expect &&
+		test_cmp expect actual
+	"
+}
+
 # On Windows, we are using MSYS's bash, which mangles the paths.
 # Absolute paths are anchored at the MSYS installation directory,
 # which means that the path / accounts for this many characters:
@@ -244,4 +252,15 @@ relative_path "<null>"		"<empty>"	./
 relative_path "<null>"		"<null>"	./
 relative_path "<null>"		/foo/a/b	./
 
+test_git_path A=B                info/grafts .git/info/grafts
+test_git_path GIT_GRAFT_FILE=foo info/grafts foo
+test_git_path GIT_GRAFT_FILE=foo info/////grafts foo
+test_git_path GIT_INDEX_FILE=foo index foo
+test_git_path GIT_INDEX_FILE=foo index/foo .git/index/foo
+test_git_path GIT_INDEX_FILE=foo index2 .git/index2
+test_expect_success 'setup fake objects directory foo' 'mkdir foo'
+test_git_path GIT_OBJECT_DIRECTORY=foo objects foo
+test_git_path GIT_OBJECT_DIRECTORY=foo objects/foo foo/foo
+test_git_path GIT_OBJECT_DIRECTORY=foo objects2 .git/objects2
+
 test_done
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 07/32] *.sh: respect $GIT_INDEX_FILE
  2014-09-28  1:22     ` [PATCH v3 " Nguyễn Thái Ngọc Duy
                         ` (5 preceding siblings ...)
  2014-09-28  1:22       ` [PATCH v3 06/32] git_path(): be aware of file relocation in $GIT_DIR Nguyễn Thái Ngọc Duy
@ 2014-09-28  1:22       ` Nguyễn Thái Ngọc Duy
  2014-09-28  1:22       ` [PATCH v3 08/32] reflog: avoid constructing .lock path with git_path Nguyễn Thái Ngọc Duy
                         ` (25 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-28  1:22 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 git-pull.sh  | 2 +-
 git-stash.sh | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/git-pull.sh b/git-pull.sh
index 4d4fc77..ad44226 100755
--- a/git-pull.sh
+++ b/git-pull.sh
@@ -240,7 +240,7 @@ test true = "$rebase" && {
 	if ! git rev-parse -q --verify HEAD >/dev/null
 	then
 		# On an unborn branch
-		if test -f "$GIT_DIR/index"
+		if test -f "$(git rev-parse --git-path index)"
 		then
 			die "$(gettext "updating an unborn branch with changes added to the index")"
 		fi
diff --git a/git-stash.sh b/git-stash.sh
index 0158c73..4294aa0 100755
--- a/git-stash.sh
+++ b/git-stash.sh
@@ -20,7 +20,7 @@ require_work_tree
 cd_to_toplevel
 
 TMP="$GIT_DIR/.git-stash.$$"
-TMPindex=${GIT_INDEX_FILE-"$GIT_DIR/index"}.stash.$$
+TMPindex=${GIT_INDEX_FILE-"$(git rev-parse --git-path index)"}.stash.$$
 trap 'rm -f "$TMP-"* "$TMPindex"' 0
 
 ref_stash=refs/stash
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 08/32] reflog: avoid constructing .lock path with git_path
  2014-09-28  1:22     ` [PATCH v3 " Nguyễn Thái Ngọc Duy
                         ` (6 preceding siblings ...)
  2014-09-28  1:22       ` [PATCH v3 07/32] *.sh: respect $GIT_INDEX_FILE Nguyễn Thái Ngọc Duy
@ 2014-09-28  1:22       ` Nguyễn Thái Ngọc Duy
  2014-09-28  1:22       ` [PATCH v3 09/32] fast-import: use git_path() for accessing .git dir instead of get_git_dir() Nguyễn Thái Ngọc Duy
                         ` (24 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-28  1:22 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Among pathnames in $GIT_DIR, e.g. "index" or "packed-refs", we want to
automatically and silently map some of them to the $GIT_DIR of the
repository we are borrowing from via $GIT_COMMON_DIR mechanism.  When
we formulate the pathname for its lockfile, we want it to be in the
same location as its final destination.  "index" is not shared and
needs to remain in the borrowing repository, while "packed-refs" is
shared and needs to go to the borrowed repository.

git_path() could be taught about the ".lock" suffix and map
"index.lock" and "packed-refs.lock" the same way their basenames are
mapped, but instead the caller can help by asking where the basename
(e.g. "index") is mapped to git_path() and then appending ".lock"
after the mapping is done.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 builtin/reflog.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/builtin/reflog.c b/builtin/reflog.c
index e8a8fb1..9bd874d 100644
--- a/builtin/reflog.c
+++ b/builtin/reflog.c
@@ -372,7 +372,7 @@ static int expire_reflog(const char *ref, const unsigned char *sha1, int unused,
 	if (!reflog_exists(ref))
 		goto finish;
 	if (!cmd->dry_run) {
-		newlog_path = git_pathdup("logs/%s.lock", ref);
+		newlog_path = mkpathdup("%s.lock", log_file);
 		cb.newlog = fopen(newlog_path, "w");
 	}
 
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 09/32] fast-import: use git_path() for accessing .git dir instead of get_git_dir()
  2014-09-28  1:22     ` [PATCH v3 " Nguyễn Thái Ngọc Duy
                         ` (7 preceding siblings ...)
  2014-09-28  1:22       ` [PATCH v3 08/32] reflog: avoid constructing .lock path with git_path Nguyễn Thái Ngọc Duy
@ 2014-09-28  1:22       ` Nguyễn Thái Ngọc Duy
  2014-09-28  1:22       ` [PATCH v3 10/32] commit: use SEQ_DIR instead of hardcoding "sequencer" Nguyễn Thái Ngọc Duy
                         ` (23 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-28  1:22 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

This allows git_path() to redirect info/fast-import to another place
if needed

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 fast-import.c | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/fast-import.c b/fast-import.c
index 9ddcd3b..69e4c3f 100644
--- a/fast-import.c
+++ b/fast-import.c
@@ -3130,12 +3130,9 @@ static void parse_progress(void)
 
 static char* make_fast_import_path(const char *path)
 {
-	struct strbuf abs_path = STRBUF_INIT;
-
 	if (!relative_marks_paths || is_absolute_path(path))
 		return xstrdup(path);
-	strbuf_addf(&abs_path, "%s/info/fast-import/%s", get_git_dir(), path);
-	return strbuf_detach(&abs_path, NULL);
+	return xstrdup(git_path("info/fast-import/%s", path));
 }
 
 static void option_import_marks(const char *marks,
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 10/32] commit: use SEQ_DIR instead of hardcoding "sequencer"
  2014-09-28  1:22     ` [PATCH v3 " Nguyễn Thái Ngọc Duy
                         ` (8 preceding siblings ...)
  2014-09-28  1:22       ` [PATCH v3 09/32] fast-import: use git_path() for accessing .git dir instead of get_git_dir() Nguyễn Thái Ngọc Duy
@ 2014-09-28  1:22       ` Nguyễn Thái Ngọc Duy
  2014-09-28  1:22       ` [PATCH v3 11/32] $GIT_COMMON_DIR: a new environment variable Nguyễn Thái Ngọc Duy
                         ` (22 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-28  1:22 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 builtin/commit.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/builtin/commit.c b/builtin/commit.c
index b0fe784..08df727 100644
--- a/builtin/commit.c
+++ b/builtin/commit.c
@@ -169,7 +169,7 @@ static void determine_whence(struct wt_status *s)
 		whence = FROM_MERGE;
 	else if (file_exists(git_path("CHERRY_PICK_HEAD"))) {
 		whence = FROM_CHERRY_PICK;
-		if (file_exists(git_path("sequencer")))
+		if (file_exists(git_path(SEQ_DIR)))
 			sequencer_in_use = 1;
 	}
 	else
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 11/32] $GIT_COMMON_DIR: a new environment variable
  2014-09-28  1:22     ` [PATCH v3 " Nguyễn Thái Ngọc Duy
                         ` (9 preceding siblings ...)
  2014-09-28  1:22       ` [PATCH v3 10/32] commit: use SEQ_DIR instead of hardcoding "sequencer" Nguyễn Thái Ngọc Duy
@ 2014-09-28  1:22       ` Nguyễn Thái Ngọc Duy
  2014-09-28  1:22       ` [PATCH v3 12/32] git-sh-setup.sh: use rev-parse --git-path to get $GIT_DIR/objects Nguyễn Thái Ngọc Duy
                         ` (21 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-28  1:22 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

This variable is intended to support multiple working directories
attached to a repository. Such a repository may have a main working
directory, created by either "git init" or "git clone" and one or more
linked working directories. These working directories and the main
repository share the same repository directory.

In linked working directories, $GIT_COMMON_DIR must be defined to point
to the real repository directory and $GIT_DIR points to an unused
subdirectory inside $GIT_COMMON_DIR. File locations inside the
repository are reorganized from the linked worktree view point:

 - worktree-specific such as HEAD, logs/HEAD, index, other top-level
   refs and unrecognized files are from $GIT_DIR.

 - the rest like objects, refs, info, hooks, packed-refs, shallow...
   are from $GIT_COMMON_DIR (except info/sparse-checkout, but that's
   a separate patch)

Scripts are supposed to retrieve paths in $GIT_DIR with "git rev-parse
--git-path", which will take care of "$GIT_DIR vs $GIT_COMMON_DIR"
business.

The redirection is done by git_path(), git_pathdup() and
strbuf_git_path(). The selected list of paths goes to $GIT_COMMON_DIR,
not the other way around in case a developer adds a new
worktree-specific file and it's accidentally promoted to be shared
across repositories (this includes unknown files added by third party
commands)

The list of known files that belong to $GIT_DIR are:

ADD_EDIT.patch BISECT_ANCESTORS_OK BISECT_EXPECTED_REV BISECT_LOG
BISECT_NAMES CHERRY_PICK_HEAD COMMIT_MSG FETCH_HEAD HEAD MERGE_HEAD
MERGE_MODE MERGE_RR NOTES_EDITMSG NOTES_MERGE_WORKTREE ORIG_HEAD
REVERT_HEAD SQUASH_MSG TAG_EDITMSG fast_import_crash_* logs/HEAD
next-index-* rebase-apply rebase-merge rsync-refs-* sequencer/*
shallow_*

Path mapping is NOT done for git_path_submodule(). Multi-checkouts are
not supported as submodules.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
---
 Documentation/git.txt                  |  8 ++++++
 Documentation/gitrepository-layout.txt | 45 +++++++++++++++++++++++++---------
 cache.h                                |  4 ++-
 environment.c                          | 28 +++++++++++++++------
 path.c                                 | 34 +++++++++++++++++++++++++
 t/t0060-path-utils.sh                  | 16 ++++++++++++
 6 files changed, 116 insertions(+), 19 deletions(-)

diff --git a/Documentation/git.txt b/Documentation/git.txt
index 8b2c542..176d37c 100644
--- a/Documentation/git.txt
+++ b/Documentation/git.txt
@@ -802,6 +802,14 @@ Git so take care if using Cogito etc.
 	an explicit repository directory set via 'GIT_DIR' or on the
 	command line.
 
+'GIT_COMMON_DIR'::
+	If this variable is set to a path, non-worktree files that are
+	normally in $GIT_DIR will be taken from this path
+	instead. Worktree-specific files such as HEAD or index are
+	taken from $GIT_DIR. See linkgit:gitrepository-layout[5] for
+	details. This variable has lower precedence than other path
+	variables such as GIT_INDEX_FILE, GIT_OBJECT_DIRECTORY...
+
 Git Commits
 ~~~~~~~~~~~
 'GIT_AUTHOR_NAME'::
diff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt
index 79653f3..2b5966a 100644
--- a/Documentation/gitrepository-layout.txt
+++ b/Documentation/gitrepository-layout.txt
@@ -46,6 +46,9 @@ of incomplete object store is not suitable to be published for
 use with dumb transports but otherwise is OK as long as
 `objects/info/alternates` points at the object stores it
 borrows from.
++
+This directory is ignored if $GIT_COMMON_DIR is set and
+"$GIT_COMMON_DIR/objects" will be used instead.
 
 objects/[0-9a-f][0-9a-f]::
 	A newly created object is stored in its own file.
@@ -92,7 +95,8 @@ refs::
 	References are stored in subdirectories of this
 	directory.  The 'git prune' command knows to preserve
 	objects reachable from refs found in this directory and
-	its subdirectories.
+	its subdirectories. This directory is ignored if $GIT_COMMON_DIR
+	is set and "$GIT_COMMON_DIR/refs" will be used instead.
 
 refs/heads/`name`::
 	records tip-of-the-tree commit objects of branch `name`
@@ -114,7 +118,8 @@ refs/replace/`<obj-sha1>`::
 packed-refs::
 	records the same information as refs/heads/, refs/tags/,
 	and friends record in a more efficient way.  See
-	linkgit:git-pack-refs[1].
+	linkgit:git-pack-refs[1]. This file is ignored if $GIT_COMMON_DIR
+	is set and "$GIT_COMMON_DIR/packed-refs" will be used instead.
 
 HEAD::
 	A symref (see glossary) to the `refs/heads/` namespace
@@ -133,6 +138,11 @@ being a symref to point at the current branch.  Such a state
 is often called 'detached HEAD.'  See linkgit:git-checkout[1]
 for details.
 
+config::
+	Repository specific configuration file. This file is ignored
+	if $GIT_COMMON_DIR is set and "$GIT_COMMON_DIR/config" will be
+	used instead.
+
 branches::
 	A slightly deprecated way to store shorthands to be used
 	to specify a URL to 'git fetch', 'git pull' and 'git push'.
@@ -140,7 +150,10 @@ branches::
 	'name' can be given to these commands in place of
 	'repository' argument.  See the REMOTES section in
 	linkgit:git-fetch[1] for details.  This mechanism is legacy
-	and not likely to be found in modern repositories.
+	and not likely to be found in modern repositories. This
+	directory is ignored if $GIT_COMMON_DIR is set and
+	"$GIT_COMMON_DIR/branches" will be used instead.
+
 
 hooks::
 	Hooks are customization scripts used by various Git
@@ -149,7 +162,9 @@ hooks::
 	default.  To enable, the `.sample` suffix has to be
 	removed from the filename by renaming.
 	Read linkgit:githooks[5] for more details about
-	each hook.
+	each hook. This directory is ignored if $GIT_COMMON_DIR is set
+	and "$GIT_COMMON_DIR/hooks" will be used instead.
+
 
 index::
 	The current index file for the repository.  It is
@@ -161,7 +176,8 @@ sharedindex.<SHA-1>::
 
 info::
 	Additional information about the repository is recorded
-	in this directory.
+	in this directory. This directory is ignored if $GIT_COMMON_DIR
+	is set and "$GIT_COMMON_DIR/index" will be used instead.
 
 info/refs::
 	This file helps dumb transports discover what refs are
@@ -201,12 +217,15 @@ remotes::
 	when interacting with remote repositories via 'git fetch',
 	'git pull' and 'git push' commands.  See the REMOTES section
 	in linkgit:git-fetch[1] for details.  This mechanism is legacy
-	and not likely to be found in modern repositories.
+	and not likely to be found in modern repositories. This
+	directory is ignored if $GIT_COMMON_DIR is set and
+	"$GIT_COMMON_DIR/remotes" will be used instead.
 
 logs::
-	Records of changes made to refs are stored in this
-	directory.  See linkgit:git-update-ref[1]
-	for more information.
+	Records of changes made to refs are stored in this directory.
+	See linkgit:git-update-ref[1] for more information. This
+	directory is ignored if $GIT_COMMON_DIR is set and
+	"$GIT_COMMON_DIR/logs" will be used instead.
 
 logs/refs/heads/`name`::
 	Records all changes made to the branch tip named `name`.
@@ -217,10 +236,14 @@ logs/refs/tags/`name`::
 shallow::
 	This is similar to `info/grafts` but is internally used
 	and maintained by shallow clone mechanism.  See `--depth`
-	option to linkgit:git-clone[1] and linkgit:git-fetch[1].
+	option to linkgit:git-clone[1] and linkgit:git-fetch[1]. This
+	file is ignored if $GIT_COMMON_DIR is set and
+	"$GIT_COMMON_DIR/shallow" will be used instead.
 
 modules::
-	Contains the git-repositories of the submodules.
+	Contains the git-repositories of the submodules. This
+	directory is ignored if $GIT_COMMON_DIR is set and
+	"$GIT_COMMON_DIR/modules" will be used instead.
 
 SEE ALSO
 --------
diff --git a/cache.h b/cache.h
index 161dbd5..bf26301 100644
--- a/cache.h
+++ b/cache.h
@@ -377,6 +377,7 @@ static inline enum object_type object_type(unsigned int mode)
 
 /* Double-check local_repo_env below if you add to this list. */
 #define GIT_DIR_ENVIRONMENT "GIT_DIR"
+#define GIT_COMMON_DIR_ENVIRONMENT "GIT_COMMON_DIR"
 #define GIT_NAMESPACE_ENVIRONMENT "GIT_NAMESPACE"
 #define GIT_WORK_TREE_ENVIRONMENT "GIT_WORK_TREE"
 #define GIT_PREFIX_ENVIRONMENT "GIT_PREFIX"
@@ -430,6 +431,7 @@ extern int is_inside_git_dir(void);
 extern char *git_work_tree_cfg;
 extern int is_inside_work_tree(void);
 extern const char *get_git_dir(void);
+extern const char *get_git_common_dir(void);
 extern int is_git_directory(const char *path);
 extern char *get_object_directory(void);
 extern char *get_index_file(void);
@@ -635,7 +637,7 @@ extern int fsync_object_files;
 extern int core_preload_index;
 extern int core_apply_sparse_checkout;
 extern int precomposed_unicode;
-extern int git_db_env, git_index_env, git_graft_env;
+extern int git_db_env, git_index_env, git_graft_env, git_common_dir_env;
 
 /*
  * The character that begins a commented line in user-editable file
diff --git a/environment.c b/environment.c
index fee12a6..78a07e4 100644
--- a/environment.c
+++ b/environment.c
@@ -81,9 +81,9 @@ static char *work_tree;
 static const char *namespace;
 static size_t namespace_len;
 
-static const char *git_dir;
+static const char *git_dir, *git_common_dir;
 static char *git_object_dir, *git_index_file, *git_graft_file;
-int git_db_env, git_index_env, git_graft_env;
+int git_db_env, git_index_env, git_graft_env, git_common_dir_env;
 
 /*
  * Repository-local GIT_* environment variables; see cache.h for details.
@@ -125,8 +125,8 @@ static char *expand_namespace(const char *raw_namespace)
 	return strbuf_detach(&buf, NULL);
 }
 
-static char *git_path_from_env(const char *envvar, const char *path,
-			       int *fromenv)
+static char *git_path_from_env(const char *envvar, const char *git_dir,
+			       const char *path, int *fromenv)
 {
 	const char *value = getenv(envvar);
 	if (!value) {
@@ -149,9 +149,18 @@ static void setup_git_env(void)
 		git_dir = DEFAULT_GIT_DIR_ENVIRONMENT;
 	gitfile = read_gitfile(git_dir);
 	git_dir = xstrdup(gitfile ? gitfile : git_dir);
-	git_object_dir = git_path_from_env(DB_ENVIRONMENT, "objects", &git_db_env);
-	git_index_file = git_path_from_env(INDEX_ENVIRONMENT, "index", &git_index_env);
-	git_graft_file = git_path_from_env(GRAFT_ENVIRONMENT, "info/grafts", &git_graft_env);
+	git_common_dir = getenv(GIT_COMMON_DIR_ENVIRONMENT);
+	if (git_common_dir) {
+		git_common_dir_env = 1;
+		git_common_dir = xstrdup(git_common_dir);
+	} else
+		git_common_dir = git_dir;
+	git_object_dir = git_path_from_env(DB_ENVIRONMENT, git_common_dir,
+					   "objects", &git_db_env);
+	git_index_file = git_path_from_env(INDEX_ENVIRONMENT, git_dir,
+					   "index", &git_index_env);
+	git_graft_file = git_path_from_env(GRAFT_ENVIRONMENT, git_common_dir,
+					   "info/grafts", &git_graft_env);
 	if (getenv(NO_REPLACE_OBJECTS_ENVIRONMENT))
 		check_replace_refs = 0;
 	namespace = expand_namespace(getenv(GIT_NAMESPACE_ENVIRONMENT));
@@ -174,6 +183,11 @@ const char *get_git_dir(void)
 	return git_dir;
 }
 
+const char *get_git_common_dir(void)
+{
+	return git_common_dir;
+}
+
 const char *get_git_namespace(void)
 {
 	if (!namespace)
diff --git a/path.c b/path.c
index 4910783..94db501 100644
--- a/path.c
+++ b/path.c
@@ -90,6 +90,38 @@ static void replace_dir(struct strbuf *buf, int len, const char *newdir)
 		buf->buf[newlen] = '/';
 }
 
+static const char *common_list[] = {
+	"/branches", "/hooks", "/info", "/logs", "/lost-found", "/modules",
+	"/objects", "/refs", "/remotes", "/rr-cache", "/svn",
+	"config", "gc.pid", "packed-refs", "shallow",
+	NULL
+};
+
+static void update_common_dir(struct strbuf *buf, int git_dir_len)
+{
+	char *base = buf->buf + git_dir_len;
+	const char **p;
+
+	if (is_dir_file(base, "logs", "HEAD"))
+		return;	/* keep this in $GIT_DIR */
+	for (p = common_list; *p; p++) {
+		const char *path = *p;
+		int is_dir = 0;
+		if (*path == '/') {
+			path++;
+			is_dir = 1;
+		}
+		if (is_dir && dir_prefix(base, path)) {
+			replace_dir(buf, git_dir_len, get_git_common_dir());
+			return;
+		}
+		if (!is_dir && !strcmp(base, path)) {
+			replace_dir(buf, git_dir_len, get_git_common_dir());
+			return;
+		}
+	}
+}
+
 static void adjust_git_path(struct strbuf *buf, int git_dir_len)
 {
 	const char *base = buf->buf + git_dir_len;
@@ -101,6 +133,8 @@ static void adjust_git_path(struct strbuf *buf, int git_dir_len)
 			      get_index_file(), strlen(get_index_file()));
 	else if (git_db_env && dir_prefix(base, "objects"))
 		replace_dir(buf, git_dir_len + 7, get_object_directory());
+	else if (git_common_dir_env)
+		update_common_dir(buf, git_dir_len);
 }
 
 static void do_git_path(struct strbuf *buf, const char *fmt, va_list args)
diff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh
index 33d2818..f5d6f80 100755
--- a/t/t0060-path-utils.sh
+++ b/t/t0060-path-utils.sh
@@ -262,5 +262,21 @@ test_expect_success 'setup fake objects directory foo' 'mkdir foo'
 test_git_path GIT_OBJECT_DIRECTORY=foo objects foo
 test_git_path GIT_OBJECT_DIRECTORY=foo objects/foo foo/foo
 test_git_path GIT_OBJECT_DIRECTORY=foo objects2 .git/objects2
+test_expect_success 'setup common repository' 'git --git-dir=bar init'
+test_git_path GIT_COMMON_DIR=bar index                    .git/index
+test_git_path GIT_COMMON_DIR=bar HEAD                     .git/HEAD
+test_git_path GIT_COMMON_DIR=bar logs/HEAD                .git/logs/HEAD
+test_git_path GIT_COMMON_DIR=bar objects                  bar/objects
+test_git_path GIT_COMMON_DIR=bar objects/bar              bar/objects/bar
+test_git_path GIT_COMMON_DIR=bar info/exclude             bar/info/exclude
+test_git_path GIT_COMMON_DIR=bar info/grafts              bar/info/grafts
+test_git_path GIT_COMMON_DIR=bar remotes/bar              bar/remotes/bar
+test_git_path GIT_COMMON_DIR=bar branches/bar             bar/branches/bar
+test_git_path GIT_COMMON_DIR=bar logs/refs/heads/master   bar/logs/refs/heads/master
+test_git_path GIT_COMMON_DIR=bar refs/heads/master        bar/refs/heads/master
+test_git_path GIT_COMMON_DIR=bar hooks/me                 bar/hooks/me
+test_git_path GIT_COMMON_DIR=bar config                   bar/config
+test_git_path GIT_COMMON_DIR=bar packed-refs              bar/packed-refs
+test_git_path GIT_COMMON_DIR=bar shallow                  bar/shallow
 
 test_done
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 12/32] git-sh-setup.sh: use rev-parse --git-path to get $GIT_DIR/objects
  2014-09-28  1:22     ` [PATCH v3 " Nguyễn Thái Ngọc Duy
                         ` (10 preceding siblings ...)
  2014-09-28  1:22       ` [PATCH v3 11/32] $GIT_COMMON_DIR: a new environment variable Nguyễn Thái Ngọc Duy
@ 2014-09-28  1:22       ` Nguyễn Thái Ngọc Duy
  2014-09-28  1:22       ` [PATCH v3 13/32] *.sh: avoid hardcoding $GIT_DIR/hooks/ Nguyễn Thái Ngọc Duy
                         ` (20 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-28  1:22 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

If $GIT_COMMON_DIR is set, $GIT_OBJECT_DIRECTORY should be
$GIT_COMMON_DIR/objects, not $GIT_DIR/objects. Just let rev-parse
--git-path handle it.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 git-sh-setup.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/git-sh-setup.sh b/git-sh-setup.sh
index 9447980..d3dbb2f 100644
--- a/git-sh-setup.sh
+++ b/git-sh-setup.sh
@@ -345,7 +345,7 @@ then
 		echo >&2 "Unable to determine absolute path of git directory"
 		exit 1
 	}
-	: ${GIT_OBJECT_DIRECTORY="$GIT_DIR/objects"}
+	: ${GIT_OBJECT_DIRECTORY="$(git rev-parse --git-path objects)"}
 fi
 
 peel_committish () {
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 13/32] *.sh: avoid hardcoding $GIT_DIR/hooks/...
  2014-09-28  1:22     ` [PATCH v3 " Nguyễn Thái Ngọc Duy
                         ` (11 preceding siblings ...)
  2014-09-28  1:22       ` [PATCH v3 12/32] git-sh-setup.sh: use rev-parse --git-path to get $GIT_DIR/objects Nguyễn Thái Ngọc Duy
@ 2014-09-28  1:22       ` Nguyễn Thái Ngọc Duy
  2014-09-28  1:22       ` [PATCH v3 14/32] git-stash: avoid hardcoding $GIT_DIR/logs/ Nguyễn Thái Ngọc Duy
                         ` (19 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-28  1:22 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

If $GIT_COMMON_DIR is set, it should be $GIT_COMMON_DIR/hooks/, not
$GIT_DIR/hooks/. Just let rev-parse --git-path handle it.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 git-am.sh                              | 22 +++++++++++-----------
 git-rebase--interactive.sh             |  6 +++---
 git-rebase--merge.sh                   |  6 ++----
 git-rebase.sh                          |  4 ++--
 templates/hooks--applypatch-msg.sample |  4 ++--
 templates/hooks--pre-applypatch.sample |  4 ++--
 6 files changed, 22 insertions(+), 24 deletions(-)

diff --git a/git-am.sh b/git-am.sh
index ee61a77..66803d1 100755
--- a/git-am.sh
+++ b/git-am.sh
@@ -810,10 +810,10 @@ To restore the original branch and stop patching run \"\$cmdline --abort\"."
 		continue
 	fi
 
-	if test -x "$GIT_DIR"/hooks/applypatch-msg
+	hook="$(git rev-parse --git-path hooks/applypatch-msg)"
+	if test -x "$hook"
 	then
-		"$GIT_DIR"/hooks/applypatch-msg "$dotest/final-commit" ||
-		stop_here $this
+		"$hook" "$dotest/final-commit" || stop_here $this
 	fi
 
 	if test -f "$dotest/final-commit"
@@ -887,9 +887,10 @@ did you forget to use 'git add'?"
 		stop_here_user_resolve $this
 	fi
 
-	if test -x "$GIT_DIR"/hooks/pre-applypatch
+	hook="$(git rev-parse --git-path hooks/pre-applypatch)"
+	if test -x "$hook"
 	then
-		"$GIT_DIR"/hooks/pre-applypatch || stop_here $this
+		"$hook" || stop_here $this
 	fi
 
 	tree=$(git write-tree) &&
@@ -916,18 +917,17 @@ did you forget to use 'git add'?"
 		echo "$(cat "$dotest/original-commit") $commit" >> "$dotest/rewritten"
 	fi
 
-	if test -x "$GIT_DIR"/hooks/post-applypatch
-	then
-		"$GIT_DIR"/hooks/post-applypatch
-	fi
+	hook="$(git rev-parse --git-path hooks/post-applypatch)"
+	test -x "$hook" && "$hook"
 
 	go_next
 done
 
 if test -s "$dotest"/rewritten; then
     git notes copy --for-rewrite=rebase < "$dotest"/rewritten
-    if test -x "$GIT_DIR"/hooks/post-rewrite; then
-	"$GIT_DIR"/hooks/post-rewrite rebase < "$dotest"/rewritten
+    hook="$(git rev-parse --git-path hooks/post-rewrite)"
+    if test -x "$hook"; then
+	"$hook" rebase < "$dotest"/rewritten
     fi
 fi
 
diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
index b64dd28..b32f797 100644
--- a/git-rebase--interactive.sh
+++ b/git-rebase--interactive.sh
@@ -642,9 +642,9 @@ do_next () {
 		git notes copy --for-rewrite=rebase < "$rewritten_list" ||
 		true # we don't care if this copying failed
 	} &&
-	if test -x "$GIT_DIR"/hooks/post-rewrite &&
-		test -s "$rewritten_list"; then
-		"$GIT_DIR"/hooks/post-rewrite rebase < "$rewritten_list"
+	hook="$(git rev-parse --git-path hooks/post-rewrite)"
+	if test -x "$hook" && test -s "$rewritten_list"; then
+		"$hook" rebase < "$rewritten_list"
 		true # we don't care if this hook failed
 	fi &&
 	warn "Successfully rebased and updated $head_name."
diff --git a/git-rebase--merge.sh b/git-rebase--merge.sh
index d3fb67d..2cc2a6d 100644
--- a/git-rebase--merge.sh
+++ b/git-rebase--merge.sh
@@ -94,10 +94,8 @@ finish_rb_merge () {
 	if test -s "$state_dir"/rewritten
 	then
 		git notes copy --for-rewrite=rebase <"$state_dir"/rewritten
-		if test -x "$GIT_DIR"/hooks/post-rewrite
-		then
-			"$GIT_DIR"/hooks/post-rewrite rebase <"$state_dir"/rewritten
-		fi
+		hook="$(git rev-parse --git-path hooks/post-rewrite)"
+		test -x "$hook" && "$hook" rebase <"$state_dir"/rewritten
 	fi
 	say All done.
 }
diff --git a/git-rebase.sh b/git-rebase.sh
index 55da9db..fb935a0 100755
--- a/git-rebase.sh
+++ b/git-rebase.sh
@@ -202,9 +202,9 @@ run_specific_rebase () {
 
 run_pre_rebase_hook () {
 	if test -z "$ok_to_skip_pre_rebase" &&
-	   test -x "$GIT_DIR/hooks/pre-rebase"
+	   test -x "$(git rev-parse --git-path hooks/pre-rebase)"
 	then
-		"$GIT_DIR/hooks/pre-rebase" ${1+"$@"} ||
+		"$(git rev-parse --git-path hooks/pre-rebase)" ${1+"$@"} ||
 		die "$(gettext "The pre-rebase hook refused to rebase.")"
 	fi
 }
diff --git a/templates/hooks--applypatch-msg.sample b/templates/hooks--applypatch-msg.sample
index 8b2a2fe..a5d7b84 100755
--- a/templates/hooks--applypatch-msg.sample
+++ b/templates/hooks--applypatch-msg.sample
@@ -10,6 +10,6 @@
 # To enable this hook, rename this file to "applypatch-msg".
 
 . git-sh-setup
-test -x "$GIT_DIR/hooks/commit-msg" &&
-	exec "$GIT_DIR/hooks/commit-msg" ${1+"$@"}
+commitmsg="$(git rev-parse --git-path hooks/commit-msg)"
+test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"}
 :
diff --git a/templates/hooks--pre-applypatch.sample b/templates/hooks--pre-applypatch.sample
index b1f187c..4142082 100755
--- a/templates/hooks--pre-applypatch.sample
+++ b/templates/hooks--pre-applypatch.sample
@@ -9,6 +9,6 @@
 # To enable this hook, rename this file to "pre-applypatch".
 
 . git-sh-setup
-test -x "$GIT_DIR/hooks/pre-commit" &&
-	exec "$GIT_DIR/hooks/pre-commit" ${1+"$@"}
+precommit="$(git rev-parse --git-path hooks/pre-commit)"
+test -x "$precommit" && exec "$precommit" ${1+"$@"}
 :
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 14/32] git-stash: avoid hardcoding $GIT_DIR/logs/....
  2014-09-28  1:22     ` [PATCH v3 " Nguyễn Thái Ngọc Duy
                         ` (12 preceding siblings ...)
  2014-09-28  1:22       ` [PATCH v3 13/32] *.sh: avoid hardcoding $GIT_DIR/hooks/ Nguyễn Thái Ngọc Duy
@ 2014-09-28  1:22       ` Nguyễn Thái Ngọc Duy
  2014-09-28  1:22       ` [PATCH v3 15/32] setup.c: convert is_git_directory() to use strbuf Nguyễn Thái Ngọc Duy
                         ` (18 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-28  1:22 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 git-stash.sh | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/git-stash.sh b/git-stash.sh
index 4294aa0..2784f8b 100755
--- a/git-stash.sh
+++ b/git-stash.sh
@@ -184,7 +184,7 @@ store_stash () {
 	fi
 
 	# Make sure the reflog for stash is kept.
-	: >>"$GIT_DIR/logs/$ref_stash"
+	: >>"$(git rev-parse --git-path logs/$ref_stash)"
 	git update-ref -m "$stash_msg" $ref_stash $w_commit
 	ret=$?
 	test $ret != 0 && test -z $quiet &&
@@ -259,7 +259,7 @@ save_stash () {
 		say "$(gettext "No local changes to save")"
 		exit 0
 	fi
-	test -f "$GIT_DIR/logs/$ref_stash" ||
+	test -f "$(git rev-parse --git-path logs/$ref_stash)" ||
 		clear_stash || die "$(gettext "Cannot initialize stash")"
 
 	create_stash "$stash_msg" $untracked
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 15/32] setup.c: convert is_git_directory() to use strbuf
  2014-09-28  1:22     ` [PATCH v3 " Nguyễn Thái Ngọc Duy
                         ` (13 preceding siblings ...)
  2014-09-28  1:22       ` [PATCH v3 14/32] git-stash: avoid hardcoding $GIT_DIR/logs/ Nguyễn Thái Ngọc Duy
@ 2014-09-28  1:22       ` Nguyễn Thái Ngọc Duy
  2014-09-28  1:22       ` [PATCH v3 16/32] setup.c: detect $GIT_COMMON_DIR in is_git_directory() Nguyễn Thái Ngọc Duy
                         ` (17 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-28  1:22 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 setup.c | 37 +++++++++++++++++++++----------------
 1 file changed, 21 insertions(+), 16 deletions(-)

diff --git a/setup.c b/setup.c
index 979b13f..afd6742 100644
--- a/setup.c
+++ b/setup.c
@@ -238,31 +238,36 @@ void verify_non_filename(const char *prefix, const char *arg)
  */
 int is_git_directory(const char *suspect)
 {
-	char path[PATH_MAX];
-	size_t len = strlen(suspect);
+	struct strbuf path = STRBUF_INIT;
+	int ret = 0;
+	size_t len;
 
-	if (PATH_MAX <= len + strlen("/objects"))
-		die("Too long path: %.*s", 60, suspect);
-	strcpy(path, suspect);
+	strbuf_addstr(&path, suspect);
+	len = path.len;
 	if (getenv(DB_ENVIRONMENT)) {
 		if (access(getenv(DB_ENVIRONMENT), X_OK))
-			return 0;
+			goto done;
 	}
 	else {
-		strcpy(path + len, "/objects");
-		if (access(path, X_OK))
-			return 0;
+		strbuf_addstr(&path, "/objects");
+		if (access(path.buf, X_OK))
+			goto done;
 	}
 
-	strcpy(path + len, "/refs");
-	if (access(path, X_OK))
-		return 0;
+	strbuf_setlen(&path, len);
+	strbuf_addstr(&path, "/refs");
+	if (access(path.buf, X_OK))
+		goto done;
 
-	strcpy(path + len, "/HEAD");
-	if (validate_headref(path))
-		return 0;
+	strbuf_setlen(&path, len);
+	strbuf_addstr(&path, "/HEAD");
+	if (validate_headref(path.buf))
+		goto done;
 
-	return 1;
+	ret = 1;
+done:
+	strbuf_release(&path);
+	return ret;
 }
 
 int is_inside_git_dir(void)
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 16/32] setup.c: detect $GIT_COMMON_DIR in is_git_directory()
  2014-09-28  1:22     ` [PATCH v3 " Nguyễn Thái Ngọc Duy
                         ` (14 preceding siblings ...)
  2014-09-28  1:22       ` [PATCH v3 15/32] setup.c: convert is_git_directory() to use strbuf Nguyễn Thái Ngọc Duy
@ 2014-09-28  1:22       ` Nguyễn Thái Ngọc Duy
  2014-09-28  1:22       ` [PATCH v3 17/32] setup.c: convert check_repository_format_gently to use strbuf Nguyễn Thái Ngọc Duy
                         ` (16 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-28  1:22 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

If the file "$GIT_DIR/commondir" exists, it contains the value of
$GIT_COMMON_DIR.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 Documentation/gitrepository-layout.txt |  7 ++++++
 setup.c                                | 43 +++++++++++++++++++++++++++++-----
 2 files changed, 44 insertions(+), 6 deletions(-)

diff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt
index 2b5966a..2dc5667 100644
--- a/Documentation/gitrepository-layout.txt
+++ b/Documentation/gitrepository-layout.txt
@@ -240,6 +240,13 @@ shallow::
 	file is ignored if $GIT_COMMON_DIR is set and
 	"$GIT_COMMON_DIR/shallow" will be used instead.
 
+commondir::
+	If this file exists, $GIT_COMMON_DIR (see linkgit:git[1]) will
+	be set to the path specified in this file if it is not
+	explicitly set. If the specified path is relative, it is
+	relative to $GIT_DIR. The repository with commondir is
+	incomplete without the repository pointed by "commondir".
+
 modules::
 	Contains the git-repositories of the submodules. This
 	directory is ignored if $GIT_COMMON_DIR is set and
diff --git a/setup.c b/setup.c
index afd6742..6c52f75 100644
--- a/setup.c
+++ b/setup.c
@@ -224,6 +224,33 @@ void verify_non_filename(const char *prefix, const char *arg)
 	    "'git <command> [<revision>...] -- [<file>...]'", arg);
 }
 
+static void get_common_dir(struct strbuf *sb, const char *gitdir)
+{
+	struct strbuf data = STRBUF_INIT;
+	struct strbuf path = STRBUF_INIT;
+	const char *git_common_dir = getenv(GIT_COMMON_DIR_ENVIRONMENT);
+	if (git_common_dir) {
+		strbuf_addstr(sb, git_common_dir);
+		return;
+	}
+	strbuf_addf(&path, "%s/commondir", gitdir);
+	if (file_exists(path.buf)) {
+		if (strbuf_read_file(&data, path.buf, 0) <= 0)
+			die_errno(_("failed to read %s"), path.buf);
+		while (data.len && (data.buf[data.len - 1] == '\n' ||
+				    data.buf[data.len - 1] == '\r'))
+			data.len--;
+		data.buf[data.len] = '\0';
+		strbuf_reset(&path);
+		if (!is_absolute_path(data.buf))
+			strbuf_addf(&path, "%s/", gitdir);
+		strbuf_addbuf(&path, &data);
+		strbuf_addstr(sb, real_path(path.buf));
+	} else
+		strbuf_addstr(sb, gitdir);
+	strbuf_release(&data);
+	strbuf_release(&path);
+}
 
 /*
  * Test if it looks like we're at a git directory.
@@ -242,13 +269,22 @@ int is_git_directory(const char *suspect)
 	int ret = 0;
 	size_t len;
 
-	strbuf_addstr(&path, suspect);
+	/* Check worktree-related signatures */
+	strbuf_addf(&path, "%s/HEAD", suspect);
+	if (validate_headref(path.buf))
+		goto done;
+
+	strbuf_reset(&path);
+	get_common_dir(&path, suspect);
 	len = path.len;
+
+	/* Check non-worktree-related signatures */
 	if (getenv(DB_ENVIRONMENT)) {
 		if (access(getenv(DB_ENVIRONMENT), X_OK))
 			goto done;
 	}
 	else {
+		strbuf_setlen(&path, len);
 		strbuf_addstr(&path, "/objects");
 		if (access(path.buf, X_OK))
 			goto done;
@@ -259,11 +295,6 @@ int is_git_directory(const char *suspect)
 	if (access(path.buf, X_OK))
 		goto done;
 
-	strbuf_setlen(&path, len);
-	strbuf_addstr(&path, "/HEAD");
-	if (validate_headref(path.buf))
-		goto done;
-
 	ret = 1;
 done:
 	strbuf_release(&path);
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 17/32] setup.c: convert check_repository_format_gently to use strbuf
  2014-09-28  1:22     ` [PATCH v3 " Nguyễn Thái Ngọc Duy
                         ` (15 preceding siblings ...)
  2014-09-28  1:22       ` [PATCH v3 16/32] setup.c: detect $GIT_COMMON_DIR in is_git_directory() Nguyễn Thái Ngọc Duy
@ 2014-09-28  1:22       ` Nguyễn Thái Ngọc Duy
  2014-09-28  1:22       ` [PATCH v3 18/32] setup.c: detect $GIT_COMMON_DIR check_repository_format_gently() Nguyễn Thái Ngọc Duy
                         ` (15 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-28  1:22 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 setup.c | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)

diff --git a/setup.c b/setup.c
index 6c52f75..00a23e6 100644
--- a/setup.c
+++ b/setup.c
@@ -342,7 +342,9 @@ void setup_work_tree(void)
 
 static int check_repository_format_gently(const char *gitdir, int *nongit_ok)
 {
-	char repo_config[PATH_MAX+1];
+	struct strbuf sb = STRBUF_INIT;
+	const char *repo_config;
+	int ret = 0;
 
 	/*
 	 * git_config() can't be used here because it calls git_pathdup()
@@ -353,7 +355,8 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)
 	 * Use a gentler version of git_config() to check if this repo
 	 * is a good one.
 	 */
-	snprintf(repo_config, PATH_MAX, "%s/config", gitdir);
+	strbuf_addf(&sb, "%s/config", gitdir);
+	repo_config = sb.buf;
 	git_config_early(check_repository_format_version, NULL, repo_config);
 	if (GIT_REPO_VERSION < repository_format_version) {
 		if (!nongit_ok)
@@ -363,9 +366,10 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)
 			GIT_REPO_VERSION, repository_format_version);
 		warning("Please upgrade Git");
 		*nongit_ok = -1;
-		return -1;
+		ret = -1;
 	}
-	return 0;
+	strbuf_release(&sb);
+	return ret;
 }
 
 /*
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 18/32] setup.c: detect $GIT_COMMON_DIR check_repository_format_gently()
  2014-09-28  1:22     ` [PATCH v3 " Nguyễn Thái Ngọc Duy
                         ` (16 preceding siblings ...)
  2014-09-28  1:22       ` [PATCH v3 17/32] setup.c: convert check_repository_format_gently to use strbuf Nguyễn Thái Ngọc Duy
@ 2014-09-28  1:22       ` Nguyễn Thái Ngọc Duy
  2014-09-28  1:22       ` [PATCH v3 19/32] setup.c: support multi-checkout repo setup Nguyễn Thái Ngọc Duy
                         ` (14 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-28  1:22 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 setup.c | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/setup.c b/setup.c
index 00a23e6..1d4f1aa 100644
--- a/setup.c
+++ b/setup.c
@@ -346,6 +346,10 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)
 	const char *repo_config;
 	int ret = 0;
 
+	get_common_dir(&sb, gitdir);
+	strbuf_addstr(&sb, "/config");
+	repo_config = sb.buf;
+
 	/*
 	 * git_config() can't be used here because it calls git_pathdup()
 	 * to get $GIT_CONFIG/config. That call will make setup_git_env()
@@ -355,8 +359,6 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)
 	 * Use a gentler version of git_config() to check if this repo
 	 * is a good one.
 	 */
-	strbuf_addf(&sb, "%s/config", gitdir);
-	repo_config = sb.buf;
 	git_config_early(check_repository_format_version, NULL, repo_config);
 	if (GIT_REPO_VERSION < repository_format_version) {
 		if (!nongit_ok)
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 19/32] setup.c: support multi-checkout repo setup
  2014-09-28  1:22     ` [PATCH v3 " Nguyễn Thái Ngọc Duy
                         ` (17 preceding siblings ...)
  2014-09-28  1:22       ` [PATCH v3 18/32] setup.c: detect $GIT_COMMON_DIR check_repository_format_gently() Nguyễn Thái Ngọc Duy
@ 2014-09-28  1:22       ` Nguyễn Thái Ngọc Duy
  2014-10-01 16:19         ` Johannes Sixt
  2014-09-28  1:22       ` [PATCH v3 20/32] wrapper.c: wrapper to open a file, fprintf then close Nguyễn Thái Ngọc Duy
                         ` (13 subsequent siblings)
  32 siblings, 1 reply; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-28  1:22 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

The repo setup procedure is updated to detect $GIT_DIR/commondir and
set $GIT_COMMON_DIR properly.

The core.worktree is ignored when $GIT_COMMON_DIR is set. This is
because the config file is shared in multi-checkout setup, but
checkout directories _are_ different. Making core.worktree effective
in all checkouts mean it's back to a single checkout.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 Documentation/config.txt        |  2 ++
 Documentation/git-rev-parse.txt |  3 ++
 builtin/rev-parse.c             |  4 +++
 cache.h                         |  1 +
 environment.c                   |  8 ++---
 setup.c                         | 33 +++++++++++++-----
 t/t1501-worktree.sh             | 76 +++++++++++++++++++++++++++++++++++++++++
 t/t1510-repo-setup.sh           |  1 +
 trace.c                         |  1 +
 9 files changed, 115 insertions(+), 14 deletions(-)

diff --git a/Documentation/config.txt b/Documentation/config.txt
index 3b5b24a..98b8ef0 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -380,6 +380,8 @@ false), while all other repositories are assumed to be bare (bare
 
 core.worktree::
 	Set the path to the root of the working tree.
+	If GIT_COMMON_DIR environment variable is set, core.worktree
+	is ignored and not used for determining the root of working tree.
 	This can be overridden by the GIT_WORK_TREE environment
 	variable and the '--work-tree' command-line option.
 	The value can be an absolute path or relative to the path to
diff --git a/Documentation/git-rev-parse.txt b/Documentation/git-rev-parse.txt
index 0fed5a4..c10cc62 100644
--- a/Documentation/git-rev-parse.txt
+++ b/Documentation/git-rev-parse.txt
@@ -215,6 +215,9 @@ If `$GIT_DIR` is not defined and the current directory
 is not detected to lie in a Git repository or work tree
 print a message to stderr and exit with nonzero status.
 
+--git-common-dir::
+	Show `$GIT_COMMON_DIR` if defined, else `$GIT_DIR`.
+
 --is-inside-git-dir::
 	When the current working directory is below the repository
 	directory print "true", otherwise "false".
diff --git a/builtin/rev-parse.c b/builtin/rev-parse.c
index a6f4697..1ce73e8 100644
--- a/builtin/rev-parse.c
+++ b/builtin/rev-parse.c
@@ -759,6 +759,10 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)
 				free(cwd);
 				continue;
 			}
+			if (!strcmp(arg, "--git-common-dir")) {
+				puts(get_git_common_dir());
+				continue;
+			}
 			if (!strcmp(arg, "--resolve-git-dir")) {
 				const char *gitdir = argv[++i];
 				if (!gitdir)
diff --git a/cache.h b/cache.h
index bf26301..1784fc6 100644
--- a/cache.h
+++ b/cache.h
@@ -437,6 +437,7 @@ extern char *get_object_directory(void);
 extern char *get_index_file(void);
 extern char *get_graft_file(void);
 extern int set_git_dir(const char *path);
+extern int get_common_dir(struct strbuf *sb, const char *gitdir);
 extern const char *get_git_namespace(void);
 extern const char *strip_namespace(const char *namespaced_ref);
 extern const char *get_git_work_tree(void);
diff --git a/environment.c b/environment.c
index 78a07e4..d5b0788 100644
--- a/environment.c
+++ b/environment.c
@@ -141,6 +141,7 @@ static char *git_path_from_env(const char *envvar, const char *git_dir,
 
 static void setup_git_env(void)
 {
+	struct strbuf sb = STRBUF_INIT;
 	const char *gitfile;
 	const char *shallow_file;
 
@@ -149,12 +150,9 @@ static void setup_git_env(void)
 		git_dir = DEFAULT_GIT_DIR_ENVIRONMENT;
 	gitfile = read_gitfile(git_dir);
 	git_dir = xstrdup(gitfile ? gitfile : git_dir);
-	git_common_dir = getenv(GIT_COMMON_DIR_ENVIRONMENT);
-	if (git_common_dir) {
+	if (get_common_dir(&sb, git_dir))
 		git_common_dir_env = 1;
-		git_common_dir = xstrdup(git_common_dir);
-	} else
-		git_common_dir = git_dir;
+	git_common_dir = strbuf_detach(&sb, NULL);
 	git_object_dir = git_path_from_env(DB_ENVIRONMENT, git_common_dir,
 					   "objects", &git_db_env);
 	git_index_file = git_path_from_env(INDEX_ENVIRONMENT, git_dir,
diff --git a/setup.c b/setup.c
index 1d4f1aa..b99bdd3 100644
--- a/setup.c
+++ b/setup.c
@@ -224,14 +224,15 @@ void verify_non_filename(const char *prefix, const char *arg)
 	    "'git <command> [<revision>...] -- [<file>...]'", arg);
 }
 
-static void get_common_dir(struct strbuf *sb, const char *gitdir)
+int get_common_dir(struct strbuf *sb, const char *gitdir)
 {
 	struct strbuf data = STRBUF_INIT;
 	struct strbuf path = STRBUF_INIT;
 	const char *git_common_dir = getenv(GIT_COMMON_DIR_ENVIRONMENT);
+	int ret = 0;
 	if (git_common_dir) {
 		strbuf_addstr(sb, git_common_dir);
-		return;
+		return 1;
 	}
 	strbuf_addf(&path, "%s/commondir", gitdir);
 	if (file_exists(path.buf)) {
@@ -246,10 +247,12 @@ static void get_common_dir(struct strbuf *sb, const char *gitdir)
 			strbuf_addf(&path, "%s/", gitdir);
 		strbuf_addbuf(&path, &data);
 		strbuf_addstr(sb, real_path(path.buf));
+		ret = 1;
 	} else
 		strbuf_addstr(sb, gitdir);
 	strbuf_release(&data);
 	strbuf_release(&path);
+	return ret;
 }
 
 /*
@@ -340,13 +343,26 @@ void setup_work_tree(void)
 	initialized = 1;
 }
 
+static int check_repo_format(const char *var, const char *value, void *cb)
+{
+	if (strcmp(var, "core.repositoryformatversion") == 0)
+		repository_format_version = git_config_int(var, value);
+	else if (strcmp(var, "core.sharedrepository") == 0)
+		shared_repository = git_config_perm(var, value);
+	return 0;
+}
+
 static int check_repository_format_gently(const char *gitdir, int *nongit_ok)
 {
 	struct strbuf sb = STRBUF_INIT;
 	const char *repo_config;
+	config_fn_t fn;
 	int ret = 0;
 
-	get_common_dir(&sb, gitdir);
+	if (get_common_dir(&sb, gitdir))
+		fn = check_repo_format;
+	else
+		fn = check_repository_format_version;
 	strbuf_addstr(&sb, "/config");
 	repo_config = sb.buf;
 
@@ -359,7 +375,7 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)
 	 * Use a gentler version of git_config() to check if this repo
 	 * is a good one.
 	 */
-	git_config_early(check_repository_format_version, NULL, repo_config);
+	git_config_early(fn, NULL, repo_config);
 	if (GIT_REPO_VERSION < repository_format_version) {
 		if (!nongit_ok)
 			die ("Expected git repo version <= %d, found %d",
@@ -841,11 +857,10 @@ int git_config_perm(const char *var, const char *value)
 
 int check_repository_format_version(const char *var, const char *value, void *cb)
 {
-	if (strcmp(var, "core.repositoryformatversion") == 0)
-		repository_format_version = git_config_int(var, value);
-	else if (strcmp(var, "core.sharedrepository") == 0)
-		shared_repository = git_config_perm(var, value);
-	else if (strcmp(var, "core.bare") == 0) {
+	int ret = check_repo_format(var, value, cb);
+	if (ret)
+		return ret;
+	if (strcmp(var, "core.bare") == 0) {
 		is_bare_repository_cfg = git_config_bool(var, value);
 		if (is_bare_repository_cfg == 1)
 			inside_work_tree = -1;
diff --git a/t/t1501-worktree.sh b/t/t1501-worktree.sh
index 8f36aa9..e6ac7a4 100755
--- a/t/t1501-worktree.sh
+++ b/t/t1501-worktree.sh
@@ -346,4 +346,80 @@ test_expect_success 'relative $GIT_WORK_TREE and git subprocesses' '
 	test_cmp expected actual
 '
 
+test_expect_success 'Multi-worktree setup' '
+	mkdir work &&
+	mkdir -p repo.git/repos/foo &&
+	cp repo.git/HEAD repo.git/index repo.git/repos/foo &&
+	sane_unset GIT_DIR GIT_CONFIG GIT_WORK_TREE
+'
+
+test_expect_success 'GIT_DIR set (1)' '
+	echo "gitdir: repo.git/repos/foo" >gitfile &&
+	echo ../.. >repo.git/repos/foo/commondir &&
+	(
+		cd work &&
+		GIT_DIR=../gitfile git rev-parse --git-common-dir >actual &&
+		test-path-utils real_path "$TRASH_DIRECTORY/repo.git" >expect &&
+		test_cmp expect actual
+	)
+'
+
+test_expect_success 'GIT_DIR set (2)' '
+	echo "gitdir: repo.git/repos/foo" >gitfile &&
+	echo "$TRASH_DIRECTORY/repo.git" >repo.git/repos/foo/commondir &&
+	(
+		cd work &&
+		GIT_DIR=../gitfile git rev-parse --git-common-dir >actual &&
+		test-path-utils real_path "$TRASH_DIRECTORY/repo.git" >expect &&
+		test_cmp expect actual
+	)
+'
+
+test_expect_success 'Auto discovery' '
+	echo "gitdir: repo.git/repos/foo" >.git &&
+	echo ../.. >repo.git/repos/foo/commondir &&
+	(
+		cd work &&
+		git rev-parse --git-common-dir >actual &&
+		test-path-utils real_path "$TRASH_DIRECTORY/repo.git" >expect &&
+		test_cmp expect actual &&
+		echo haha >data1 &&
+		git add data1 &&
+		git ls-files --full-name :/ | grep data1 >actual &&
+		echo work/data1 >expect &&
+		test_cmp expect actual
+	)
+'
+
+test_expect_success '$GIT_DIR/common overrides core.worktree' '
+	mkdir elsewhere &&
+	git --git-dir=repo.git config core.worktree "$TRASH_DIRECTORY/elsewhere" &&
+	echo "gitdir: repo.git/repos/foo" >.git &&
+	echo ../.. >repo.git/repos/foo/commondir &&
+	(
+		cd work &&
+		git rev-parse --git-common-dir >actual &&
+		test-path-utils real_path "$TRASH_DIRECTORY/repo.git" >expect &&
+		test_cmp expect actual &&
+		echo haha >data2 &&
+		git add data2 &&
+		git ls-files --full-name :/ | grep data2 >actual &&
+		echo work/data2 >expect &&
+		test_cmp expect actual
+	)
+'
+
+test_expect_success '$GIT_WORK_TREE overrides $GIT_DIR/common' '
+	echo "gitdir: repo.git/repos/foo" >.git &&
+	echo ../.. >repo.git/repos/foo/commondir &&
+	(
+		cd work &&
+		echo haha >data3 &&
+		git --git-dir=../.git --work-tree=. add data3 &&
+		git ls-files --full-name -- :/ | grep data3 >actual &&
+		echo data3 >expect &&
+		test_cmp expect actual
+	)
+'
+
 test_done
diff --git a/t/t1510-repo-setup.sh b/t/t1510-repo-setup.sh
index e1b2a99..33c1a58 100755
--- a/t/t1510-repo-setup.sh
+++ b/t/t1510-repo-setup.sh
@@ -106,6 +106,7 @@ setup_env () {
 expect () {
 	cat >"$1/expected" <<-EOF
 	setup: git_dir: $2
+	setup: git_common_dir: $2
 	setup: worktree: $3
 	setup: cwd: $4
 	setup: prefix: $5
diff --git a/trace.c b/trace.c
index b6f25a2..c073a7d 100644
--- a/trace.c
+++ b/trace.c
@@ -312,6 +312,7 @@ void trace_repo_setup(const char *prefix)
 		prefix = "(null)";
 
 	trace_printf_key(&key, "setup: git_dir: %s\n", quote_crnl(get_git_dir()));
+	trace_printf_key(&key, "setup: git_common_dir: %s\n", quote_crnl(get_git_common_dir()));
 	trace_printf_key(&key, "setup: worktree: %s\n", quote_crnl(git_work_tree));
 	trace_printf_key(&key, "setup: cwd: %s\n", quote_crnl(cwd));
 	trace_printf_key(&key, "setup: prefix: %s\n", quote_crnl(prefix));
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 20/32] wrapper.c: wrapper to open a file, fprintf then close
  2014-09-28  1:22     ` [PATCH v3 " Nguyễn Thái Ngọc Duy
                         ` (18 preceding siblings ...)
  2014-09-28  1:22       ` [PATCH v3 19/32] setup.c: support multi-checkout repo setup Nguyễn Thái Ngọc Duy
@ 2014-09-28  1:22       ` Nguyễn Thái Ngọc Duy
  2014-09-28  1:22       ` [PATCH v3 21/32] use new wrapper write_file() for simple file writing Nguyễn Thái Ngọc Duy
                         ` (12 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-28  1:22 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 cache.h   |  2 ++
 wrapper.c | 31 +++++++++++++++++++++++++++++++
 2 files changed, 33 insertions(+)

diff --git a/cache.h b/cache.h
index 1784fc6..aedfd05 100644
--- a/cache.h
+++ b/cache.h
@@ -1452,6 +1452,8 @@ static inline ssize_t write_str_in_full(int fd, const char *str)
 {
 	return write_in_full(fd, str, strlen(str));
 }
+__attribute__((format (printf,3,4)))
+extern int write_file(const char *path, int fatal, const char *fmt, ...);
 
 /* pager.c */
 extern void setup_pager(void);
diff --git a/wrapper.c b/wrapper.c
index 25074d7..d53a3b3 100644
--- a/wrapper.c
+++ b/wrapper.c
@@ -537,3 +537,34 @@ char *xgetcwd(void)
 		die_errno(_("unable to get current working directory"));
 	return strbuf_detach(&sb, NULL);
 }
+
+int write_file(const char *path, int fatal, const char *fmt, ...)
+{
+	struct strbuf sb = STRBUF_INIT;
+	va_list params;
+	int fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0666);
+	if (fd < 0) {
+		if (fatal)
+			die_errno(_("could not open %s for writing"), path);
+		return -1;
+	}
+	va_start(params, fmt);
+	strbuf_vaddf(&sb, fmt, params);
+	va_end(params);
+	if (write_in_full(fd, sb.buf, sb.len) != sb.len) {
+		int err = errno;
+		close(fd);
+		strbuf_release(&sb);
+		errno = err;
+		if (fatal)
+			die_errno(_("could not write to %s"), path);
+		return -1;
+	}
+	strbuf_release(&sb);
+	if (close(fd)) {
+		if (fatal)
+			die_errno(_("could not close %s"), path);
+		return -1;
+	}
+	return 0;
+}
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 21/32] use new wrapper write_file() for simple file writing
  2014-09-28  1:22     ` [PATCH v3 " Nguyễn Thái Ngọc Duy
                         ` (19 preceding siblings ...)
  2014-09-28  1:22       ` [PATCH v3 20/32] wrapper.c: wrapper to open a file, fprintf then close Nguyễn Thái Ngọc Duy
@ 2014-09-28  1:22       ` Nguyễn Thái Ngọc Duy
  2014-09-28  1:22       ` [PATCH v3 22/32] checkout: support checking out into a new working directory Nguyễn Thái Ngọc Duy
                         ` (11 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-28  1:22 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

This fixes common problems in these code about error handling,
forgetting to close the file handle after fprintf() fails, or not
printing out the error string..

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 builtin/branch.c  |  4 +---
 builtin/init-db.c |  7 +------
 daemon.c          | 11 +----------
 submodule.c       |  9 ++-------
 transport.c       |  8 +++-----
 5 files changed, 8 insertions(+), 31 deletions(-)

diff --git a/builtin/branch.c b/builtin/branch.c
index 9e4666f..a8b79bf 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -761,7 +761,6 @@ static const char edit_description[] = "BRANCH_DESCRIPTION";
 
 static int edit_branch_description(const char *branch_name)
 {
-	FILE *fp;
 	int status;
 	struct strbuf buf = STRBUF_INIT;
 	struct strbuf name = STRBUF_INIT;
@@ -774,8 +773,7 @@ static int edit_branch_description(const char *branch_name)
 		    "  %s\n"
 		    "Lines starting with '%c' will be stripped.\n",
 		    branch_name, comment_line_char);
-	fp = fopen(git_path(edit_description), "w");
-	if ((fwrite(buf.buf, 1, buf.len, fp) < buf.len) || fclose(fp)) {
+	if (write_file(git_path(edit_description), 0, "%s", buf.buf)) {
 		strbuf_release(&buf);
 		return error(_("could not write branch description template: %s"),
 			     strerror(errno));
diff --git a/builtin/init-db.c b/builtin/init-db.c
index 587a505..6b7fa5f 100644
--- a/builtin/init-db.c
+++ b/builtin/init-db.c
@@ -342,7 +342,6 @@ int set_git_dir_init(const char *git_dir, const char *real_git_dir,
 static void separate_git_dir(const char *git_dir)
 {
 	struct stat st;
-	FILE *fp;
 
 	if (!stat(git_link, &st)) {
 		const char *src;
@@ -358,11 +357,7 @@ static void separate_git_dir(const char *git_dir)
 			die_errno(_("unable to move %s to %s"), src, git_dir);
 	}
 
-	fp = fopen(git_link, "w");
-	if (!fp)
-		die(_("Could not create git link %s"), git_link);
-	fprintf(fp, "gitdir: %s\n", git_dir);
-	fclose(fp);
+	write_file(git_link, 1, "gitdir: %s\n", git_dir);
 }
 
 int init_db(const char *template_dir, unsigned int flags)
diff --git a/daemon.c b/daemon.c
index 4dcfff9..8420fdb 100644
--- a/daemon.c
+++ b/daemon.c
@@ -1073,15 +1073,6 @@ static struct credentials *prepare_credentials(const char *user_name,
 }
 #endif
 
-static void store_pid(const char *path)
-{
-	FILE *f = fopen(path, "w");
-	if (!f)
-		die_errno("cannot open pid file '%s'", path);
-	if (fprintf(f, "%"PRIuMAX"\n", (uintmax_t) getpid()) < 0 || fclose(f) != 0)
-		die_errno("failed to write pid file '%s'", path);
-}
-
 static int serve(struct string_list *listen_addr, int listen_port,
     struct credentials *cred)
 {
@@ -1292,7 +1283,7 @@ int main(int argc, char **argv)
 		sanitize_stdfds();
 
 	if (pid_file)
-		store_pid(pid_file);
+		write_file(pid_file, 1, "%"PRIuMAX"\n", (uintmax_t) getpid());
 
 	/* prepare argv for serving-processes */
 	cld_argv = xmalloc(sizeof (char *) * (argc + 2));
diff --git a/submodule.c b/submodule.c
index 0690dc5..34094f5 100644
--- a/submodule.c
+++ b/submodule.c
@@ -1102,16 +1102,11 @@ void connect_work_tree_and_git_dir(const char *work_tree, const char *git_dir)
 	struct strbuf file_name = STRBUF_INIT;
 	struct strbuf rel_path = STRBUF_INIT;
 	const char *real_work_tree = xstrdup(real_path(work_tree));
-	FILE *fp;
 
 	/* Update gitfile */
 	strbuf_addf(&file_name, "%s/.git", work_tree);
-	fp = fopen(file_name.buf, "w");
-	if (!fp)
-		die(_("Could not create git link %s"), file_name.buf);
-	fprintf(fp, "gitdir: %s\n", relative_path(git_dir, real_work_tree,
-						  &rel_path));
-	fclose(fp);
+	write_file(file_name.buf, 1, "gitdir: %s\n",
+		   relative_path(git_dir, real_work_tree, &rel_path));
 
 	/* Update core.worktree setting */
 	strbuf_reset(&file_name);
diff --git a/transport.c b/transport.c
index 7388bb8..0bb15ee 100644
--- a/transport.c
+++ b/transport.c
@@ -282,7 +282,6 @@ static int write_one_ref(const char *name, const unsigned char *sha1,
 {
 	struct strbuf *buf = data;
 	int len = buf->len;
-	FILE *f;
 
 	/* when called via for_each_ref(), flags is non-zero */
 	if (flags && !starts_with(name, "refs/heads/") &&
@@ -291,10 +290,9 @@ static int write_one_ref(const char *name, const unsigned char *sha1,
 
 	strbuf_addstr(buf, name);
 	if (safe_create_leading_directories(buf->buf) ||
-			!(f = fopen(buf->buf, "w")) ||
-			fprintf(f, "%s\n", sha1_to_hex(sha1)) < 0 ||
-			fclose(f))
-		return error("problems writing temporary file %s", buf->buf);
+	    write_file(buf->buf, 0, "%s\n", sha1_to_hex(sha1)))
+		return error("problems writing temporary file %s: %s",
+			     buf->buf, strerror(errno));
 	strbuf_setlen(buf, len);
 	return 0;
 }
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 22/32] checkout: support checking out into a new working directory
  2014-09-28  1:22     ` [PATCH v3 " Nguyễn Thái Ngọc Duy
                         ` (20 preceding siblings ...)
  2014-09-28  1:22       ` [PATCH v3 21/32] use new wrapper write_file() for simple file writing Nguyễn Thái Ngọc Duy
@ 2014-09-28  1:22       ` Nguyễn Thái Ngọc Duy
  2014-09-28  1:22       ` [PATCH v3 23/32] prune: strategies for linked checkouts Nguyễn Thái Ngọc Duy
                         ` (10 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-28  1:22 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

"git checkout --to" sets up a new working directory with a .git file
pointing to $GIT_DIR/worktrees/<id>. It then executes "git checkout"
again on the new worktree with the same arguments except "--to" is
taken out. The second checkout execution, which is not contaminated
with any info from the current repository, will actually check out and
everything that normal "git checkout" does.

Helped-by: Marc Branchaud <marcnarc@xiplink.com>
Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
---
 Documentation/git-checkout.txt         | 46 ++++++++++++++++
 Documentation/git.txt                  |  3 +-
 Documentation/gitrepository-layout.txt |  7 +++
 builtin/checkout.c                     | 95 +++++++++++++++++++++++++++++++++-
 path.c                                 |  2 +-
 t/t2025-checkout-to.sh (new +x)        | 63 ++++++++++++++++++++++
 6 files changed, 212 insertions(+), 4 deletions(-)
 create mode 100755 t/t2025-checkout-to.sh

diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt
index 33ad2ad..c101575 100644
--- a/Documentation/git-checkout.txt
+++ b/Documentation/git-checkout.txt
@@ -225,6 +225,13 @@ This means that you can use `git checkout -p` to selectively discard
 edits from your current working tree. See the ``Interactive Mode''
 section of linkgit:git-add[1] to learn how to operate the `--patch` mode.
 
+--to=<path>::
+	Check out a branch in a separate working directory at
+	`<path>`. A new working directory is linked to the current
+	repository, sharing everything except working directory
+	specific files such as HEAD, index... See "MULTIPLE WORKING
+	TREES" section for more information.
+
 <branch>::
 	Branch to checkout; if it refers to a branch (i.e., a name that,
 	when prepended with "refs/heads/", is a valid ref), then that
@@ -388,6 +395,45 @@ $ git reflog -2 HEAD # or
 $ git log -g -2 HEAD
 ------------
 
+MULTIPLE WORKING TREES
+----------------------
+
+A git repository can support multiple working trees, allowing you to check
+out more than one branch at a time.  With `git checkout --to` a new working
+tree is associated with the repository.  This new working tree is called a
+"linked working tree" as opposed to the "main working tree" prepared by "git
+init" or "git clone".  A repository has one main working tree (if it's not a
+bare repository) and zero or more linked working trees.
+
+Each linked working tree has a private sub-directory in the repository's
+$GIT_DIR/worktrees directory.  The private sub-directory's name is usually
+the base name of the linked working tree's path, possibly appended with a
+number to make it unique.  For example, when `$GIT_DIR=/path/main/.git` the
+command `git checkout --to /path/other/test-next next` creates the linked
+working tree in `/path/other/test-next` and also creates a
+`$GIT_DIR/worktrees/test-next` directory (or `$GIT_DIR/worktrees/test-next1`
+if `test-next` is already taken).
+
+Within a linked working tree, $GIT_DIR is set to point to this private
+directory (e.g. `/path/main/.git/worktrees/test-next` in the example) and
+$GIT_COMMON_DIR is set to point back to the main working tree's $GIT_DIR
+(e.g. `/path/main/.git`). These settings are made in a `.git` file located at
+the top directory of the linked working tree.
+
+Path resolution via `git rev-parse --git-path` uses either
+$GIT_DIR or $GIT_COMMON_DIR depending on the path. For example, in the
+linked working tree `git rev-parse --git-path HEAD` returns
+`/path/main/.git/worktrees/test-next/HEAD` (not
+`/path/other/test-next/.git/HEAD` or `/path/main/.git/HEAD`) while `git
+rev-parse --git-path refs/heads/master` uses
+$GIT_COMMON_DIR and returns `/path/main/.git/refs/heads/master`,
+since refs are shared across all working trees.
+
+See linkgit:gitrepository-layout[5] for more information. The rule of
+thumb is do not make any assumption about whether a path belongs to
+$GIT_DIR or $GIT_COMMON_DIR when you need to directly access something
+inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.
+
 EXAMPLES
 --------
 
diff --git a/Documentation/git.txt b/Documentation/git.txt
index 176d37c..54c9fb6 100644
--- a/Documentation/git.txt
+++ b/Documentation/git.txt
@@ -806,7 +806,8 @@ Git so take care if using Cogito etc.
 	If this variable is set to a path, non-worktree files that are
 	normally in $GIT_DIR will be taken from this path
 	instead. Worktree-specific files such as HEAD or index are
-	taken from $GIT_DIR. See linkgit:gitrepository-layout[5] for
+	taken from $GIT_DIR. See linkgit:gitrepository-layout[5] and
+	the section 'MULTIPLE CHECKOUT MODE' in linkgit:checkout[1]
 	details. This variable has lower precedence than other path
 	variables such as GIT_INDEX_FILE, GIT_OBJECT_DIRECTORY...
 
diff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt
index 2dc5667..8228450 100644
--- a/Documentation/gitrepository-layout.txt
+++ b/Documentation/gitrepository-layout.txt
@@ -252,6 +252,13 @@ modules::
 	directory is ignored if $GIT_COMMON_DIR is set and
 	"$GIT_COMMON_DIR/modules" will be used instead.
 
+worktrees::
+	Contains worktree specific information of linked
+	checkouts. Each subdirectory contains the worktree-related
+	part of a linked checkout. This directory is ignored if
+	$GIT_COMMON_DIR is set and "$GIT_COMMON_DIR/worktrees" will be
+	used instead.
+
 SEE ALSO
 --------
 linkgit:git-init[1],
diff --git a/builtin/checkout.c b/builtin/checkout.c
index 220f80e..ad10f99 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -48,6 +48,10 @@ struct checkout_opts {
 	const char *prefix;
 	struct pathspec pathspec;
 	struct tree *source_tree;
+
+	const char *new_worktree;
+	const char **saved_argv;
+	int new_worktree_mode;
 };
 
 static int post_checkout_hook(struct commit *old, struct commit *new,
@@ -249,6 +253,9 @@ static int checkout_paths(const struct checkout_opts *opts,
 		die(_("Cannot update paths and switch to branch '%s' at the same time."),
 		    opts->new_branch);
 
+	if (opts->new_worktree)
+		die(_("'%s' cannot be used with updating paths"), "--to");
+
 	if (opts->patch_mode)
 		return run_add_interactive(revision, "--patch=checkout",
 					   &opts->pathspec);
@@ -484,7 +491,7 @@ static int merge_working_tree(const struct checkout_opts *opts,
 			topts.dir->flags |= DIR_SHOW_IGNORED;
 			setup_standard_excludes(topts.dir);
 		}
-		tree = parse_tree_indirect(old->commit ?
+		tree = parse_tree_indirect(old->commit && !opts->new_worktree_mode ?
 					   old->commit->object.sha1 :
 					   EMPTY_TREE_SHA1_BIN);
 		init_tree_desc(&trees[0], tree->buffer, tree->size);
@@ -800,7 +807,8 @@ static int switch_branches(const struct checkout_opts *opts,
 		return ret;
 	}
 
-	if (!opts->quiet && !old.path && old.commit && new->commit != old.commit)
+	if (!opts->quiet && !old.path && old.commit &&
+	    new->commit != old.commit && !opts->new_worktree_mode)
 		orphaned_commit_warning(old.commit, new->commit);
 
 	update_refs_for_switch(opts, &old, new);
@@ -810,6 +818,76 @@ static int switch_branches(const struct checkout_opts *opts,
 	return ret || writeout_error;
 }
 
+static int prepare_linked_checkout(const struct checkout_opts *opts,
+				   struct branch_info *new)
+{
+	struct strbuf sb_git = STRBUF_INIT, sb_repo = STRBUF_INIT;
+	struct strbuf sb = STRBUF_INIT;
+	const char *path = opts->new_worktree, *name;
+	struct stat st;
+	struct child_process cp;
+	int counter = 0, len;
+
+	if (!new->commit)
+		die(_("no branch specified"));
+	if (file_exists(path))
+		die(_("'%s' already exists"), path);
+
+	len = strlen(path);
+	while (len && is_dir_sep(path[len - 1]))
+		len--;
+
+	for (name = path + len - 1; name > path; name--)
+		if (is_dir_sep(*name)) {
+			name++;
+			break;
+		}
+	strbuf_addstr(&sb_repo,
+		      git_path("worktrees/%.*s", (int)(path + len - name), name));
+	len = sb_repo.len;
+	if (safe_create_leading_directories_const(sb_repo.buf))
+		die_errno(_("could not create leading directories of '%s'"),
+			  sb_repo.buf);
+	while (!stat(sb_repo.buf, &st)) {
+		counter++;
+		strbuf_setlen(&sb_repo, len);
+		strbuf_addf(&sb_repo, "%d", counter);
+	}
+	name = strrchr(sb_repo.buf, '/') + 1;
+	if (mkdir(sb_repo.buf, 0777))
+		die_errno(_("could not create directory of '%s'"), sb_repo.buf);
+
+	strbuf_addf(&sb_git, "%s/.git", path);
+	if (safe_create_leading_directories_const(sb_git.buf))
+		die_errno(_("could not create leading directories of '%s'"),
+			  sb_git.buf);
+
+	write_file(sb_git.buf, 1, "gitdir: %s/worktrees/%s\n",
+		   real_path(get_git_common_dir()), name);
+	/*
+	 * This is to keep resolve_ref() happy. We need a valid HEAD
+	 * or is_git_directory() will reject the directory. Any valid
+	 * value would do because this value will be ignored and
+	 * replaced at the next (real) checkout.
+	 */
+	strbuf_addf(&sb, "%s/HEAD", sb_repo.buf);
+	write_file(sb.buf, 1, "%s\n", sha1_to_hex(new->commit->object.sha1));
+	strbuf_reset(&sb);
+	strbuf_addf(&sb, "%s/commondir", sb_repo.buf);
+	write_file(sb.buf, 1, "../..\n");
+
+	if (!opts->quiet)
+		fprintf_ln(stderr, _("Enter %s (identifier %s)"), path, name);
+
+	setenv("GIT_CHECKOUT_NEW_WORKTREE", "1", 1);
+	setenv(GIT_DIR_ENVIRONMENT, sb_git.buf, 1);
+	setenv(GIT_WORK_TREE_ENVIRONMENT, path, 1);
+	memset(&cp, 0, sizeof(cp));
+	cp.git_cmd = 1;
+	cp.argv = opts->saved_argv;
+	return run_command(&cp);
+}
+
 static int git_checkout_config(const char *var, const char *value, void *cb)
 {
 	if (!strcmp(var, "diff.ignoresubmodules")) {
@@ -1071,6 +1149,9 @@ static int checkout_branch(struct checkout_opts *opts,
 		die(_("Cannot switch branch to a non-commit '%s'"),
 		    new->name);
 
+	if (opts->new_worktree)
+		return prepare_linked_checkout(opts, new);
+
 	if (!new->commit && opts->new_branch) {
 		unsigned char rev[20];
 		int flag;
@@ -1113,6 +1194,8 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
 			 N_("do not limit pathspecs to sparse entries only")),
 		OPT_HIDDEN_BOOL(0, "guess", &dwim_new_local_branch,
 				N_("second guess 'git checkout no-such-branch'")),
+		OPT_FILENAME(0, "to", &opts.new_worktree,
+			   N_("check a branch out in a separate working directory")),
 		OPT_END(),
 	};
 
@@ -1121,6 +1204,9 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
 	opts.overwrite_ignore = 1;
 	opts.prefix = prefix;
 
+	opts.saved_argv = xmalloc(sizeof(const char *) * (argc + 2));
+	memcpy(opts.saved_argv, argv, sizeof(const char *) * (argc + 1));
+
 	gitmodules_config();
 	git_config(git_checkout_config, &opts);
 
@@ -1129,6 +1215,11 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
 	argc = parse_options(argc, argv, prefix, options, checkout_usage,
 			     PARSE_OPT_KEEP_DASHDASH);
 
+	/* recursive execution from checkout_new_worktree() */
+	opts.new_worktree_mode = getenv("GIT_CHECKOUT_NEW_WORKTREE") != NULL;
+	if (opts.new_worktree_mode)
+		opts.new_worktree = NULL;
+
 	if (conflict_style) {
 		opts.merge = 1; /* implied */
 		git_xmerge_config("merge.conflictstyle", conflict_style, NULL);
diff --git a/path.c b/path.c
index 94db501..72eca6d 100644
--- a/path.c
+++ b/path.c
@@ -92,7 +92,7 @@ static void replace_dir(struct strbuf *buf, int len, const char *newdir)
 
 static const char *common_list[] = {
 	"/branches", "/hooks", "/info", "/logs", "/lost-found", "/modules",
-	"/objects", "/refs", "/remotes", "/rr-cache", "/svn",
+	"/objects", "/refs", "/remotes", "/worktrees", "/rr-cache", "/svn",
 	"config", "gc.pid", "packed-refs", "shallow",
 	NULL
 };
diff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh
new file mode 100755
index 0000000..4963415
--- /dev/null
+++ b/t/t2025-checkout-to.sh
@@ -0,0 +1,63 @@
+#!/bin/sh
+
+test_description='test git checkout --to'
+
+. ./test-lib.sh
+
+test_expect_success 'setup' '
+	test_commit init
+'
+
+test_expect_success 'checkout --to not updating paths' '
+	test_must_fail git checkout --to -- init.t
+'
+
+test_expect_success 'checkout --to an existing worktree' '
+	mkdir existing &&
+	test_must_fail git checkout --detach --to existing master
+'
+
+test_expect_success 'checkout --to a new worktree' '
+	git checkout --to here master &&
+	(
+		cd here &&
+		test_cmp ../init.t init.t &&
+		git symbolic-ref HEAD >actual &&
+		echo refs/heads/master >expect &&
+		test_cmp expect actual &&
+		git fsck
+	)
+'
+
+test_expect_success 'checkout --to a new worktree from a subdir' '
+	(
+		mkdir sub &&
+		cd sub &&
+		git checkout --detach --to here master &&
+		cd here &&
+		test_cmp ../../init.t init.t
+	)
+'
+
+test_expect_success 'checkout --to from a linked checkout' '
+	(
+		cd here &&
+		git checkout --to nested-here master &&
+		cd nested-here &&
+		git fsck
+	)
+'
+
+test_expect_success 'checkout --to a new worktree creating new branch' '
+	git checkout --to there -b newmaster master &&
+	(
+		cd there &&
+		test_cmp ../init.t init.t &&
+		git symbolic-ref HEAD >actual &&
+		echo refs/heads/newmaster >expect &&
+		test_cmp expect actual &&
+		git fsck
+	)
+'
+
+test_done
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 23/32] prune: strategies for linked checkouts
  2014-09-28  1:22     ` [PATCH v3 " Nguyễn Thái Ngọc Duy
                         ` (21 preceding siblings ...)
  2014-09-28  1:22       ` [PATCH v3 22/32] checkout: support checking out into a new working directory Nguyễn Thái Ngọc Duy
@ 2014-09-28  1:22       ` Nguyễn Thái Ngọc Duy
  2014-10-01 16:20         ` Johannes Sixt
  2014-09-28  1:22       ` [PATCH v3 24/32] checkout: reject if the branch is already checked out elsewhere Nguyễn Thái Ngọc Duy
                         ` (9 subsequent siblings)
  32 siblings, 1 reply; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-28  1:22 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy,
	Marc Branchaud

(alias R=$GIT_COMMON_DIR/worktrees/<id>)

 - linked checkouts are supposed to keep its location in $R/gitdir up
   to date. The use case is auto fixup after a manual checkout move.

 - linked checkouts are supposed to update mtime of $R/gitdir. If
   $R/gitdir's mtime is older than a limit, and it points to nowhere,
   worktrees/<id> is to be pruned.

 - If $R/locked exists, worktrees/<id> is not supposed to be pruned. If
   $R/locked exists and $R/gitdir's mtime is older than a really long
   limit, warn about old unused repo.

 - "git checkout --to" is supposed to make a hard link named $R/link
   pointing to the .git file on supported file systems to help detect
   the user manually deleting the checkout. If $R/link exists and its
   link count is greated than 1, the repo is kept.

Helped-by: Marc Branchaud <marcnarc@xiplink.com>
Helped-by: Eric Sunshine <sunshine@sunshineco.com>
Signed-off-by: Marc Branchaud <marcnarc@xiplink.com>
Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
---
 Documentation/git-checkout.txt             | 20 +++++++
 Documentation/git-prune.txt                |  3 +
 Documentation/gitrepository-layout.txt     | 19 ++++++
 builtin/checkout.c                         | 19 +++++-
 builtin/prune.c                            | 95 ++++++++++++++++++++++++++++++
 setup.c                                    | 13 ++++
 t/t2026-prune-linked-checkouts.sh (new +x) | 84 ++++++++++++++++++++++++++
 7 files changed, 251 insertions(+), 2 deletions(-)
 create mode 100755 t/t2026-prune-linked-checkouts.sh

diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt
index c101575..35675da 100644
--- a/Documentation/git-checkout.txt
+++ b/Documentation/git-checkout.txt
@@ -434,6 +434,26 @@ thumb is do not make any assumption about whether a path belongs to
 $GIT_DIR or $GIT_COMMON_DIR when you need to directly access something
 inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.
 
+When you are done with a linked working tree you can simply delete it.
+You can clean up any stale $GIT_DIR/worktrees entries via `git prune
+--worktrees` in the main or any linked working tree.
+
+If you move a linked working directory to another file system, or
+within a file system that does not support hard links, you need to run
+at least one git command inside the linked working directory
+(e.g. `git status`) in order to update its entry in $GIT_DIR/worktrees
+so that it does not get automatically removed.
+
+To prevent `git prune --worktrees` from deleting a $GIT_DIR/worktrees
+entry (which can be useful in some situations, such as when the
+entry's working tree is stored on a portable device), add a file named
+'locked' to the entry's directory. The file contains the reason in
+plain text. For example, if a linked working tree's `.git` file points
+to `/path/main/.git/worktrees/test-next` then a file named
+`/path/main/.git/worktrees/test-next/locked` will prevent the
+`test-next` entry from being pruned.  See
+linkgit:gitrepository-layout[5] for details.
+
 EXAMPLES
 --------
 
diff --git a/Documentation/git-prune.txt b/Documentation/git-prune.txt
index 7a493c8..1cf3bed 100644
--- a/Documentation/git-prune.txt
+++ b/Documentation/git-prune.txt
@@ -48,6 +48,9 @@ OPTIONS
 --expire <time>::
 	Only expire loose objects older than <time>.
 
+--worktrees::
+	Prune dead working tree information in $GIT_DIR/worktrees.
+
 <head>...::
 	In addition to objects
 	reachable from any of our references, keep objects
diff --git a/Documentation/gitrepository-layout.txt b/Documentation/gitrepository-layout.txt
index 8228450..2b30a92 100644
--- a/Documentation/gitrepository-layout.txt
+++ b/Documentation/gitrepository-layout.txt
@@ -259,6 +259,25 @@ worktrees::
 	$GIT_COMMON_DIR is set and "$GIT_COMMON_DIR/worktrees" will be
 	used instead.
 
+worktrees/<id>/gitdir::
+	A text file containing the absolute path back to the .git file
+	that points to here. This is used to check if the linked
+	repository has been manually removed and there is no need to
+	keep this directory any more. mtime of this file should be
+	updated every time the linked repository is accessed.
+
+worktrees/<id>/locked::
+	If this file exists, the linked repository may be on a
+	portable device and not available. It does not mean that the
+	linked repository is gone and `worktrees/<id>` could be
+	removed. The file's content contains a reason string on why
+	the repository is locked.
+
+worktrees/<id>/link::
+	If this file exists, it is a hard link to the linked .git
+	file. It is used to detect if the linked repository is
+	manually removed.
+
 SEE ALSO
 --------
 linkgit:git-init[1],
diff --git a/builtin/checkout.c b/builtin/checkout.c
index ad10f99..ab46af9 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -826,7 +826,7 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
 	const char *path = opts->new_worktree, *name;
 	struct stat st;
 	struct child_process cp;
-	int counter = 0, len;
+	int counter = 0, len, ret;
 
 	if (!new->commit)
 		die(_("no branch specified"));
@@ -857,11 +857,21 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
 	if (mkdir(sb_repo.buf, 0777))
 		die_errno(_("could not create directory of '%s'"), sb_repo.buf);
 
+	/*
+	 * lock the incomplete repo so prune won't delete it, unlock
+	 * after the preparation is over.
+	 */
+	strbuf_addf(&sb, "%s/locked", sb_repo.buf);
+	write_file(sb.buf, 1, "initializing\n");
+
 	strbuf_addf(&sb_git, "%s/.git", path);
 	if (safe_create_leading_directories_const(sb_git.buf))
 		die_errno(_("could not create leading directories of '%s'"),
 			  sb_git.buf);
 
+	strbuf_reset(&sb);
+	strbuf_addf(&sb, "%s/gitdir", sb_repo.buf);
+	write_file(sb.buf, 1, "%s\n", real_path(sb_git.buf));
 	write_file(sb_git.buf, 1, "gitdir: %s/worktrees/%s\n",
 		   real_path(get_git_common_dir()), name);
 	/*
@@ -870,6 +880,7 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
 	 * value would do because this value will be ignored and
 	 * replaced at the next (real) checkout.
 	 */
+	strbuf_reset(&sb);
 	strbuf_addf(&sb, "%s/HEAD", sb_repo.buf);
 	write_file(sb.buf, 1, "%s\n", sha1_to_hex(new->commit->object.sha1));
 	strbuf_reset(&sb);
@@ -885,7 +896,11 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
 	memset(&cp, 0, sizeof(cp));
 	cp.git_cmd = 1;
 	cp.argv = opts->saved_argv;
-	return run_command(&cp);
+	ret = run_command(&cp);
+	strbuf_reset(&sb);
+	strbuf_addf(&sb, "%s/locked", sb_repo.buf);
+	unlink_or_warn(sb.buf);
+	return ret;
 }
 
 static int git_checkout_config(const char *var, const char *value, void *cb)
diff --git a/builtin/prune.c b/builtin/prune.c
index 144a3bd..cf56110 100644
--- a/builtin/prune.c
+++ b/builtin/prune.c
@@ -112,6 +112,91 @@ static void prune_object_dir(const char *path)
 	}
 }
 
+static int prune_worktree(const char *id, struct strbuf *reason)
+{
+	struct stat st;
+	char *path;
+	int fd, len;
+
+	if (!is_directory(git_path("worktrees/%s", id))) {
+		strbuf_addf(reason, _("Removing worktrees/%s: not a valid directory"), id);
+		return 1;
+	}
+	if (file_exists(git_path("worktrees/%s/locked", id)))
+		return 0;
+	if (stat(git_path("worktrees/%s/gitdir", id), &st)) {
+		strbuf_addf(reason, _("Removing worktrees/%s: gitdir file does not exist"), id);
+		return 1;
+	}
+	fd = open(git_path("worktrees/%s/gitdir", id), O_RDONLY);
+	if (fd < 0) {
+		strbuf_addf(reason, _("Removing worktrees/%s: unable to read gitdir file (%s)"),
+			    id, strerror(errno));
+		return 1;
+	}
+	len = st.st_size;
+	path = xmalloc(len + 1);
+	read_in_full(fd, path, len);
+	close(fd);
+	while (len && (path[len - 1] == '\n' || path[len - 1] == '\r'))
+		len--;
+	if (!len) {
+		strbuf_addf(reason, _("Removing worktrees/%s: invalid gitdir file"), id);
+		free(path);
+		return 1;
+	}
+	path[len] = '\0';
+	if (!file_exists(path)) {
+		struct stat st_link;
+		free(path);
+		/*
+		 * the repo is moved manually and has not been
+		 * accessed since?
+		 */
+		if (!stat(git_path("worktrees/%s/link", id), &st_link) &&
+		    st_link.st_nlink > 1)
+			return 0;
+		strbuf_addf(reason, _("Removing worktrees/%s: gitdir file points to non-existent location"), id);
+		return 1;
+	}
+	free(path);
+	return st.st_mtime <= expire;
+}
+
+static void prune_worktrees(void)
+{
+	struct strbuf reason = STRBUF_INIT;
+	struct strbuf path = STRBUF_INIT;
+	DIR *dir = opendir(git_path("worktrees"));
+	struct dirent *d;
+	int ret;
+	if (!dir)
+		return;
+	while ((d = readdir(dir)) != NULL) {
+		if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, ".."))
+			continue;
+		strbuf_reset(&reason);
+		if (!prune_worktree(d->d_name, &reason))
+			continue;
+		if (show_only || verbose)
+			printf("%s\n", reason.buf);
+		if (show_only)
+			continue;
+		strbuf_reset(&path);
+		strbuf_addstr(&path, git_path("worktrees/%s", d->d_name));
+		ret = remove_dir_recursively(&path, 0);
+		if (ret < 0 && errno == ENOTDIR)
+			ret = unlink(path.buf);
+		if (ret)
+			error(_("failed to remove: %s"), strerror(errno));
+	}
+	closedir(dir);
+	if (!show_only)
+		rmdir(git_path("worktrees"));
+	strbuf_release(&reason);
+	strbuf_release(&path);
+}
+
 /*
  * Write errors (particularly out of space) can result in
  * failed temporary packs (and more rarely indexes and other
@@ -138,10 +223,12 @@ int cmd_prune(int argc, const char **argv, const char *prefix)
 {
 	struct rev_info revs;
 	struct progress *progress = NULL;
+	int do_prune_worktrees = 0;
 	const struct option options[] = {
 		OPT__DRY_RUN(&show_only, N_("do not remove, show only")),
 		OPT__VERBOSE(&verbose, N_("report pruned objects")),
 		OPT_BOOL(0, "progress", &show_progress, N_("show progress")),
+		OPT_BOOL(0, "worktrees", &do_prune_worktrees, N_("prune .git/worktrees")),
 		OPT_EXPIRY_DATE(0, "expire", &expire,
 				N_("expire objects older than <time>")),
 		OPT_END()
@@ -154,6 +241,14 @@ int cmd_prune(int argc, const char **argv, const char *prefix)
 	init_revisions(&revs, prefix);
 
 	argc = parse_options(argc, argv, prefix, options, prune_usage, 0);
+
+	if (do_prune_worktrees) {
+		if (argc)
+			die(_("--worktrees does not take extra arguments"));
+		prune_worktrees();
+		return 0;
+	}
+
 	while (argc--) {
 		unsigned char sha1[20];
 		const char *name = *argv++;
diff --git a/setup.c b/setup.c
index b99bdd3..fb61860 100644
--- a/setup.c
+++ b/setup.c
@@ -390,6 +390,17 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)
 	return ret;
 }
 
+static void update_linked_gitdir(const char *gitfile, const char *gitdir)
+{
+	struct strbuf path = STRBUF_INIT;
+	struct stat st;
+
+	strbuf_addf(&path, "%s/gitfile", gitdir);
+	if (stat(path.buf, &st) || st.st_mtime + 24 * 3600 < time(NULL))
+		write_file(path.buf, 0, "%s\n", gitfile);
+	strbuf_release(&path);
+}
+
 /*
  * Try to read the location of the git directory from the .git file,
  * return path to git directory if found.
@@ -438,6 +449,8 @@ const char *read_gitfile(const char *path)
 
 	if (!is_git_directory(dir))
 		die("Not a git repository: %s", dir);
+
+	update_linked_gitdir(path, dir);
 	path = real_path(dir);
 
 	free(buf);
diff --git a/t/t2026-prune-linked-checkouts.sh b/t/t2026-prune-linked-checkouts.sh
new file mode 100755
index 0000000..3622800
--- /dev/null
+++ b/t/t2026-prune-linked-checkouts.sh
@@ -0,0 +1,84 @@
+#!/bin/sh
+
+test_description='prune $GIT_DIR/worktrees'
+
+. ./test-lib.sh
+
+test_expect_success 'prune --worktrees on normal repo' '
+	git prune --worktrees &&
+	test_must_fail git prune --worktrees abc
+'
+
+test_expect_success 'prune files inside $GIT_DIR/worktrees' '
+	mkdir .git/worktrees &&
+	: >.git/worktrees/abc &&
+	git prune --worktrees --verbose >actual &&
+	cat >expect <<EOF &&
+Removing worktrees/abc: not a valid directory
+EOF
+	test_i18ncmp expect actual &&
+	! test -f .git/worktrees/abc &&
+	! test -d .git/worktrees
+'
+
+test_expect_success 'prune directories without gitdir' '
+	mkdir -p .git/worktrees/def/abc &&
+	: >.git/worktrees/def/def &&
+	cat >expect <<EOF &&
+Removing worktrees/def: gitdir file does not exist
+EOF
+	git prune --worktrees --verbose >actual &&
+	test_i18ncmp expect actual &&
+	! test -d .git/worktrees/def &&
+	! test -d .git/worktrees
+'
+
+test_expect_success POSIXPERM 'prune directories with unreadable gitdir' '
+	mkdir -p .git/worktrees/def/abc &&
+	: >.git/worktrees/def/def &&
+	: >.git/worktrees/def/gitdir &&
+	chmod u-r .git/worktrees/def/gitdir &&
+	git prune --worktrees --verbose >actual &&
+	test_i18ngrep "Removing worktrees/def: unable to read gitdir file" actual &&
+	! test -d .git/worktrees/def &&
+	! test -d .git/worktrees
+'
+
+test_expect_success 'prune directories with invalid gitdir' '
+	mkdir -p .git/worktrees/def/abc &&
+	: >.git/worktrees/def/def &&
+	: >.git/worktrees/def/gitdir &&
+	git prune --worktrees --verbose >actual &&
+	test_i18ngrep "Removing worktrees/def: invalid gitdir file" actual &&
+	! test -d .git/worktrees/def &&
+	! test -d .git/worktrees
+'
+
+test_expect_success 'prune directories with gitdir pointing to nowhere' '
+	mkdir -p .git/worktrees/def/abc &&
+	: >.git/worktrees/def/def &&
+	echo "$TRASH_DIRECTORY"/nowhere >.git/worktrees/def/gitdir &&
+	git prune --worktrees --verbose >actual &&
+	test_i18ngrep "Removing worktrees/def: gitdir file points to non-existent location" actual &&
+	! test -d .git/worktrees/def &&
+	! test -d .git/worktrees
+'
+
+test_expect_success 'not prune locked checkout' '
+	test_when_finished rm -r .git/worktrees
+	mkdir -p .git/worktrees/ghi &&
+	: >.git/worktrees/ghi/locked &&
+	git prune --worktrees &&
+	test -d .git/worktrees/ghi
+'
+
+test_expect_success 'not prune recent checkouts' '
+	test_when_finished rm -r .git/worktrees
+	mkdir zz &&
+	mkdir -p .git/worktrees/jlm &&
+	echo "$TRASH_DIRECTORY"/zz >.git/worktrees/jlm/gitdir &&
+	git prune --worktrees --verbose --expire=2.days.ago &&
+	test -d .git/worktrees/jlm
+'
+
+test_done
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 24/32] checkout: reject if the branch is already checked out elsewhere
  2014-09-28  1:22     ` [PATCH v3 " Nguyễn Thái Ngọc Duy
                         ` (22 preceding siblings ...)
  2014-09-28  1:22       ` [PATCH v3 23/32] prune: strategies for linked checkouts Nguyễn Thái Ngọc Duy
@ 2014-09-28  1:22       ` Nguyễn Thái Ngọc Duy
  2014-09-28  1:22       ` [PATCH v3 25/32] checkout: clean up half-prepared directories in --to mode Nguyễn Thái Ngọc Duy
                         ` (8 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-28  1:22 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

One branch obviously can't be checked out at two places (but detached
heads are ok). Give the user a choice in this case: --detach, -b
new-branch, switch branch in the other checkout first or simply 'cd'
and continue to work there.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
---
 builtin/checkout.c     | 86 ++++++++++++++++++++++++++++++++++++++++++++++++--
 t/t2025-checkout-to.sh | 25 ++++++++++++---
 2 files changed, 104 insertions(+), 7 deletions(-)

diff --git a/builtin/checkout.c b/builtin/checkout.c
index ab46af9..6d68913 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -430,6 +430,11 @@ struct branch_info {
 	const char *name; /* The short name used */
 	const char *path; /* The full name of a real branch */
 	struct commit *commit; /* The named commit */
+	/*
+	 * if not null the branch is detached because it's already
+	 * checked out in this checkout
+	 */
+	char *checkout;
 };
 
 static void setup_branch_path(struct branch_info *branch)
@@ -958,12 +963,78 @@ static const char *unique_tracking_name(const char *name, unsigned char *sha1)
 	return NULL;
 }
 
+static void check_linked_checkout(struct branch_info *new, const char *id)
+{
+	struct strbuf sb = STRBUF_INIT;
+	struct strbuf path = STRBUF_INIT;
+	struct strbuf gitdir = STRBUF_INIT;
+	const char *start, *end;
+
+	if (id)
+		strbuf_addf(&path, "%s/worktrees/%s/HEAD", get_git_common_dir(), id);
+	else
+		strbuf_addf(&path, "%s/HEAD", get_git_common_dir());
+
+	if (strbuf_read_file(&sb, path.buf, 0) < 0 ||
+	    !skip_prefix(sb.buf, "ref:", &start))
+		goto done;
+	while (isspace(*start))
+		start++;
+	end = start;
+	while (*end && !isspace(*end))
+		end++;
+	if (strncmp(start, new->path, end - start) || new->path[end - start] != '\0')
+		goto done;
+	if (id) {
+		strbuf_reset(&path);
+		strbuf_addf(&path, "%s/worktrees/%s/gitdir", get_git_common_dir(), id);
+		if (strbuf_read_file(&gitdir, path.buf, 0) <= 0)
+			goto done;
+		strbuf_rtrim(&gitdir);
+	} else
+		strbuf_addstr(&gitdir, get_git_common_dir());
+	die(_("'%s' is already checked out at '%s'"), new->name, gitdir.buf);
+done:
+	strbuf_release(&path);
+	strbuf_release(&sb);
+	strbuf_release(&gitdir);
+}
+
+static void check_linked_checkouts(struct branch_info *new)
+{
+	struct strbuf path = STRBUF_INIT;
+	DIR *dir;
+	struct dirent *d;
+
+	strbuf_addf(&path, "%s/worktrees", get_git_common_dir());
+	if ((dir = opendir(path.buf)) == NULL) {
+		strbuf_release(&path);
+		return;
+	}
+
+	/*
+	 * $GIT_COMMON_DIR/HEAD is practically outside
+	 * $GIT_DIR so resolve_ref_unsafe() won't work (it
+	 * uses git_path). Parse the ref ourselves.
+	 */
+	check_linked_checkout(new, NULL);
+
+	while ((d = readdir(dir)) != NULL) {
+		if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, ".."))
+			continue;
+		check_linked_checkout(new, d->d_name);
+	}
+	strbuf_release(&path);
+	closedir(dir);
+}
+
 static int parse_branchname_arg(int argc, const char **argv,
 				int dwim_new_local_branch_ok,
 				struct branch_info *new,
 				struct tree **source_tree,
 				unsigned char rev[20],
-				const char **new_branch)
+				const char **new_branch,
+				int force_detach)
 {
 	int argcount = 0;
 	unsigned char branch_rev[20];
@@ -1085,6 +1156,16 @@ static int parse_branchname_arg(int argc, const char **argv,
 	else
 		new->path = NULL; /* not an existing branch */
 
+	if (new->path && !force_detach && !*new_branch) {
+		unsigned char sha1[20];
+		int flag;
+		char *head_ref = resolve_refdup("HEAD", sha1, 0, &flag);
+		if (head_ref &&
+		    (!(flag & REF_ISSYMREF) || strcmp(head_ref, new->path)))
+			check_linked_checkouts(new);
+		free(head_ref);
+	}
+
 	new->commit = lookup_commit_reference_gently(rev, 1);
 	if (!new->commit) {
 		/* not a commit */
@@ -1291,7 +1372,8 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
 			!opts.new_branch;
 		int n = parse_branchname_arg(argc, argv, dwim_ok,
 					     &new, &opts.source_tree,
-					     rev, &opts.new_branch);
+					     rev, &opts.new_branch,
+					     opts.force_detach);
 		argv += n;
 		argc -= n;
 	}
diff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh
index 4963415..edd3404 100755
--- a/t/t2025-checkout-to.sh
+++ b/t/t2025-checkout-to.sh
@@ -18,13 +18,14 @@ test_expect_success 'checkout --to an existing worktree' '
 '
 
 test_expect_success 'checkout --to a new worktree' '
-	git checkout --to here master &&
+	git rev-parse HEAD >expect &&
+	git checkout --detach --to here master &&
 	(
 		cd here &&
 		test_cmp ../init.t init.t &&
-		git symbolic-ref HEAD >actual &&
-		echo refs/heads/master >expect &&
-		test_cmp expect actual &&
+		test_must_fail git symbolic-ref HEAD &&
+		git rev-parse HEAD >actual &&
+		test_cmp ../expect actual &&
 		git fsck
 	)
 '
@@ -42,7 +43,7 @@ test_expect_success 'checkout --to a new worktree from a subdir' '
 test_expect_success 'checkout --to from a linked checkout' '
 	(
 		cd here &&
-		git checkout --to nested-here master &&
+		git checkout --detach --to nested-here master &&
 		cd nested-here &&
 		git fsck
 	)
@@ -60,4 +61,18 @@ test_expect_success 'checkout --to a new worktree creating new branch' '
 	)
 '
 
+test_expect_success 'die the same branch is already checked out' '
+	(
+		cd here &&
+		test_must_fail git checkout newmaster
+	)
+'
+
+test_expect_success 'not die on re-checking out current branch' '
+	(
+		cd there &&
+		git checkout newmaster
+	)
+'
+
 test_done
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 25/32] checkout: clean up half-prepared directories in --to mode
  2014-09-28  1:22     ` [PATCH v3 " Nguyễn Thái Ngọc Duy
                         ` (23 preceding siblings ...)
  2014-09-28  1:22       ` [PATCH v3 24/32] checkout: reject if the branch is already checked out elsewhere Nguyễn Thái Ngọc Duy
@ 2014-09-28  1:22       ` Nguyễn Thái Ngọc Duy
  2014-09-28  1:22       ` [PATCH v3 26/32] gc: style change -- no SP before closing parenthesis Nguyễn Thái Ngọc Duy
                         ` (7 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-28  1:22 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
---
 builtin/checkout.c     | 48 ++++++++++++++++++++++++++++++++++++++++++++++++
 t/t2025-checkout-to.sh |  6 ++++++
 2 files changed, 54 insertions(+)

diff --git a/builtin/checkout.c b/builtin/checkout.c
index 6d68913..bd0e2d5 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -20,6 +20,7 @@
 #include "resolve-undo.h"
 #include "submodule.h"
 #include "argv-array.h"
+#include "sigchain.h"
 
 static const char * const checkout_usage[] = {
 	N_("git checkout [options] <branch>"),
@@ -823,6 +824,35 @@ static int switch_branches(const struct checkout_opts *opts,
 	return ret || writeout_error;
 }
 
+static char *junk_work_tree;
+static char *junk_git_dir;
+static int is_junk;
+static pid_t junk_pid;
+
+static void remove_junk(void)
+{
+	struct strbuf sb = STRBUF_INIT;
+	if (!is_junk || getpid() != junk_pid)
+		return;
+	if (junk_git_dir) {
+		strbuf_addstr(&sb, junk_git_dir);
+		remove_dir_recursively(&sb, 0);
+		strbuf_reset(&sb);
+	}
+	if (junk_work_tree) {
+		strbuf_addstr(&sb, junk_work_tree);
+		remove_dir_recursively(&sb, 0);
+	}
+	strbuf_release(&sb);
+}
+
+static void remove_junk_on_signal(int signo)
+{
+	remove_junk();
+	sigchain_pop(signo);
+	raise(signo);
+}
+
 static int prepare_linked_checkout(const struct checkout_opts *opts,
 				   struct branch_info *new)
 {
@@ -859,8 +889,15 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
 		strbuf_addf(&sb_repo, "%d", counter);
 	}
 	name = strrchr(sb_repo.buf, '/') + 1;
+
+	junk_pid = getpid();
+	atexit(remove_junk);
+	sigchain_push_common(remove_junk_on_signal);
+
 	if (mkdir(sb_repo.buf, 0777))
 		die_errno(_("could not create directory of '%s'"), sb_repo.buf);
+	junk_git_dir = xstrdup(sb_repo.buf);
+	is_junk = 1;
 
 	/*
 	 * lock the incomplete repo so prune won't delete it, unlock
@@ -873,6 +910,7 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
 	if (safe_create_leading_directories_const(sb_git.buf))
 		die_errno(_("could not create leading directories of '%s'"),
 			  sb_git.buf);
+	junk_work_tree = xstrdup(path);
 
 	strbuf_reset(&sb);
 	strbuf_addf(&sb, "%s/gitdir", sb_repo.buf);
@@ -902,9 +940,19 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
 	cp.git_cmd = 1;
 	cp.argv = opts->saved_argv;
 	ret = run_command(&cp);
+	if (!ret) {
+		is_junk = 0;
+		free(junk_work_tree);
+		free(junk_git_dir);
+		junk_work_tree = NULL;
+		junk_git_dir = NULL;
+	}
 	strbuf_reset(&sb);
 	strbuf_addf(&sb, "%s/locked", sb_repo.buf);
 	unlink_or_warn(sb.buf);
+	strbuf_release(&sb);
+	strbuf_release(&sb_repo);
+	strbuf_release(&sb_git);
 	return ret;
 }
 
diff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh
index edd3404..e2db078 100755
--- a/t/t2025-checkout-to.sh
+++ b/t/t2025-checkout-to.sh
@@ -17,6 +17,12 @@ test_expect_success 'checkout --to an existing worktree' '
 	test_must_fail git checkout --detach --to existing master
 '
 
+test_expect_success 'checkout --to refuses to checkout locked branch' '
+	test_must_fail git checkout --to zere master &&
+	! test -d zere &&
+	! test -d .git/worktrees/zere
+'
+
 test_expect_success 'checkout --to a new worktree' '
 	git rev-parse HEAD >expect &&
 	git checkout --detach --to here master &&
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 26/32] gc: style change -- no SP before closing parenthesis
  2014-09-28  1:22     ` [PATCH v3 " Nguyễn Thái Ngọc Duy
                         ` (24 preceding siblings ...)
  2014-09-28  1:22       ` [PATCH v3 25/32] checkout: clean up half-prepared directories in --to mode Nguyễn Thái Ngọc Duy
@ 2014-09-28  1:22       ` Nguyễn Thái Ngọc Duy
  2014-09-28  1:22       ` [PATCH v3 27/32] gc: factor out gc.pruneexpire parsing code Nguyễn Thái Ngọc Duy
                         ` (6 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-28  1:22 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 builtin/gc.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/builtin/gc.c b/builtin/gc.c
index ced1456..b690929 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -287,7 +287,7 @@ int cmd_gc(int argc, const char **argv, const char *prefix)
 	argv_array_pushl(&pack_refs_cmd, "pack-refs", "--all", "--prune", NULL);
 	argv_array_pushl(&reflog, "reflog", "expire", "--all", NULL);
 	argv_array_pushl(&repack, "repack", "-d", "-l", NULL);
-	argv_array_pushl(&prune, "prune", "--expire", NULL );
+	argv_array_pushl(&prune, "prune", "--expire", NULL);
 	argv_array_pushl(&rerere, "rerere", "gc", NULL);
 
 	gc_config();
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 27/32] gc: factor out gc.pruneexpire parsing code
  2014-09-28  1:22     ` [PATCH v3 " Nguyễn Thái Ngọc Duy
                         ` (25 preceding siblings ...)
  2014-09-28  1:22       ` [PATCH v3 26/32] gc: style change -- no SP before closing parenthesis Nguyễn Thái Ngọc Duy
@ 2014-09-28  1:22       ` Nguyễn Thái Ngọc Duy
  2014-09-28  1:22       ` [PATCH v3 28/32] gc: support prune --worktrees Nguyễn Thái Ngọc Duy
                         ` (5 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-28  1:22 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 builtin/gc.c | 22 ++++++++++++----------
 1 file changed, 12 insertions(+), 10 deletions(-)

diff --git a/builtin/gc.c b/builtin/gc.c
index b690929..849a87c 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -55,6 +55,17 @@ static void remove_pidfile_on_signal(int signo)
 	raise(signo);
 }
 
+static void git_config_date_string(const char *key, const char **output)
+{
+	if (git_config_get_string_const(key, output))
+		return;
+	if (strcmp(*output, "now")) {
+		unsigned long now = approxidate("now");
+		if (approxidate(*output) >= now)
+			git_die_config(key, _("Invalid %s: '%s'"), key, *output);
+	}
+}
+
 static void gc_config(void)
 {
 	const char *value;
@@ -71,16 +82,7 @@ static void gc_config(void)
 	git_config_get_int("gc.auto", &gc_auto_threshold);
 	git_config_get_int("gc.autopacklimit", &gc_auto_pack_limit);
 	git_config_get_bool("gc.autodetach", &detach_auto);
-
-	if (!git_config_get_string_const("gc.pruneexpire", &prune_expire)) {
-		if (strcmp(prune_expire, "now")) {
-			unsigned long now = approxidate("now");
-			if (approxidate(prune_expire) >= now) {
-				git_die_config("gc.pruneexpire", _("Invalid gc.pruneexpire: '%s'"),
-						prune_expire);
-			}
-		}
-	}
+	git_config_date_string("gc.pruneexpire", &prune_expire);
 	git_config(git_default_config, NULL);
 }
 
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 28/32] gc: support prune --worktrees
  2014-09-28  1:22     ` [PATCH v3 " Nguyễn Thái Ngọc Duy
                         ` (26 preceding siblings ...)
  2014-09-28  1:22       ` [PATCH v3 27/32] gc: factor out gc.pruneexpire parsing code Nguyễn Thái Ngọc Duy
@ 2014-09-28  1:22       ` Nguyễn Thái Ngọc Duy
  2014-09-28  1:22       ` [PATCH v3 29/32] count-objects: report unused files in $GIT_DIR/worktrees/ Nguyễn Thái Ngọc Duy
                         ` (4 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-28  1:22 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy,
	Marc Branchaud

Helped-by: Marc Branchaud <marcnarc@xiplink.com>
Signed-off-by: Marc Branchaud <marcnarc@xiplink.com>
Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
---
 Documentation/config.txt       |  7 +++++++
 Documentation/git-checkout.txt | 11 +++++++----
 builtin/gc.c                   | 10 ++++++++++
 3 files changed, 24 insertions(+), 4 deletions(-)

diff --git a/Documentation/config.txt b/Documentation/config.txt
index 98b8ef0..8351c8a 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -1212,6 +1212,13 @@ gc.pruneexpire::
 	"now" may be used to disable this  grace period and always prune
 	unreachable objects immediately.
 
+gc.pruneworktreesexpire::
+	When 'git gc' is run, it will call
+	'prune --worktrees --expire 3.months.ago'.
+	Override the grace period with this config variable. The value
+	"now" may be used to disable the grace period and prune
+	$GIT_DIR/worktrees immediately.
+
 gc.reflogexpire::
 gc.<pattern>.reflogexpire::
 	'git reflog expire' removes reflog entries older than
diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt
index 35675da..0c13825 100644
--- a/Documentation/git-checkout.txt
+++ b/Documentation/git-checkout.txt
@@ -435,8 +435,11 @@ $GIT_DIR or $GIT_COMMON_DIR when you need to directly access something
 inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.
 
 When you are done with a linked working tree you can simply delete it.
-You can clean up any stale $GIT_DIR/worktrees entries via `git prune
---worktrees` in the main or any linked working tree.
+The working tree's entry in the repository's $GIT_DIR/worktrees
+directory will eventually be removed automatically (see
+`gc.pruneworktreesexpire` in linkgit::git-config[1]), or you can run
+`git prune --worktrees` in the main or any linked working tree to
+clean up any stale entries in $GIT_DIR/worktrees.
 
 If you move a linked working directory to another file system, or
 within a file system that does not support hard links, you need to run
@@ -444,8 +447,8 @@ at least one git command inside the linked working directory
 (e.g. `git status`) in order to update its entry in $GIT_DIR/worktrees
 so that it does not get automatically removed.
 
-To prevent `git prune --worktrees` from deleting a $GIT_DIR/worktrees
-entry (which can be useful in some situations, such as when the
+To prevent a $GIT_DIR/worktrees entry from from being pruned (which
+can be useful in some situations, such as when the
 entry's working tree is stored on a portable device), add a file named
 'locked' to the entry's directory. The file contains the reason in
 plain text. For example, if a linked working tree's `.git` file points
diff --git a/builtin/gc.c b/builtin/gc.c
index 849a87c..35542f3 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -33,11 +33,13 @@ static int gc_auto_threshold = 6700;
 static int gc_auto_pack_limit = 50;
 static int detach_auto = 1;
 static const char *prune_expire = "2.weeks.ago";
+static const char *prune_worktrees_expire = "3.months.ago";
 
 static struct argv_array pack_refs_cmd = ARGV_ARRAY_INIT;
 static struct argv_array reflog = ARGV_ARRAY_INIT;
 static struct argv_array repack = ARGV_ARRAY_INIT;
 static struct argv_array prune = ARGV_ARRAY_INIT;
+static struct argv_array prune_worktrees = ARGV_ARRAY_INIT;
 static struct argv_array rerere = ARGV_ARRAY_INIT;
 
 static char *pidfile;
@@ -83,6 +85,7 @@ static void gc_config(void)
 	git_config_get_int("gc.autopacklimit", &gc_auto_pack_limit);
 	git_config_get_bool("gc.autodetach", &detach_auto);
 	git_config_date_string("gc.pruneexpire", &prune_expire);
+	git_config_date_string("gc.pruneworktreesexpire", &prune_worktrees_expire);
 	git_config(git_default_config, NULL);
 }
 
@@ -290,6 +293,7 @@ int cmd_gc(int argc, const char **argv, const char *prefix)
 	argv_array_pushl(&reflog, "reflog", "expire", "--all", NULL);
 	argv_array_pushl(&repack, "repack", "-d", "-l", NULL);
 	argv_array_pushl(&prune, "prune", "--expire", NULL);
+	argv_array_pushl(&prune_worktrees, "prune", "--worktrees", "--expire", NULL);
 	argv_array_pushl(&rerere, "rerere", "gc", NULL);
 
 	gc_config();
@@ -359,6 +363,12 @@ int cmd_gc(int argc, const char **argv, const char *prefix)
 			return error(FAILED_RUN, prune.argv[0]);
 	}
 
+	if (prune_worktrees_expire) {
+		argv_array_push(&prune_worktrees, prune_worktrees_expire);
+		if (run_command_v_opt(prune_worktrees.argv, RUN_GIT_CMD))
+			return error(FAILED_RUN, prune_worktrees.argv[0]);
+	}
+
 	if (run_command_v_opt(rerere.argv, RUN_GIT_CMD))
 		return error(FAILED_RUN, rerere.argv[0]);
 
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 29/32] count-objects: report unused files in $GIT_DIR/worktrees/...
  2014-09-28  1:22     ` [PATCH v3 " Nguyễn Thái Ngọc Duy
                         ` (27 preceding siblings ...)
  2014-09-28  1:22       ` [PATCH v3 28/32] gc: support prune --worktrees Nguyễn Thái Ngọc Duy
@ 2014-09-28  1:22       ` Nguyễn Thái Ngọc Duy
  2014-09-28  1:22       ` [PATCH v3 30/32] git_path(): keep "info/sparse-checkout" per work-tree Nguyễn Thái Ngọc Duy
                         ` (3 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-28  1:22 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

In linked checkouts, borrowed parts like config is taken from
$GIT_COMMON_DIR. $GIT_DIR/config is never used. Report them as
garbage.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
---
 builtin/count-objects.c |  4 +++-
 cache.h                 |  1 +
 path.c                  | 29 +++++++++++++++++++++++++++--
 3 files changed, 31 insertions(+), 3 deletions(-)

diff --git a/builtin/count-objects.c b/builtin/count-objects.c
index a7f70cb..d3a1620 100644
--- a/builtin/count-objects.c
+++ b/builtin/count-objects.c
@@ -102,8 +102,10 @@ int cmd_count_objects(int argc, const char **argv, const char *prefix)
 	/* we do not take arguments other than flags for now */
 	if (argc)
 		usage_with_options(count_objects_usage, opts);
-	if (verbose)
+	if (verbose) {
 		report_garbage = real_report_garbage;
+		report_linked_checkout_garbage();
+	}
 	memcpy(path, objdir, len);
 	if (len && objdir[len-1] != '/')
 		path[len++] = '/';
diff --git a/cache.h b/cache.h
index aedfd05..ab3adb6 100644
--- a/cache.h
+++ b/cache.h
@@ -713,6 +713,7 @@ extern const char *mkpath(const char *fmt, ...) __attribute__((format (printf, 1
 extern const char *git_path(const char *fmt, ...) __attribute__((format (printf, 1, 2)));
 extern const char *git_path_submodule(const char *path, const char *fmt, ...)
 	__attribute__((format (printf, 2, 3)));
+extern void report_linked_checkout_garbage(void);
 
 /*
  * Return the name of the file in the local object database that would
diff --git a/path.c b/path.c
index 72eca6d..cd8e2d6 100644
--- a/path.c
+++ b/path.c
@@ -4,6 +4,7 @@
 #include "cache.h"
 #include "strbuf.h"
 #include "string-list.h"
+#include "dir.h"
 
 static int get_st_mode_bits(const char *path, int *mode)
 {
@@ -91,9 +92,9 @@ static void replace_dir(struct strbuf *buf, int len, const char *newdir)
 }
 
 static const char *common_list[] = {
-	"/branches", "/hooks", "/info", "/logs", "/lost-found", "/modules",
+	"/branches", "/hooks", "/info", "!/logs", "/lost-found", "/modules",
 	"/objects", "/refs", "/remotes", "/worktrees", "/rr-cache", "/svn",
-	"config", "gc.pid", "packed-refs", "shallow",
+	"config", "!gc.pid", "packed-refs", "shallow",
 	NULL
 };
 
@@ -107,6 +108,8 @@ static void update_common_dir(struct strbuf *buf, int git_dir_len)
 	for (p = common_list; *p; p++) {
 		const char *path = *p;
 		int is_dir = 0;
+		if (*path == '!')
+			path++;
 		if (*path == '/') {
 			path++;
 			is_dir = 1;
@@ -122,6 +125,28 @@ static void update_common_dir(struct strbuf *buf, int git_dir_len)
 	}
 }
 
+void report_linked_checkout_garbage(void)
+{
+	struct strbuf sb = STRBUF_INIT;
+	const char **p;
+	int len;
+
+	if (!git_common_dir_env)
+		return;
+	strbuf_addf(&sb, "%s/", get_git_dir());
+	len = sb.len;
+	for (p = common_list; *p; p++) {
+		const char *path = *p;
+		if (*path == '!')
+			continue;
+		strbuf_setlen(&sb, len);
+		strbuf_addstr(&sb, path);
+		if (file_exists(sb.buf))
+			report_garbage("unused in linked checkout", sb.buf);
+	}
+	strbuf_release(&sb);
+}
+
 static void adjust_git_path(struct strbuf *buf, int git_dir_len)
 {
 	const char *base = buf->buf + git_dir_len;
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 30/32] git_path(): keep "info/sparse-checkout" per work-tree
  2014-09-28  1:22     ` [PATCH v3 " Nguyễn Thái Ngọc Duy
                         ` (28 preceding siblings ...)
  2014-09-28  1:22       ` [PATCH v3 29/32] count-objects: report unused files in $GIT_DIR/worktrees/ Nguyễn Thái Ngọc Duy
@ 2014-09-28  1:22       ` Nguyễn Thái Ngọc Duy
  2014-09-28  1:22       ` [PATCH v3 31/32] checkout: don't require a work tree when checking out into a new one Nguyễn Thái Ngọc Duy
                         ` (2 subsequent siblings)
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-28  1:22 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Currently git_path("info/sparse-checkout") resolves to
$GIT_COMMON_DIR/info/sparse-checkout in multiple worktree mode. It
makes more sense for the sparse checkout patterns to be per worktree,
so you can have multiple checkouts with different parts of the tree.

With this, "git checkout --to <new>" on a sparse checkout will create
<new> as a full checkout. Which is expected, it's how a new checkout
is made. The user can reshape the worktree afterwards.

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 path.c                | 3 ++-
 t/t0060-path-utils.sh | 1 +
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/path.c b/path.c
index cd8e2d6..35d498e 100644
--- a/path.c
+++ b/path.c
@@ -103,7 +103,8 @@ static void update_common_dir(struct strbuf *buf, int git_dir_len)
 	char *base = buf->buf + git_dir_len;
 	const char **p;
 
-	if (is_dir_file(base, "logs", "HEAD"))
+	if (is_dir_file(base, "logs", "HEAD") ||
+	    is_dir_file(base, "info", "sparse-checkout"))
 		return;	/* keep this in $GIT_DIR */
 	for (p = common_list; *p; p++) {
 		const char *path = *p;
diff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh
index f5d6f80..93605f4 100755
--- a/t/t0060-path-utils.sh
+++ b/t/t0060-path-utils.sh
@@ -270,6 +270,7 @@ test_git_path GIT_COMMON_DIR=bar objects                  bar/objects
 test_git_path GIT_COMMON_DIR=bar objects/bar              bar/objects/bar
 test_git_path GIT_COMMON_DIR=bar info/exclude             bar/info/exclude
 test_git_path GIT_COMMON_DIR=bar info/grafts              bar/info/grafts
+test_git_path GIT_COMMON_DIR=bar info/sparse-checkout     .git/info/sparse-checkout
 test_git_path GIT_COMMON_DIR=bar remotes/bar              bar/remotes/bar
 test_git_path GIT_COMMON_DIR=bar branches/bar             bar/branches/bar
 test_git_path GIT_COMMON_DIR=bar logs/refs/heads/master   bar/logs/refs/heads/master
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 31/32] checkout: don't require a work tree when checking out into a new one
  2014-09-28  1:22     ` [PATCH v3 " Nguyễn Thái Ngọc Duy
                         ` (29 preceding siblings ...)
  2014-09-28  1:22       ` [PATCH v3 30/32] git_path(): keep "info/sparse-checkout" per work-tree Nguyễn Thái Ngọc Duy
@ 2014-09-28  1:22       ` Nguyễn Thái Ngọc Duy
  2014-09-28  1:22       ` [PATCH v3 32/32] t2025: add a test to make sure grafts is working from a linked checkout Nguyễn Thái Ngọc Duy
  2014-09-28  6:35       ` [PATCH v3 00/32] nd/multiple-work-trees Junio C Hamano
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-28  1:22 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Dennis Kaarsemaker,
	Nguyễn Thái Ngọc Duy

From: Dennis Kaarsemaker <dennis@kaarsemaker.net>

For normal use cases, it does not make sense for 'checkout' to work on
a bare repository, without a worktree. But "checkout --to" is an
exception because it _creates_ a new worktree. Allow this option to
run on bare repositories.

People who check out from a bare repository should remember that
core.logallrefupdates is off by default and it should be turned back
on. `--to` cannot do this automatically behind the user's back because
some user may deliberately want no reflog.

For people interested in repository setup/discovery code,
is_bare_repository_cfg (aka "core.bare") is unchanged by this patch,
which means 'true' by default for bare repos. Fortunately when we get
the repo through a linked checkout, is_bare_repository_cfg is never
used. So all is still good.

[nd: commit message]

Signed-off-by: Dennis Kaarsemaker <dennis@kaarsemaker.net>
Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
---
 builtin/checkout.c     |  3 +++
 git.c                  |  2 +-
 t/t2025-checkout-to.sh | 15 +++++++++++++++
 3 files changed, 19 insertions(+), 1 deletion(-)

diff --git a/builtin/checkout.c b/builtin/checkout.c
index bd0e2d5..01d0f2f 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -1364,6 +1364,9 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
 	if (opts.new_worktree_mode)
 		opts.new_worktree = NULL;
 
+	if (!opts.new_worktree)
+		setup_work_tree();
+
 	if (conflict_style) {
 		opts.merge = 1; /* implied */
 		git_xmerge_config("merge.conflictstyle", conflict_style, NULL);
diff --git a/git.c b/git.c
index 4076b01..6c1ac0c 100644
--- a/git.c
+++ b/git.c
@@ -383,7 +383,7 @@ static struct cmd_struct commands[] = {
 	{ "check-ignore", cmd_check_ignore, RUN_SETUP | NEED_WORK_TREE },
 	{ "check-mailmap", cmd_check_mailmap, RUN_SETUP },
 	{ "check-ref-format", cmd_check_ref_format },
-	{ "checkout", cmd_checkout, RUN_SETUP | NEED_WORK_TREE },
+	{ "checkout", cmd_checkout, RUN_SETUP },
 	{ "checkout-index", cmd_checkout_index,
 		RUN_SETUP | NEED_WORK_TREE},
 	{ "cherry", cmd_cherry, RUN_SETUP },
diff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh
index e2db078..4bd1df4 100755
--- a/t/t2025-checkout-to.sh
+++ b/t/t2025-checkout-to.sh
@@ -81,4 +81,19 @@ test_expect_success 'not die on re-checking out current branch' '
 	)
 '
 
+test_expect_success 'checkout --to from a bare repo' '
+	(
+		git clone --bare . bare &&
+		cd bare &&
+		git checkout --to ../there2 -b bare-master master
+	)
+'
+
+test_expect_success 'checkout from a bare repo without --to' '
+	(
+		cd bare &&
+		test_must_fail git checkout master
+	)
+'
+
 test_done
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* [PATCH v3 32/32] t2025: add a test to make sure grafts is working from a linked checkout
  2014-09-28  1:22     ` [PATCH v3 " Nguyễn Thái Ngọc Duy
                         ` (30 preceding siblings ...)
  2014-09-28  1:22       ` [PATCH v3 31/32] checkout: don't require a work tree when checking out into a new one Nguyễn Thái Ngọc Duy
@ 2014-09-28  1:22       ` Nguyễn Thái Ngọc Duy
  2014-09-28  6:35       ` [PATCH v3 00/32] nd/multiple-work-trees Junio C Hamano
  32 siblings, 0 replies; 134+ messages in thread
From: Nguyễn Thái Ngọc Duy @ 2014-09-28  1:22 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, Nguyễn Thái Ngọc Duy

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>
---
 t/t2025-checkout-to.sh | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)

diff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh
index 4bd1df4..eddd325 100755
--- a/t/t2025-checkout-to.sh
+++ b/t/t2025-checkout-to.sh
@@ -96,4 +96,22 @@ test_expect_success 'checkout from a bare repo without --to' '
 	)
 '
 
+test_expect_success 'checkout with grafts' '
+	test_when_finished rm .git/info/grafts &&
+	test_commit abc &&
+	SHA1=`git rev-parse HEAD` &&
+	test_commit def &&
+	test_commit xyz &&
+	echo "`git rev-parse HEAD` $SHA1" >.git/info/grafts &&
+	cat >expected <<-\EOF &&
+	xyz
+	abc
+	EOF
+	git log --format=%s -2 >actual &&
+	test_cmp expected actual &&
+	git checkout --detach --to grafted master &&
+	git --git-dir=grafted/.git log --format=%s -2 >actual &&
+	test_cmp expected actual
+'
+
 test_done
-- 
2.1.0.rc0.78.gc0d8480

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* Re: [PATCH v3 00/32] nd/multiple-work-trees
  2014-09-28  1:22     ` [PATCH v3 " Nguyễn Thái Ngọc Duy
                         ` (31 preceding siblings ...)
  2014-09-28  1:22       ` [PATCH v3 32/32] t2025: add a test to make sure grafts is working from a linked checkout Nguyễn Thái Ngọc Duy
@ 2014-09-28  6:35       ` Junio C Hamano
  32 siblings, 0 replies; 134+ messages in thread
From: Junio C Hamano @ 2014-09-28  6:35 UTC (permalink / raw)
  To: Nguyễn Thái Ngọc Duy; +Cc: git

Nguyễn Thái Ngọc Duy  <pclouds@gmail.com> writes:

> On Fri, Sep 26, 2014 at 4:20 AM, Junio C Hamano <gitster@pobox.com> wrote:
>> It has been a while since the last review exchanges were seen.  Will
>> it be time for v3 soon?
>
> Sorry I've been slow on picking up feedback from v2. v3 is rebased on
> latest master. Other changes are mostly *.txt, and one broken &&
> chain.

As there have been quite a lot of changes in overlapping areas
(e.g. with Ronnie and Jonathan's refs.c clean-up series, and also
with Tanay's config API changes in builtin/gc.c), I agree that it no
longer makes sense to stick to the old fork point.

This applies cleanly to the 'master', the result merges cleanly to
where the old series were merged to 'pu', and the difference between
the old/new "pu^{/^Merge branch 'nd/multiple-work-trees' into pu}"
commits matches what I remember from the review discussion you were
having on the old series with others (attached below for others to
eyeball as a rough substitute for an interdiff).

Thanks.

diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt
index 23f0c80..0c13825 100644
--- a/Documentation/git-checkout.txt
+++ b/Documentation/git-checkout.txt
@@ -229,8 +229,8 @@ section of linkgit:git-add[1] to learn how to operate the `--patch` mode.
 	Check out a branch in a separate working directory at
 	`<path>`. A new working directory is linked to the current
 	repository, sharing everything except working directory
-	specific files such as HEAD, index... See "MULTIPLE CHECKOUT
-	MODE" section for more information.
+	specific files such as HEAD, index... See "MULTIPLE WORKING
+	TREES" section for more information.
 
 <branch>::
 	Branch to checkout; if it refers to a branch (i.e., a name that,
@@ -395,58 +395,66 @@ $ git reflog -2 HEAD # or
 $ git log -g -2 HEAD
 ------------
 
-MULTIPLE CHECKOUT MODE
+MULTIPLE WORKING TREES
 ----------------------
-Normally a working directory is attached to repository. When "git
-checkout --to" is used, a new working directory is attached to the
-current repository. This new working directory is called "linked
-checkout" as compared to the "main checkout" prepared by "git init" or
-"git clone". A repository has one main checkout and zero or more
-linked checkouts.
-
-Each linked checkout has a private directory in $GIT_DIR/worktrees in
-the main checkout, usually named after the base name of the new
-working directory, optionally with a number added to make it
-unique. For example, the command `git checkout --to ../test-next next`
-running with $GIT_DIR=/path/main may create the directory
-`$GIT_DIR/worktrees/test-next` (or `$GIT_DIR/worktrees/test-next1` if
-`test-next` is already taken).
-
-Within a linked checkout, $GIT_DIR is set to point to this private
-directory (e.g. `/path/main/worktrees/test-next` in the example) and
-$GIT_COMMON_DIR is set to point back to the main checkout's $GIT_DIR
-(e.g. `/path/main`). Setting is done via a .git file located at the
-top directory of the linked checkout.
-
-Path resolution via `git rev-parse --git-path` would use either
-$GIT_DIR or $GIT_COMMON_DIR depending on the path. For example, the
-linked checkout's `$GIT_DIR/HEAD` resolve to
-`/path/main/worktrees/test-next/HEAD` (not `/path/main/HEAD` which is
-the main checkout's HEAD) while `$GIT_DIR/refs/heads/master` would use
-$GIT_COMMON_DIR and resolve to `/path/main/refs/heads/master`, which
-is shared across checkouts.
+
+A git repository can support multiple working trees, allowing you to check
+out more than one branch at a time.  With `git checkout --to` a new working
+tree is associated with the repository.  This new working tree is called a
+"linked working tree" as opposed to the "main working tree" prepared by "git
+init" or "git clone".  A repository has one main working tree (if it's not a
+bare repository) and zero or more linked working trees.
+
+Each linked working tree has a private sub-directory in the repository's
+$GIT_DIR/worktrees directory.  The private sub-directory's name is usually
+the base name of the linked working tree's path, possibly appended with a
+number to make it unique.  For example, when `$GIT_DIR=/path/main/.git` the
+command `git checkout --to /path/other/test-next next` creates the linked
+working tree in `/path/other/test-next` and also creates a
+`$GIT_DIR/worktrees/test-next` directory (or `$GIT_DIR/worktrees/test-next1`
+if `test-next` is already taken).
+
+Within a linked working tree, $GIT_DIR is set to point to this private
+directory (e.g. `/path/main/.git/worktrees/test-next` in the example) and
+$GIT_COMMON_DIR is set to point back to the main working tree's $GIT_DIR
+(e.g. `/path/main/.git`). These settings are made in a `.git` file located at
+the top directory of the linked working tree.
+
+Path resolution via `git rev-parse --git-path` uses either
+$GIT_DIR or $GIT_COMMON_DIR depending on the path. For example, in the
+linked working tree `git rev-parse --git-path HEAD` returns
+`/path/main/.git/worktrees/test-next/HEAD` (not
+`/path/other/test-next/.git/HEAD` or `/path/main/.git/HEAD`) while `git
+rev-parse --git-path refs/heads/master` uses
+$GIT_COMMON_DIR and returns `/path/main/.git/refs/heads/master`,
+since refs are shared across all working trees.
 
 See linkgit:gitrepository-layout[5] for more information. The rule of
 thumb is do not make any assumption about whether a path belongs to
 $GIT_DIR or $GIT_COMMON_DIR when you need to directly access something
 inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.
 
-When you are done, simply deleting the linked working directory would
-suffice. $GIT_DIR/worktrees can be cleaned up using `git prune
---worktrees`, which is part of automated garbage collection.
-
-After you move a linked working directory to another file system, or
-on a file system that does not support hard link, execute any git
-command (e.g. `git status`) in the new working directory so that it
-could update its location in $GIT_DIR/worktrees and not be
-accidentally pruned.
-
-To stop `git prune --worktrees` from deleting a specific working
-directory (e.g. because it's on a portable device), you could add the
-file 'locked' to $GIT_DIR/worktrees. For example, if `.git` file of
-the new working directory points to `/path/main/worktrees/test-next`,
-the full path of the 'locked' file would be
-`/path/main/worktrees/test-next/locked`. See
+When you are done with a linked working tree you can simply delete it.
+The working tree's entry in the repository's $GIT_DIR/worktrees
+directory will eventually be removed automatically (see
+`gc.pruneworktreesexpire` in linkgit::git-config[1]), or you can run
+`git prune --worktrees` in the main or any linked working tree to
+clean up any stale entries in $GIT_DIR/worktrees.
+
+If you move a linked working directory to another file system, or
+within a file system that does not support hard links, you need to run
+at least one git command inside the linked working directory
+(e.g. `git status`) in order to update its entry in $GIT_DIR/worktrees
+so that it does not get automatically removed.
+
+To prevent a $GIT_DIR/worktrees entry from from being pruned (which
+can be useful in some situations, such as when the
+entry's working tree is stored on a portable device), add a file named
+'locked' to the entry's directory. The file contains the reason in
+plain text. For example, if a linked working tree's `.git` file points
+to `/path/main/.git/worktrees/test-next` then a file named
+`/path/main/.git/worktrees/test-next/locked` will prevent the
+`test-next` entry from being pruned.  See
 linkgit:gitrepository-layout[5] for details.
 
 EXAMPLES
diff --git a/Documentation/git-prune.txt b/Documentation/git-prune.txt
index a0ea381..1cf3bed 100644
--- a/Documentation/git-prune.txt
+++ b/Documentation/git-prune.txt
@@ -49,7 +49,7 @@ OPTIONS
 	Only expire loose objects older than <time>.
 
 --worktrees::
-	Prune dead worktree information in $GIT_DIR/worktrees.
+	Prune dead working tree information in $GIT_DIR/worktrees.
 
 <head>...::
 	In addition to objects
diff --git a/builtin/gc.c b/builtin/gc.c
index 125f0dd..35542f3 100644
--- a/builtin/gc.c
+++ b/builtin/gc.c
@@ -57,12 +57,14 @@ static void remove_pidfile_on_signal(int signo)
 	raise(signo);
 }
 
-static void git_config_get_date_string(const char *var, const char **out)
+static void git_config_date_string(const char *key, const char **output)
 {
-	if (!git_config_get_string_const(var, out) && strcmp(*out, "now")) {
+	if (git_config_get_string_const(key, output))
+		return;
+	if (strcmp(*output, "now")) {
 		unsigned long now = approxidate("now");
-		if (approxidate(*out) >= now)
-			git_die_config(var, _("Invalid %s: '%s'"), var, *out);
+		if (approxidate(*output) >= now)
+			git_die_config(key, _("Invalid %s: '%s'"), key, *output);
 	}
 }
 
@@ -82,8 +84,8 @@ static void gc_config(void)
 	git_config_get_int("gc.auto", &gc_auto_threshold);
 	git_config_get_int("gc.autopacklimit", &gc_auto_pack_limit);
 	git_config_get_bool("gc.autodetach", &detach_auto);
-	git_config_get_date_string("gc.pruneexpire", &prune_expire);
-	git_config_get_date_string("gc.pruneworktreesexpire", &prune_worktrees_expire);
+	git_config_date_string("gc.pruneexpire", &prune_expire);
+	git_config_date_string("gc.pruneworktreesexpire", &prune_worktrees_expire);
 	git_config(git_default_config, NULL);
 }
 
diff --git a/refs.c b/refs.c
index c4850f5..33ac01c 100644
--- a/refs.c
+++ b/refs.c
@@ -2935,12 +2935,14 @@ static int log_ref_write(const char *refname, const unsigned char *old_sha1,
 		error("Unable to append to %s", log_file);
 		errno = save_errno;
 		result = -1;
+		goto done;
 	}
 	if (close(logfd)) {
 		int save_errno = errno;
 		error("Unable to append to %s", log_file);
 		errno = save_errno;
 		result = -1;
+		goto done;
 	}
 done:
 	strbuf_release(&sb_log_file);
diff --git a/t/t2025-checkout-to.sh b/t/t2025-checkout-to.sh
index 27384a1..eddd325 100755
--- a/t/t2025-checkout-to.sh
+++ b/t/t2025-checkout-to.sh
@@ -49,7 +49,7 @@ test_expect_success 'checkout --to a new worktree from a subdir' '
 test_expect_success 'checkout --to from a linked checkout' '
 	(
 		cd here &&
-		git checkout --detach --to nested-here master
+		git checkout --detach --to nested-here master &&
 		cd nested-here &&
 		git fsck
 	)
diff --git a/wrapper.c b/wrapper.c
index 6f40341..e821d05 100644
--- a/wrapper.c
+++ b/wrapper.c
@@ -531,6 +531,14 @@ struct passwd *xgetpwuid_self(void)
 	return pw;
 }
 
+char *xgetcwd(void)
+{
+	struct strbuf sb = STRBUF_INIT;
+	if (strbuf_getcwd(&sb))
+		die_errno(_("unable to get current working directory"));
+	return strbuf_detach(&sb, NULL);
+}
+
 int write_file(const char *path, int fatal, const char *fmt, ...)
 {
 	struct strbuf sb = STRBUF_INIT;
@@ -561,11 +569,3 @@ int write_file(const char *path, int fatal, const char *fmt, ...)
 	}
 	return 0;
 }
-
-char *xgetcwd(void)
-{
-	struct strbuf sb = STRBUF_INIT;
-	if (strbuf_getcwd(&sb))
-		die_errno(_("unable to get current working directory"));
-	return strbuf_detach(&sb, NULL);
-}

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* Re: [PATCH v3 19/32] setup.c: support multi-checkout repo setup
  2014-09-28  1:22       ` [PATCH v3 19/32] setup.c: support multi-checkout repo setup Nguyễn Thái Ngọc Duy
@ 2014-10-01 16:19         ` Johannes Sixt
  0 siblings, 0 replies; 134+ messages in thread
From: Johannes Sixt @ 2014-10-01 16:19 UTC (permalink / raw)
  To: Nguyễn Thái Ngọc Duy; +Cc: git, Junio C Hamano

Am 28.09.2014 um 03:22 schrieb Nguyễn Thái Ngọc Duy:
> +test_expect_success 'GIT_DIR set (2)' '
> +	echo "gitdir: repo.git/repos/foo" >gitfile &&
> +	echo "$TRASH_DIRECTORY/repo.git" >repo.git/repos/foo/commondir &&
> +	(
> +		cd work &&
> +		GIT_DIR=../gitfile git rev-parse --git-common-dir >actual &&
> +		test-path-utils real_path "$TRASH_DIRECTORY/repo.git" >expect &&
> +		test_cmp expect actual
> +	)
> +'

This requires the following fixup because MinGW git will understand
only DOS style absolute paths, but $TRASH_DIRECTORY is in
POSIX-MSYS-style /c/foo/bar.

--- 8< ---
Subject: [PATCH] fixup! setup.c: support multi-checkout repo setup

Signed-off-by: Johannes Sixt <j6t@kdbg.org>
---
 t/t1501-worktree.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/t/t1501-worktree.sh b/t/t1501-worktree.sh
index e6ac7a4..4df7a2f 100755
--- a/t/t1501-worktree.sh
+++ b/t/t1501-worktree.sh
@@ -366,7 +366,7 @@ test_expect_success 'GIT_DIR set (1)' '
 
 test_expect_success 'GIT_DIR set (2)' '
 	echo "gitdir: repo.git/repos/foo" >gitfile &&
-	echo "$TRASH_DIRECTORY/repo.git" >repo.git/repos/foo/commondir &&
+	echo "$(pwd)/repo.git" >repo.git/repos/foo/commondir &&
 	(
 		cd work &&
 		GIT_DIR=../gitfile git rev-parse --git-common-dir >actual &&
-- 
2.0.0.12.gbcf935e

^ permalink raw reply related	[flat|nested] 134+ messages in thread

* Re: [PATCH v3 23/32] prune: strategies for linked checkouts
  2014-09-28  1:22       ` [PATCH v3 23/32] prune: strategies for linked checkouts Nguyễn Thái Ngọc Duy
@ 2014-10-01 16:20         ` Johannes Sixt
  0 siblings, 0 replies; 134+ messages in thread
From: Johannes Sixt @ 2014-10-01 16:20 UTC (permalink / raw)
  To: Nguyễn Thái Ngọc Duy; +Cc: git, Junio C Hamano, Marc Branchaud

Am 28.09.2014 um 03:22 schrieb Nguyễn Thái Ngọc Duy:
> +test_expect_success 'prune directories with gitdir pointing to nowhere' '
> +	mkdir -p .git/worktrees/def/abc &&
> +	: >.git/worktrees/def/def &&
> +	echo "$TRASH_DIRECTORY"/nowhere >.git/worktrees/def/gitdir &&
> +	git prune --worktrees --verbose >actual &&
> +	test_i18ngrep "Removing worktrees/def: gitdir file points to non-existent location" actual &&
> +	! test -d .git/worktrees/def &&
> +	! test -d .git/worktrees
> +'
> ...
> +test_expect_success 'not prune recent checkouts' '
> +	test_when_finished rm -r .git/worktrees
> +	mkdir zz &&
> +	mkdir -p .git/worktrees/jlm &&
> +	echo "$TRASH_DIRECTORY"/zz >.git/worktrees/jlm/gitdir &&
> +	git prune --worktrees --verbose --expire=2.days.ago &&
> +	test -d .git/worktrees/jlm
> +'

These require the following fixups because MinGW git will understand
only DOS style absolute paths, but $TRASH_DIRECTORY is in
POSIX-MSYS-style /c/foo/bar.

--- 8< ---
Subject: [PATCH] fixup! prune: strategies for linked checkouts

Signed-off-by: Johannes Sixt <j6t@kdbg.org>
---
 t/t2026-prune-linked-checkouts.sh | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/t/t2026-prune-linked-checkouts.sh b/t/t2026-prune-linked-checkouts.sh
index 3622800..170aefe 100755
--- a/t/t2026-prune-linked-checkouts.sh
+++ b/t/t2026-prune-linked-checkouts.sh
@@ -57,7 +57,7 @@ test_expect_success 'prune directories with invalid gitdir' '
 test_expect_success 'prune directories with gitdir pointing to nowhere' '
 	mkdir -p .git/worktrees/def/abc &&
 	: >.git/worktrees/def/def &&
-	echo "$TRASH_DIRECTORY"/nowhere >.git/worktrees/def/gitdir &&
+	echo "$(pwd)"/nowhere >.git/worktrees/def/gitdir &&
 	git prune --worktrees --verbose >actual &&
 	test_i18ngrep "Removing worktrees/def: gitdir file points to non-existent location" actual &&
 	! test -d .git/worktrees/def &&
@@ -76,7 +76,7 @@ test_expect_success 'not prune recent checkouts' '
 	test_when_finished rm -r .git/worktrees
 	mkdir zz &&
 	mkdir -p .git/worktrees/jlm &&
-	echo "$TRASH_DIRECTORY"/zz >.git/worktrees/jlm/gitdir &&
+	echo "$(pwd)"/zz >.git/worktrees/jlm/gitdir &&
 	git prune --worktrees --verbose --expire=2.days.ago &&
 	test -d .git/worktrees/jlm
 '
-- 
2.0.0.12.gbcf935e

^ permalink raw reply related	[flat|nested] 134+ messages in thread

end of thread, other threads:[~2014-10-01 16:20 UTC | newest]

Thread overview: 134+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2014-08-30  8:33 [PATCH 00/32] nd/multiple-work-trees cleanup Nguyễn Thái Ngọc Duy
2014-08-30  8:33 ` [PATCH 01/32] path.c: make get_pathname() return strbuf instead of static buffer Nguyễn Thái Ngọc Duy
2014-08-30  8:33 ` [PATCH 02/32] path.c: make get_pathname() call sites return const char * Nguyễn Thái Ngọc Duy
2014-08-30  8:33 ` [PATCH 03/32] git_snpath(): retire and replace with strbuf_git_path() Nguyễn Thái Ngọc Duy
2014-08-30  8:33 ` [PATCH 04/32] path.c: rename vsnpath() to do_git_path() Nguyễn Thái Ngọc Duy
2014-08-30  8:33 ` [PATCH 05/32] path.c: group git_path(), git_pathdup() and strbuf_git_path() together Nguyễn Thái Ngọc Duy
2014-08-30  8:33 ` [PATCH 06/32] git_path(): be aware of file relocation in $GIT_DIR Nguyễn Thái Ngọc Duy
2014-08-30  8:33 ` [PATCH 07/32] *.sh: respect $GIT_INDEX_FILE Nguyễn Thái Ngọc Duy
2014-08-30  8:33 ` [PATCH 08/32] reflog: avoid constructing .lock path with git_path Nguyễn Thái Ngọc Duy
2014-08-30  8:33 ` [PATCH 09/32] fast-import: use git_path() for accessing .git dir instead of get_git_dir() Nguyễn Thái Ngọc Duy
2014-08-30  8:33 ` [PATCH 10/32] commit: use SEQ_DIR instead of hardcoding "sequencer" Nguyễn Thái Ngọc Duy
2014-08-30  8:33 ` [PATCH 11/32] $GIT_COMMON_DIR: a new environment variable Nguyễn Thái Ngọc Duy
2014-08-30  8:33 ` [PATCH 12/32] git-sh-setup.sh: use rev-parse --git-path to get $GIT_DIR/objects Nguyễn Thái Ngọc Duy
2014-08-30  8:33 ` [PATCH 13/32] *.sh: avoid hardcoding $GIT_DIR/hooks/ Nguyễn Thái Ngọc Duy
2014-08-30  8:33 ` [PATCH 14/32] git-stash: avoid hardcoding $GIT_DIR/logs/ Nguyễn Thái Ngọc Duy
2014-08-30  8:33 ` [PATCH 15/32] setup.c: convert is_git_directory() to use strbuf Nguyễn Thái Ngọc Duy
2014-08-30  8:33 ` [PATCH 16/32] setup.c: detect $GIT_COMMON_DIR in is_git_directory() Nguyễn Thái Ngọc Duy
2014-08-30  8:33 ` [PATCH 17/32] setup.c: convert check_repository_format_gently to use strbuf Nguyễn Thái Ngọc Duy
2014-08-30  8:33 ` [PATCH 18/32] setup.c: detect $GIT_COMMON_DIR check_repository_format_gently() Nguyễn Thái Ngọc Duy
2014-08-30  8:33 ` [PATCH 19/32] setup.c: support multi-checkout repo setup Nguyễn Thái Ngọc Duy
2014-08-30  8:33 ` [PATCH 20/32] wrapper.c: wrapper to open a file, fprintf then close Nguyễn Thái Ngọc Duy
2014-08-30  8:33 ` [PATCH 21/32] use new wrapper write_file() for simple file writing Nguyễn Thái Ngọc Duy
2014-08-30  8:33 ` [PATCH 22/32] checkout: support checking out into a new working directory Nguyễn Thái Ngọc Duy
2014-08-30 20:50   ` Philip Oakley
2014-08-31  4:49     ` Duy Nguyen
2014-08-31  4:52       ` Duy Nguyen
2014-08-31 11:08         ` Philip Oakley
2014-09-02 12:27           ` Duy Nguyen
2014-09-02 14:51             ` Marc Branchaud
2014-09-02 17:33               ` Junio C Hamano
2014-09-04 14:19                 ` Duy Nguyen
2014-09-08 10:52                 ` Duy Nguyen
2014-09-08 14:06                   ` Marc Branchaud
2014-09-05  3:26               ` Scott Schmit
2014-09-05 13:42                 ` Duy Nguyen
2014-08-30  8:33 ` [PATCH 23/32] checkout: clean up half-prepared directories in --to mode Nguyễn Thái Ngọc Duy
2014-08-30  8:33 ` [PATCH 24/32] checkout: reject if the branch is already checked out elsewhere Nguyễn Thái Ngọc Duy
2014-08-30  8:33 ` [PATCH 25/32] prune: strategies for linked checkouts Nguyễn Thái Ngọc Duy
2014-08-30  8:33 ` [PATCH 26/32] gc: style change -- no SP before closing parenthesis Nguyễn Thái Ngọc Duy
2014-08-30  8:33 ` [PATCH 27/32] gc: factor out gc.pruneexpire parsing code Nguyễn Thái Ngọc Duy
2014-08-30  8:33 ` [PATCH 28/32] gc: support prune --repos Nguyễn Thái Ngọc Duy
2014-08-30  8:33 ` [PATCH 29/32] count-objects: report unused files in $GIT_DIR/repos/ Nguyễn Thái Ngọc Duy
2014-08-30  8:34 ` [PATCH 30/32] git_path(): keep "info/sparse-checkout" per work-tree Nguyễn Thái Ngọc Duy
2014-08-30  8:34 ` [PATCH 31/32] checkout: don't require a work tree when checking out into a new one Nguyễn Thái Ngọc Duy
2014-08-30  8:34 ` [PATCH 32/32] t2025: add a test to make sure grafts is working from a linked checkout Nguyễn Thái Ngọc Duy
2014-08-30 11:11 ` [PATCH 00/32] nd/multiple-work-trees cleanup Eric Sunshine
2014-08-30 11:14   ` Duy Nguyen
2014-09-02 17:29 ` Junio C Hamano
2014-09-10 22:41 ` [PATCH v2 00/32] nd/multiple-work-trees Nguyễn Thái Ngọc Duy
2014-09-10 22:41   ` [PATCH v2 01/32] path.c: make get_pathname() return strbuf instead of static buffer Nguyễn Thái Ngọc Duy
2014-09-10 22:41   ` [PATCH v2 02/32] path.c: make get_pathname() call sites return const char * Nguyễn Thái Ngọc Duy
2014-09-10 22:41   ` [PATCH v2 03/32] git_snpath(): retire and replace with strbuf_git_path() Nguyễn Thái Ngọc Duy
2014-09-10 22:41   ` [PATCH v2 04/32] path.c: rename vsnpath() to do_git_path() Nguyễn Thái Ngọc Duy
2014-09-10 22:41   ` [PATCH v2 05/32] path.c: group git_path(), git_pathdup() and strbuf_git_path() together Nguyễn Thái Ngọc Duy
2014-09-10 22:41   ` [PATCH v2 06/32] git_path(): be aware of file relocation in $GIT_DIR Nguyễn Thái Ngọc Duy
2014-09-10 22:41   ` [PATCH v2 07/32] *.sh: respect $GIT_INDEX_FILE Nguyễn Thái Ngọc Duy
2014-09-10 22:41   ` [PATCH v2 08/32] reflog: avoid constructing .lock path with git_path Nguyễn Thái Ngọc Duy
2014-09-10 22:41   ` [PATCH v2 09/32] fast-import: use git_path() for accessing .git dir instead of get_git_dir() Nguyễn Thái Ngọc Duy
2014-09-10 22:41   ` [PATCH v2 10/32] commit: use SEQ_DIR instead of hardcoding "sequencer" Nguyễn Thái Ngọc Duy
2014-09-10 22:41   ` [PATCH v2 11/32] $GIT_COMMON_DIR: a new environment variable Nguyễn Thái Ngọc Duy
2014-09-10 22:41   ` [PATCH v2 12/32] git-sh-setup.sh: use rev-parse --git-path to get $GIT_DIR/objects Nguyễn Thái Ngọc Duy
2014-09-10 22:41   ` [PATCH v2 13/32] *.sh: avoid hardcoding $GIT_DIR/hooks/ Nguyễn Thái Ngọc Duy
2014-09-10 22:41   ` [PATCH v2 14/32] git-stash: avoid hardcoding $GIT_DIR/logs/ Nguyễn Thái Ngọc Duy
2014-09-10 22:41   ` [PATCH v2 15/32] setup.c: convert is_git_directory() to use strbuf Nguyễn Thái Ngọc Duy
2014-09-10 22:41   ` [PATCH v2 16/32] setup.c: detect $GIT_COMMON_DIR in is_git_directory() Nguyễn Thái Ngọc Duy
2014-09-10 22:41   ` [PATCH v2 17/32] setup.c: convert check_repository_format_gently to use strbuf Nguyễn Thái Ngọc Duy
2014-09-10 22:41   ` [PATCH v2 18/32] setup.c: detect $GIT_COMMON_DIR check_repository_format_gently() Nguyễn Thái Ngọc Duy
2014-09-10 22:41   ` [PATCH v2 19/32] setup.c: support multi-checkout repo setup Nguyễn Thái Ngọc Duy
2014-09-10 22:41   ` [PATCH v2 20/32] wrapper.c: wrapper to open a file, fprintf then close Nguyễn Thái Ngọc Duy
2014-09-10 22:41   ` [PATCH v2 21/32] use new wrapper write_file() for simple file writing Nguyễn Thái Ngọc Duy
2014-09-10 22:41   ` [PATCH v2 22/32] checkout: support checking out into a new working directory Nguyễn Thái Ngọc Duy
2014-09-11 15:02     ` Marc Branchaud
2014-09-21  2:41       ` Duy Nguyen
2014-09-21  3:10         ` Eric Sunshine
2014-09-21  9:50           ` Duy Nguyen
2014-09-22 21:00             ` Marc Branchaud
2014-09-22 23:01               ` Eric Sunshine
2014-09-10 22:41   ` [PATCH v2 23/32] prune: strategies for linked checkouts Nguyễn Thái Ngọc Duy
2014-09-11 15:36     ` Marc Branchaud
2014-09-12  3:06       ` Eric Sunshine
2014-09-12 19:08         ` Marc Branchaud
2014-09-21  2:54         ` Duy Nguyen
2014-09-21  3:01           ` Eric Sunshine
2014-09-21 10:29             ` Duy Nguyen
2014-09-22 21:06               ` Marc Branchaud
2014-09-22 22:19               ` Eric Sunshine
2014-09-10 22:42   ` [PATCH v2 24/32] checkout: reject if the branch is already checked out elsewhere Nguyễn Thái Ngọc Duy
2014-09-10 22:42   ` [PATCH v2 25/32] checkout: clean up half-prepared directories in --to mode Nguyễn Thái Ngọc Duy
2014-09-10 22:42   ` [PATCH v2 26/32] gc: style change -- no SP before closing parenthesis Nguyễn Thái Ngọc Duy
2014-09-10 22:42   ` [PATCH v2 27/32] gc: factor out gc.pruneexpire parsing code Nguyễn Thái Ngọc Duy
2014-09-10 22:42   ` [PATCH v2 28/32] gc: support prune --worktrees Nguyễn Thái Ngọc Duy
2014-09-21 10:43     ` Duy Nguyen
2014-09-22 21:06       ` Marc Branchaud
2014-09-10 22:42   ` [PATCH v2 29/32] count-objects: report unused files in $GIT_DIR/worktrees/ Nguyễn Thái Ngọc Duy
2014-09-10 22:42   ` [PATCH v2 30/32] git_path(): keep "info/sparse-checkout" per work-tree Nguyễn Thái Ngọc Duy
2014-09-10 22:42   ` [PATCH v2 31/32] checkout: don't require a work tree when checking out into a new one Nguyễn Thái Ngọc Duy
2014-09-10 22:42   ` [PATCH v2 32/32] t2025: add a test to make sure grafts is working from a linked checkout Nguyễn Thái Ngọc Duy
2014-09-25 21:20   ` [PATCH v2 00/32] nd/multiple-work-trees Junio C Hamano
2014-09-28  1:22     ` [PATCH v3 " Nguyễn Thái Ngọc Duy
2014-09-28  1:22       ` [PATCH v3 01/32] path.c: make get_pathname() return strbuf instead of static buffer Nguyễn Thái Ngọc Duy
2014-09-28  1:22       ` [PATCH v3 02/32] path.c: make get_pathname() call sites return const char * Nguyễn Thái Ngọc Duy
2014-09-28  1:22       ` [PATCH v3 03/32] git_snpath(): retire and replace with strbuf_git_path() Nguyễn Thái Ngọc Duy
2014-09-28  1:22       ` [PATCH v3 04/32] path.c: rename vsnpath() to do_git_path() Nguyễn Thái Ngọc Duy
2014-09-28  1:22       ` [PATCH v3 05/32] path.c: group git_path(), git_pathdup() and strbuf_git_path() together Nguyễn Thái Ngọc Duy
2014-09-28  1:22       ` [PATCH v3 06/32] git_path(): be aware of file relocation in $GIT_DIR Nguyễn Thái Ngọc Duy
2014-09-28  1:22       ` [PATCH v3 07/32] *.sh: respect $GIT_INDEX_FILE Nguyễn Thái Ngọc Duy
2014-09-28  1:22       ` [PATCH v3 08/32] reflog: avoid constructing .lock path with git_path Nguyễn Thái Ngọc Duy
2014-09-28  1:22       ` [PATCH v3 09/32] fast-import: use git_path() for accessing .git dir instead of get_git_dir() Nguyễn Thái Ngọc Duy
2014-09-28  1:22       ` [PATCH v3 10/32] commit: use SEQ_DIR instead of hardcoding "sequencer" Nguyễn Thái Ngọc Duy
2014-09-28  1:22       ` [PATCH v3 11/32] $GIT_COMMON_DIR: a new environment variable Nguyễn Thái Ngọc Duy
2014-09-28  1:22       ` [PATCH v3 12/32] git-sh-setup.sh: use rev-parse --git-path to get $GIT_DIR/objects Nguyễn Thái Ngọc Duy
2014-09-28  1:22       ` [PATCH v3 13/32] *.sh: avoid hardcoding $GIT_DIR/hooks/ Nguyễn Thái Ngọc Duy
2014-09-28  1:22       ` [PATCH v3 14/32] git-stash: avoid hardcoding $GIT_DIR/logs/ Nguyễn Thái Ngọc Duy
2014-09-28  1:22       ` [PATCH v3 15/32] setup.c: convert is_git_directory() to use strbuf Nguyễn Thái Ngọc Duy
2014-09-28  1:22       ` [PATCH v3 16/32] setup.c: detect $GIT_COMMON_DIR in is_git_directory() Nguyễn Thái Ngọc Duy
2014-09-28  1:22       ` [PATCH v3 17/32] setup.c: convert check_repository_format_gently to use strbuf Nguyễn Thái Ngọc Duy
2014-09-28  1:22       ` [PATCH v3 18/32] setup.c: detect $GIT_COMMON_DIR check_repository_format_gently() Nguyễn Thái Ngọc Duy
2014-09-28  1:22       ` [PATCH v3 19/32] setup.c: support multi-checkout repo setup Nguyễn Thái Ngọc Duy
2014-10-01 16:19         ` Johannes Sixt
2014-09-28  1:22       ` [PATCH v3 20/32] wrapper.c: wrapper to open a file, fprintf then close Nguyễn Thái Ngọc Duy
2014-09-28  1:22       ` [PATCH v3 21/32] use new wrapper write_file() for simple file writing Nguyễn Thái Ngọc Duy
2014-09-28  1:22       ` [PATCH v3 22/32] checkout: support checking out into a new working directory Nguyễn Thái Ngọc Duy
2014-09-28  1:22       ` [PATCH v3 23/32] prune: strategies for linked checkouts Nguyễn Thái Ngọc Duy
2014-10-01 16:20         ` Johannes Sixt
2014-09-28  1:22       ` [PATCH v3 24/32] checkout: reject if the branch is already checked out elsewhere Nguyễn Thái Ngọc Duy
2014-09-28  1:22       ` [PATCH v3 25/32] checkout: clean up half-prepared directories in --to mode Nguyễn Thái Ngọc Duy
2014-09-28  1:22       ` [PATCH v3 26/32] gc: style change -- no SP before closing parenthesis Nguyễn Thái Ngọc Duy
2014-09-28  1:22       ` [PATCH v3 27/32] gc: factor out gc.pruneexpire parsing code Nguyễn Thái Ngọc Duy
2014-09-28  1:22       ` [PATCH v3 28/32] gc: support prune --worktrees Nguyễn Thái Ngọc Duy
2014-09-28  1:22       ` [PATCH v3 29/32] count-objects: report unused files in $GIT_DIR/worktrees/ Nguyễn Thái Ngọc Duy
2014-09-28  1:22       ` [PATCH v3 30/32] git_path(): keep "info/sparse-checkout" per work-tree Nguyễn Thái Ngọc Duy
2014-09-28  1:22       ` [PATCH v3 31/32] checkout: don't require a work tree when checking out into a new one Nguyễn Thái Ngọc Duy
2014-09-28  1:22       ` [PATCH v3 32/32] t2025: add a test to make sure grafts is working from a linked checkout Nguyễn Thái Ngọc Duy
2014-09-28  6:35       ` [PATCH v3 00/32] nd/multiple-work-trees Junio C Hamano

Code repositories for project(s) associated with this public inbox

	https://80x24.org/mirrors/git.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).