Subject: [PATCH v3 6/6] worktree add: switch to worktree version 1

From: Nguyễn Thái Ngọc Duy <[email-masked]>
Date: Tue, 26 Jan 2016 18:44:45 +0700

The most obvious use case is, "git worktree add" creates the first
linked worktree. In this case, we should be able to move to latest
worktree version. The following happens:

 - common/config is created with extensions.worktree 1 and
   core.repositoryformatversion 1

 - all config keys except a few per-worktree are moved to
   common/config

 - per-worktree keys stay with the main worktree's config file

 - the main worktree config file also has worktree version explicitly
   set to 1. This is to prevent older Git binaries from reading it.

What if the repo already has another linked worktree and the user
wants to stay at version 0, maybe because multiple git binaries can
access this repo? "worktree add --version=0" can be used, but it's
really not recommended to stay at lower (and buggy) version.

A note about core.bare staying per-worktree. On the surface it does
not make sense for core.bare to be worktree specific. It's made so in
order to "grow" new worktrees from a bare repo. In these new linked
worktrees, core.bare will be hidden away and worktree-related commands
won't complain about bare repository.

Signed-off-by: Nguyễn Thái Ngọc Duy <[email-masked]>
---
 Documentation/git-worktree.txt |  9 +++-
 builtin/worktree.c             | 95 ++++++++++++++++++++++++++++++++++++++++++
 t/t2028-worktree-config.sh     | 47 ++++++++++++++++++++-
 3 files changed, 148 insertions(+), 3 deletions(-)

diff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt
index 6082d4d..0d7d523 100644
--- a/Documentation/git-worktree.txt
+++ b/Documentation/git-worktree.txt
@@ -9,7 +9,7 @@ git-worktree - Manage multiple working trees
 SYNOPSIS
 --------
 [verse]
-'git worktree add' [-f] [--detach] [-b <new-branch>] <path> [<branch>]
+'git worktree add' [-f] [--detach] [-b <new-branch>] [--version=<N>] <path> [<branch>]
 'git worktree prune' [-n] [-v] [--expire <expire>]
 'git worktree list' [--porcelain]
 
@@ -89,6 +89,13 @@ OPTIONS
 	With `add`, detach HEAD in the new working tree. See "DETACHED HEAD"
 	in linkgit:git-checkout[1].
 
+--version=<N>::
+	By default when a new working directory is added, worktree
+	layout is automatically migrated to latest version. This
+	option can be used to specify only migrate to the specified
+	version, or no migrate at all if it's already current worktree
+	version.
+
 -n::
 --dry-run::
 	With `prune`, do not remove anything; just report what it would
diff --git a/builtin/worktree.c b/builtin/worktree.c
index 475b958..551fe37 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -140,6 +140,7 @@ static char *junk_work_tree;
 static char *junk_git_dir;
 static int is_junk;
 static pid_t junk_pid;
+static int target_version = 1;
 
 static void remove_junk(void)
 {
@@ -184,6 +185,93 @@ static const char *worktree_basename(const char *path, int *olen)
 	return name;
 }
 
+struct key_data {
+	const char *key;
+	char *value;
+};
+
+static int get_one_key(const char *key, const char *value, void *cb)
+{
+	struct key_data *kd = cb;
+
+	if (!strcmp(key, kd->key))
+		kd->value = xstrdup(value);
+
+	return 0;
+}
+
+static char *get_key(const char *file, const char *key)
+{
+	struct key_data kd;
+
+	kd.key = key;
+	kd.value = NULL;
+	if (git_config_from_file(get_one_key, file, &kd))
+		return NULL;
+	return kd.value;
+}
+
+static void migrate_worktree_layout(void)
+{
+	const char *per_wortree_keys[] = {
+		"core.bare",
+		"core.ignorestat",
+		"core.sparsecheckout",
+		"core.worktree",
+		NULL
+	};
+	struct strbuf sb = STRBUF_INIT;
+	const char **key_p;
+
+	switch (repository_format_worktree_version) {
+	case 0:
+		strbuf_addf(&sb, "%s/common", get_git_common_dir());
+		if (mkdir_in_gitdir(sb.buf))
+			die_errno(_("failed to create directory %s"), sb.buf);
+		if (repository_format_version < 1 &&
+		    git_config_set("core.repositoryformatversion", "1"))
+			die(_("failed to set core.repositoryformatversion to one"));
+		if (git_config_set("extensions.worktree", "1"))
+			die(_("failed to set extensions.worktree to one"));
+		strbuf_addstr(&sb, "/config");
+		if (rename(git_path("config"), sb.buf))
+			die_errno(_("failed to set move config file to %s"),
+				  sb.buf);
+		for (key_p = per_wortree_keys; *key_p; key_p++) {
+			const char *key = *key_p;
+			char *value = get_key(sb.buf, key);
+
+			if (value) {
+				if (git_config_set(key, value))
+					die(_("failed to keep %s in main worktree's config file"), key);
+				if (git_config_set_in_file(sb.buf, key, NULL))
+					die(_("failed to delete %s in shared config file"), key);
+				free(value);
+			}
+		}
+
+		/*
+		 * we're still in version 0 in this process, this will
+		 * create a new file $GIT_COMMON_DIR/config with only
+		 * one key, extensions.worktree. This will force old
+		 * git binaries that do not understand v1 to bail out.
+		 */
+		if (repository_format_version < 1 &&
+		    git_config_set("core.repositoryformatversion", "1"))
+			die(_("failed to set core.repositoryformatversion to one"));
+		if (git_config_set("extensions.worktree", "1"))
+			die(_("failed to set extensions.worktree to one"));
+
+		repository_format_worktree_version = 1;
+		break;
+	case 1:
+		break;
+	default:
+		die(_("unsupported worktree format version %d"),
+		    repository_format_worktree_version);
+	}
+}
+
 static int add_worktree(const char *path, const char *refname,
 			const struct add_opts *opts)
 {
@@ -297,6 +385,9 @@ static int add_worktree(const char *path, const char *refname,
 		free(junk_git_dir);
 		junk_work_tree = NULL;
 		junk_git_dir = NULL;
+
+		while (repository_format_worktree_version < target_version)
+			migrate_worktree_layout();
 	}
 done:
 	strbuf_reset(&sb);
@@ -322,6 +413,8 @@ static int add(int ac, const char **av, const char *prefix)
 		OPT_STRING('B', NULL, &new_branch_force, N_("branch"),
 			   N_("create or reset a branch")),
 		OPT_BOOL(0, "detach", &opts.detach, N_("detach HEAD at named commit")),
+		OPT_INTEGER(0, "version", &target_version,
+			   N_("stay at this worktree version")),
 		OPT_END()
 	};
 
