Subject: [PATCH 2/2 nd/init-gitdir] init, clone: support --separate-git-dir for .git file

From: Nguyễn Thái Ngọc Duy <[email-masked]>
Date: Sat, 19 Mar 2011 22:16:56 +0700

--separate-git-dir tells git to create git dir at the specified
location, instead of where it is supposed to be. A .git file that
points to that location will be put in place so that it appears normal
to repo discovery process.

Signed-off-by: Nguyễn Thái Ngọc Duy <[email-masked]>
---
  - rebased on top of master because it uses real_path().
  - implemented repo move if "git init --separate-git-dir" is run on
    an existing repo.
  - docs
  - tests

 Documentation/git-clone.txt |   10 ++++++
 Documentation/git-init.txt  |   17 +++++++++-
 builtin/clone.c             |    8 ++++-
 builtin/init-db.c           |   68 +++++++++++++++++++++++++++++++++++++++++--
 cache.h                     |    1 +
 t/t0001-init.sh             |   46 +++++++++++++++++++++++++++++
 t/t5601-clone.sh            |   13 ++++++++
 7 files changed, 157 insertions(+), 6 deletions(-)

diff --git a/Documentation/git-clone.txt b/Documentation/git-clone.txt
index 8577480..86eb4c9 100644
--- a/Documentation/git-clone.txt
+++ b/Documentation/git-clone.txt
@@ -12,6 +12,7 @@ SYNOPSIS
 'git clone' [--template=<template_directory>]
 	  [-l] [-s] [--no-hardlinks] [-q] [-n] [--bare] [--mirror]
 	  [-o <name>] [-b <name>] [-u <upload-pack>] [--reference <repository>]
+	  [--separate-git-dir|-L <git dir>]
 	  [--depth <depth>] [--recursive|--recurse-submodules] [--] <repository>
 	  [<directory>]
 
@@ -176,6 +177,15 @@ objects from the source repository into a pack in the cloned repository.
 	repository does not have a worktree/checkout (i.e. if any of
 	`--no-checkout`/`-n`, `--bare`, or `--mirror` is given)
 
+-L=<git dir>::
+--separate-git-dir=<git dir>::
+	Instead of placing the cloned repository where it is supposed
+	to be, place the cloned repository at the specified directory,
+	then make a filesytem-agnostic git symbolic link to there.
+	The result is git repository can be separated from working
+	tree.
+
+
 <repository>::
 	The (possibly remote) repository to clone from.  See the
 	<<URLS,URLS>> section below for more information on specifying
diff --git a/Documentation/git-init.txt b/Documentation/git-init.txt
index 0a4a20e..58cd011 100644
--- a/Documentation/git-init.txt
+++ b/Documentation/git-init.txt
@@ -8,7 +8,9 @@ git-init - Create an empty git repository or reinitialize an existing one
 
 SYNOPSIS
 --------
-'git init' [-q | --quiet] [--bare] [--template=<template_directory>] [--shared[=<permissions>]] [directory]
+'git init' [-q | --quiet] [--bare] [--template=<template_directory>]
+	  [--separate-git-dir|-L <git dir>]
+	  [--shared[=<permissions>]] [directory]

 DESCRIPTION
@@ -29,7 +31,8 @@ directory is used.
 
 Running 'git init' in an existing repository is safe. It will not
 overwrite things that are already there. The primary reason for
-rerunning 'git init' is to pick up newly added templates.
+rerunning 'git init' is to pick up newly added templates (or to move
+the repository to another place if --separate-git-dir is given).
 
 OPTIONS
 -------
@@ -51,6 +54,16 @@ current working directory.
 Specify the directory from which templates will be used.  (See the "TEMPLATE
 DIRECTORY" section below.)
 
+-L=<git dir>::
+--separate-git-dir=<git dir>::
+
+Instead of initializing the repository where it is supposed to be,
+place a filesytem-agnostic git symbolic link there, pointing to the
+specified git path, and initialize a git repository at the path. The
+result is git repository can be separated from working tree. If this
+is reinitialization, the repository will be moved to the specified
+path.
+
 --shared[=(false|true|umask|group|all|world|everybody|0xxx)]::
 
 Specify that the git repository is to be shared amongst several users.  This
diff --git a/builtin/clone.c b/builtin/clone.c
index 404f589..097beca 100644
--- a/builtin/clone.c
+++ b/builtin/clone.c
@@ -42,6 +42,7 @@ static int option_local, option_no_hardlinks, option_shared, option_recursive;
 static char *option_template, *option_reference, *option_depth;
 static char *option_origin = NULL;
 static char *option_branch = NULL;
+static const char *real_git_dir;
 static char *option_upload_pack = "git-upload-pack";
 static int option_verbosity;
 static int option_progress;
@@ -80,6 +81,8 @@ static struct option builtin_clone_options[] = {
 		   "path to git-upload-pack on the remote"),
 	OPT_STRING(0, "depth", &option_depth, "depth",
 		    "create a shallow clone of that depth"),
+	OPT_STRING('L', "separate-git-dir", &real_git_dir, "gitdir",
+		   "separate git dir from working tree"),
 
 	OPT_END()
 };
@@ -466,7 +469,10 @@ int cmd_clone(int argc, const char **argv, const char *prefix)
 
 	if (safe_create_leading_directories_const(git_dir) < 0)
 		die("could not create leading directories of '%s'", git_dir);