@@ -331,6 +424,8 @@ static int add(int ac, const char **av, const char *prefix)
 		die(_("-b, -B, and --detach are mutually exclusive"));
 	if (ac < 1 || ac > 2)
 		usage_with_options(worktree_usage, options);
+	if (target_version < 0 || target_version > 1)
+		die(_("invalid worktree version %d"), target_version);
 
 	path = prefix ? prefix_filename(prefix, strlen(prefix), av[0]) : av[0];
 	branch = ac < 2 ? "HEAD" : av[1];
diff --git a/t/t2028-worktree-config.sh b/t/t2028-worktree-config.sh
index d11b2ce..0d4cb8e 100755
--- a/t/t2028-worktree-config.sh
+++ b/t/t2028-worktree-config.sh
@@ -6,8 +6,8 @@ test_description="config file in multi worktree"
 
 test_expect_success 'setup' '
 	test_commit start &&
-	git worktree add wt1 &&
-	git worktree add wt2
+	git worktree add --version=0 wt1 &&
+	git worktree add --version=0 wt2
 '
 
 test_expect_success 'main config is shared in version 0' '
@@ -58,4 +58,47 @@ test_expect_success 'config --repo on v1' '
 	test_cmp expected actual
 '
 
+test_expect_success 'prepare worktree v0' '
+	test_create_repo repo-v0 &&
+	(
+		cd repo-v0 &&
+		test_commit v0 &&
+		git config core.sparsecheckout true &&
+		git config core.ignorestat true &&
+		git config core.worktree "$TEST_DIRECTORY" &&
+		git config share.key value
+	)
+'
+
+test_expect_success 'migrate v0 to v1' '
+	git -C repo-v0 worktree add --version=1 wt
+'
+
+test_expect_success 'after migration: main wortree has extensions.worktree' '
+	test "`git -C repo-v0 config core.repositoryformatversion`" = 1 &&
+	test "`git -C repo-v0 config extensions.worktree`" = 1
+'
+
+test_expect_success 'after migration: linked wortree has extensions.worktree' '
+	test "`git -C repo-v0/wt config core.repositoryformatversion`" = 1 &&
+	test "`git -C repo-v0/wt config extensions.worktree`" = 1
+'
+
+test_expect_success 'after migration: main wortree keeps per-worktree vars' '
+	test "`git -C repo-v0 config core.sparsecheckout`" = true &&
+	test "`git -C repo-v0 config core.ignorestat`" = true &&
+	test "`git -C repo-v0 config core.worktree`" = "$TEST_DIRECTORY"
+'
+
+test_expect_success 'after migration: linked wortree has no per-worktree vars' '
+	test_must_fail git -C repo-v0/wt config core.sparsecheckout &&
+	test_must_fail git -C repo-v0/wt config core.ignorestat &&
+	test_must_fail git -C repo-v0/wt config core.worktree
+'
+
+test_expect_success 'after migration: shared vars are shared' '
+	test "`git -C repo-v0 config share.key`" = value &&
+	test "`git -C repo-v0/wt config share.key`" = value
+'
+
 test_done
-- 
2.7.0.288.g1d8ad15