-	set_git_dir(real_path(git_dir));
+
+	set_git_dir_init(git_dir, real_git_dir, 0);
+	if (real_git_dir)
+		git_dir = real_git_dir;
 
 	if (0 <= option_verbosity)
 		printf("Cloning into %s%s...\n",
diff --git a/builtin/init-db.c b/builtin/init-db.c
index 8f5cfd7..8879399 100644
--- a/builtin/init-db.c
+++ b/builtin/init-db.c
@@ -21,6 +21,7 @@
 static int init_is_bare_repository = 0;
 static int init_shared_repository = -1;
 static const char *init_db_template_dir;
+static const char *git_link;
 
 static void safe_create_dir(const char *dir, int share)
 {
@@ -311,11 +312,67 @@ static void create_object_directory(void)
 	free(path);
 }
 
+int set_git_dir_init(const char *git_dir, const char *real_git_dir,
+		     int exist_ok)
+{
+	if (real_git_dir) {
+		struct stat st;
+
+		if (!exist_ok && !stat(git_dir, &st))
+			die("%s already exists", git_dir);
+
+		if (!exist_ok && !stat(real_git_dir, &st))
+			die("%s already exists", real_git_dir);
+
+		/*
+		 * make sure symlinks are resolved because we'll be
+		 * moving the target repo later on in separate_git_dir()
+		 */
+		git_link = xstrdup(real_path(git_dir));
+	}
+	else {
+		real_git_dir = real_path(git_dir);
+		git_link = NULL;
+	}
+	set_git_dir(real_path(real_git_dir));
+	return 0;
+}
+
+static void separate_git_dir(const char *git_dir)
+{
+	struct stat st;
+	FILE *fp;
+
+	if (!stat(git_link, &st)) {
+		const char *src;
+
+		if (S_ISREG(st.st_mode))
+			src = read_gitfile_gently(git_link);
+		else if (S_ISDIR(st.st_mode))
+			src = git_link;
+		else
+			die("unable to handle file type %d", st.st_mode);
+
+		if (rename(src, 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);
+}
+
 int init_db(const char *template_dir, unsigned int flags)
 {
 	int reinit;
+	const char *git_dir = get_git_dir();
 
-	safe_create_dir(get_git_dir(), 0);
+	if (git_link)
+		separate_git_dir(git_dir);
+
+	safe_create_dir(git_dir, 0);
 
 	init_is_bare_repository = is_bare_repository();
 
@@ -352,7 +409,6 @@ int init_db(const char *template_dir, unsigned int flags)
 	}
 
 	if (!(flags & INIT_DB_QUIET)) {
-		const char *git_dir = get_git_dir();
 		int len = strlen(git_dir);
 		printf("%s%s Git repository in %s%s\n",
 		       reinit ? "Reinitialized existing" : "Initialized empty",
@@ -414,6 +470,7 @@ static const char *const init_db_usage[] = {
 int cmd_init_db(int argc, const char **argv, const char *prefix)
 {
 	const char *git_dir;
+	const char *real_git_dir = NULL;
 	const char *work_tree;
 	const char *template_dir = NULL;
 	unsigned int flags = 0;
@@ -427,11 +484,16 @@ int cmd_init_db(int argc, const char **argv, const char *prefix)
 			"specify that the git repository is to be shared amongst several users",
 			PARSE_OPT_OPTARG | PARSE_OPT_NONEG, shared_callback, 0},
 		OPT_BIT('q', "quiet", &flags, "be quiet", INIT_DB_QUIET),
+		OPT_STRING('L', "separate-git-dir", &real_git_dir, "gitdir",
+			   "separate git dir from working tree"),
 		OPT_END()
 	};
 
 	argc = parse_options(argc, argv, prefix, init_db_options, init_db_usage, 0);
 
+	if (real_git_dir && !is_absolute_path(real_git_dir))
+		real_git_dir = xstrdup(real_path(real_git_dir));
+
 	if (argc == 1) {
 		int mkdir_tried = 0;
 	retry:
@@ -522,7 +584,7 @@ int cmd_init_db(int argc, const char **argv, const char *prefix)
 			set_git_work_tree(real_path(work_tree));
 	}
 
-	set_git_dir(real_path(git_dir));
+	set_git_dir_init(git_dir, real_git_dir, 1);
 
 	return init_db(template_dir, flags);
 }
diff --git a/cache.h b/cache.h
index a99fd56..0b99487 100644
--- a/cache.h
+++ b/cache.h
@@ -436,6 +436,7 @@ extern void verify_non_filename(const char *prefix, const char *name);
 
 #define INIT_DB_QUIET 0x0001
 
+extern int set_git_dir_init(const char *git_dir, const char *real_git_dir, int);
 extern int init_db(const char *template_dir, unsigned int flags);
 
 #define alloc_nr(x) (((x)+16)*3/2)
diff --git a/t/t0001-init.sh b/t/t0001-init.sh
index f684993..b2e6919 100755
--- a/t/t0001-init.sh
+++ b/t/t0001-init.sh
@@ -374,4 +374,50 @@ test_expect_success 'init prefers command line to GIT_DIR' '
 	! test -d otherdir/refs
 '
 
+test_expect_success 'init with separate gitdir' '
+	rm -rf newdir &&
+	git init --separate-git-dir realgitdir newdir &&
+	echo "gitdir: `pwd`/realgitdir" >expected &&
+	test_cmp expected newdir/.git &&
+	test -d realgitdir/refs
+'
+
+test_expect_success 're-init to update git link' '
+	(
+	cd newdir &&
+	git init --separate-git-dir ../surrealgitdir
+	) &&
+	echo "gitdir: `pwd`/surrealgitdir" >expected &&
+	test_cmp expected newdir/.git &&
+	test -d surrealgitdir/refs &&
+	! test -d realgitdir/refs
+'
+
+test_expect_success 're-init to move gitdir' '
+	rm -rf newdir realgitdir surrealgitdir &&
+	git init newdir &&
+	(
+	cd newdir &&
+	git init --separate-git-dir ../realgitdir
+	) &&
+	echo "gitdir: `pwd`/realgitdir" >expected &&
+	test_cmp expected newdir/.git &&
+	test -d realgitdir/refs
+'
+
+test_expect_success 're-init to move gitdir symlink' '
+	rm -rf newdir realgitdir &&
+	git init newdir &&
+	(
+	cd newdir &&
+	mv .git here &&
+	ln -s here .git &&
+	git init -L ../realgitdir
+	) &&
+	echo "gitdir: `pwd`/realgitdir" >expected &&
+	test_cmp expected newdir/.git &&
+	test -d realgitdir/refs &&
+	! test -d newdir/here
+'
+
 test_done
diff --git a/t/t5601-clone.sh b/t/t5601-clone.sh
index 987e0c8..c467b67 100755
--- a/t/t5601-clone.sh
+++ b/t/t5601-clone.sh
@@ -192,4 +192,17 @@ test_expect_success 'do not respect url-encoding of non-url path' '
 	git clone x+y xy-regular
 '
 
+test_expect_success 'clone separate gitdir' '
+	rm -rf dst &&
+	git clone --separate-git-dir realgitdir src dst &&
+	echo "gitdir: `pwd`/realgitdir" >expected &&
+	test_cmp expected dst/.git &&
+	test -d realgitdir/refs
+'
+
+test_expect_success 'clone separate gitdir where target already exists' '
+	rm -rf dst &&
+	test_must_fail git clone --separate-git-dir realgitdir src dst
+'
+
 test_done
-- 
1.7.4.74.g639db