Subject: '.git file' alternative, native (cross-platform) workdir support.

From: Marius Storm-Olsen <[email-masked]>
Date: Fri, 29 Feb 2008 13:27:53 +0100

Hi guys,

I just caught a glimpse of the '.git file' efforts, as a file for 
redirection to a real repository.

As far as I can tell, the reason for adding the support is to in the 
end provide a cross-platform way of supporting workdirs. (If this is 
not the [main] point, please point me to the thread describing the 
real reason, I couldn't find it.)

However, wouldn't simply redirecting everything into a real repo then 
create problems with shared index file and more? A problem which could 
be tacled by file suffixes or other methods, I'm sure, but which would 
require even more patches to achieve the goal.

I was actually thinking about the whole workdir thing the other day, 
since I mainly work on Windows, and constantly green of envy of the 
'mainly Linux' guys. I figured, why not just add support for file 
redirection in
     char *git_path(const char *fmt, ...)
which we use all over the place? That surely is easy and 
cross-platform :-)

Attached you'll find a patch which will achieve 'native workdir' 
support on all platforms, independent of underlying file system. And 
if you ignore the code added to builtin-init-db.c & 
builtin-rev-parse.c for minimal usage support, it's a mere 104 lines 
touched. The patch should apply cleanly on both git's next branch, and 
Hannes' j6t master branch (the mingw port).

Please note that the patch is not meant for end-user consumption, nor 
does it follow Git coding standards. It's just meant as a proof of 
concept, and as a means of discussion.
Also note that this way of supporting workdirs suffers from the same 
'flaws' the current git-new-workdir has (locking of repo etc).

If people want, I can work with it, and implement this properly (with 
its own builtin-workdir, test cases etc). As I see it, the '.git file' 
concept still has some way to go. Maybe the '.git file' concept is the 
best way in the end, but that this way is an 'ok' temporary way of 
doing it?

^shrug^ - Input please!

To try out the patch, apply it on git 'next' branch, then in some 
random directory:
     git init --workdir-for=<abs-path to repo>
     git reset --hard
     .. hack away as normal ..
(I know I know, it just a quick hack to let ppl play with it.)

PS. Sorry for the patch being an attachment, I'm a Thunderbird slave. 
(Someone really needs to make a Thunderbird addon for sending Git 
patches ;-)

--
.marius
From a38bf926edcba780445ae2a67eb8fe33b266a94f Mon Sep 17 00:00:00 2001
From: Marius Storm-Olsen <[email-masked]>
Date: Thu, 28 Feb 2008 13:22:42 +0100
Subject: [PATCH] Add cross-platform workdir support

---
 builtin-init-db.c   |   53 ++++++++++++++++++++++++++++++++++++++-----
 builtin-rev-parse.c |   19 +++++++++++++++
 cache.h             |    2 +
 environment.c       |   63 +++++++++++++++++++++++++++++++++++++++++++++-----
 path.c              |   37 +++++++++++++++++++++++++++--
 setup.c             |    2 +-
 6 files changed, 159 insertions(+), 17 deletions(-)

diff --git a/builtin-init-db.c b/builtin-init-db.c
index 79eaf8d..94622f9 100644
--- a/builtin-init-db.c
+++ b/builtin-init-db.c
@@ -310,7 +310,7 @@ static void guess_repository_type(const char *git_dir)
 }
 
 static const char init_db_usage[] =
-"git-init [-q | --quiet] [--template=<template-directory>] [--shared]";
+"git-init [-q | --quiet] [--template=<template-directory>] [--shared] [--workdir-for=<repository>]";
 
 /*
  * If you want to, you can share the DB area with any number of branches.
@@ -323,8 +323,9 @@ int cmd_init_db(int argc, const char **argv, const char *prefix)
 	const char *git_dir;
 	const char *sha1_dir;
 	const char *template_dir = NULL;
+	char *repo_dir = NULL;
 	char *path;
-	int len, i, reinit;
+	int len, i, reinit, workdir_fd;
 	int quiet = 0;
 
 	for (i = 1; i < argc; i++, argv++) {
@@ -337,7 +338,10 @@ int cmd_init_db(int argc, const char **argv, const char *prefix)
 			shared_repository = git_config_perm("arg", arg+9);
 		else if (!strcmp(arg, "-q") || !strcmp(arg, "--quiet"))
 		        quiet = 1;
-		else
+		else if (!prefixcmp(arg, "--workdir-for=")) {
+			repo_dir = xmalloc(PATH_MAX);
+			strcpy(repo_dir, arg+14);
+		} else
 			usage(init_db_usage);
 	}
 
@@ -406,11 +410,48 @@ int cmd_init_db(int argc, const char **argv, const char *prefix)
 		git_config_set("receive.denyNonFastforwards", "true");
 	}
 
-	if (!quiet)
-		printf("%s%s Git repository in %s/\n",
+	if (repo_dir) {
+		len = strlen(git_dir);
+		memcpy(path, git_dir, len);
+		strcpy(path+len, "/workdir_for");
+		len = strlen(repo_dir);
+#ifdef __MINGW32__
+		for(i = 0; i < len; ++i) {
+			if (repo_dir[i] == '\\')
+				repo_dir[i] = '/';
+		}
+#endif
+		if (!is_git_directory(repo_dir)) {
+			strcpy(repo_dir+len, "/.git");
+			if (!is_git_directory(repo_dir))
+				die("specified --workdir-for path is not a git repository (%s)", repo_dir);
+		}
+		workdir_fd = open(path, O_CREAT | O_EXCL | O_WRONLY, 0666);
+		if (!workdir_fd)
+			die("couldn't create workdir redirection file: %s", strerror(errno));
+		write_or_die(workdir_fd, repo_dir, strlen(repo_dir));
+		if (close(workdir_fd) < 0)
+			die("closing file %s: %s", path, strerror(errno));
+
+		/* Copy current HEAD */
+		strcpy(repo_dir+len, "/HEAD");
+		len = strlen(git_dir);
+		strcpy(path+len, "/HEAD");
+		copy_file(path, repo_dir, 0);
+
+		/* Should force a checkout of the current HEAD. Oh well.. */
+	}
+
+	if (!quiet) {
+		printf("%s%s %s repository in %s/\n",
 		       reinit ? "Reinitialized existing" : "Initialized empty",
 		       shared_repository ? " shared" : "",
+		       repo_dir ? "workdir" : "Git",
 		       git_dir);
-
+		if (repo_dir)
+			printf("You now either need to do a 'git reset --hard', or force a switch to another branch.\n");
+	}
+	if (repo_dir)
+		free(repo_dir);
 	return 0;
 }
diff --git a/builtin-rev-parse.c b/builtin-rev-parse.c
index 90dbb9d..d83ba59 100644
--- a/builtin-rev-parse.c
+++ b/builtin-rev-parse.c
@@ -511,6 +511,25 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)
 				printf("%s/.git\n", cwd);
 				continue;
 			}
+			if (!strcmp(arg, "--git-redirection-dir")) {
+				const char *redir = get_git_redirection_dir();
+				printf("%s\n", redir ? redir
+						: "");
+				continue;
+			}
+			if (!strcmp(arg, "--git-object-dir")) {
+				printf("%s\n", get_object_directory());
+				continue;
+			}
+			if (!strcmp(arg, "--git-refs-dir")) {
+				printf("%s\n", get_refs_directory());
+				continue;
+			}
+			if (!strcmp(arg, "--uses-git-redirection")) {
+				printf("%s\n", get_git_redirection_dir() ? "true"
+						: "false");
+				continue;
+			}
 			if (!strcmp(arg, "--is-inside-git-dir")) {
 				printf("%s\n", is_inside_git_dir() ? "true"
 						: "false");
diff --git a/cache.h b/cache.h
index 660ea04..e8b6879 100644
--- a/cache.h
+++ b/cache.h
@@ -301,6 +301,7 @@ static inline enum object_type object_type(unsigned int mode)
 extern int is_bare_repository_cfg;
 extern int is_bare_repository(void);
 extern int is_inside_git_dir(void);
+extern int is_git_directory(const char *suspect);
 extern char *git_work_tree_cfg;
 extern int is_inside_work_tree(void);
 extern const char *get_git_dir(void);
@@ -310,6 +311,7 @@ extern char *get_index_file(void);
 extern char *get_graft_file(void);
 extern int set_git_dir(const char *path);
 extern const char *get_git_work_tree(void);
+extern const char *get_git_redirection_dir(void);
 
 #define ALTERNATE_DB_ENVIRONMENT "GIT_ALTERNATE_OBJECT_DIRECTORIES"
 
diff --git a/environment.c b/environment.c
index 6739a3f..74391de 100644
--- a/environment.c
+++ b/environment.c
@@ -44,7 +44,7 @@ char *git_work_tree_cfg;
 static const char *work_tree;
 
 static const char *git_dir;
-static char *git_object_dir, *git_index_file, *git_refs_dir, *git_graft_file;
+static char *git_object_dir, *git_index_file, *git_refs_dir, *git_graft_file, *git_workdir_redirection;
 
 static void setup_git_env(void)
 {
@@ -53,15 +53,12 @@ static void setup_git_env(void)
 		git_dir = DEFAULT_GIT_DIR_ENVIRONMENT;
 	git_object_dir = getenv(DB_ENVIRONMENT);
 	if (!git_object_dir) {
-		git_object_dir = xmalloc(strlen(git_dir) + 9);
-		sprintf(git_object_dir, "%s/objects", git_dir);
+		git_object_dir = xstrdup(git_path("objects"));
 	}
-	git_refs_dir = xmalloc(strlen(git_dir) + 6);
-	sprintf(git_refs_dir, "%s/refs", git_dir);
+	git_refs_dir = xstrdup(git_path("refs"));
 	git_index_file = getenv(INDEX_ENVIRONMENT);
 	if (!git_index_file) {
-		git_index_file = xmalloc(strlen(git_dir) + 7);
-		sprintf(git_index_file, "%s/index", git_dir);
+		git_index_file = xstrdup(git_path("index"));
 	}
 	git_graft_file = getenv(GRAFT_ENVIRONMENT);
 	if (!git_graft_file)
@@ -101,6 +98,58 @@ const char *get_git_work_tree(void)
 	return work_tree;
 }
 
+static char *workdir_for = "/workdir_for";
+const char *get_git_redirection_dir(void)
+{
+	static int checked_for_redirection = 0;
+	if (!checked_for_redirection && git_dir) {
+		char *buf;
+		struct stat st;
+		int fd;
+		size_t len;
+		int gitdirlen = strlen(git_dir);
+		int workdirlen = strlen(workdir_for);
+
+		checked_for_redirection = 1;
+
+		/* Check if we have a .git/workdir_for redirection file */
+		buf = xmalloc(gitdirlen + workdirlen + 1);
+		memcpy(buf, git_dir, gitdirlen);
+		memcpy(buf + gitdirlen, workdir_for, strlen(workdir_for));
+		buf[gitdirlen + workdirlen] = '\0';
+
+		if (stat(buf, &st) || !S_ISREG(st.st_mode)) {
+			free(buf);
+			return 0;
+		}
+
+		/* Read the 1st line of the file */
+		fd = open(buf, O_RDONLY);
+		if (fd < 0) {
+			fprintf(stderr, "Error opening %s: %s", buf, strerror(errno));
+			free(buf);
+			return 0;
+		}
+		buf = xrealloc(buf, st.st_size + 1);
+		len = read_in_full(fd, buf, st.st_size);
+		close(fd);
+		buf[len] = '\0';
+
+		if (len != st.st_size)
+			fprintf(stderr, "Error reading .git%s", workdir_for);
+
+		if (len < 4 || !is_git_directory(buf)) {
+			fprintf(stderr, ".git%s pointing to a non-proper git repo path '%s'\n", workdir_for, buf);
+			free(buf);
+			return 0;
+		}
+
+		git_workdir_redirection = buf;
+		/* buf not freed, since redirected_gitpath now points to it */
+	}
+	return git_workdir_redirection;
+}
+
 char *get_object_directory(void)
 {
 	if (!git_object_dir)
diff --git a/path.c b/path.c
index 4260952..f6332d6 100644
--- a/path.c
+++ b/path.c
@@ -46,12 +46,28 @@ char *mkpath(const char *fmt, ...)
 	return cleanup_path(pathname);
 }
 
+static char *redirection_paths[] = { "config", "refs" ,"logs/refs", "objects", "info", "hooks", "packed-refs", "remotes", "rr-cache", 0 };
+static const char *get_redirected_dir(const char *req_path)
+{
+	int i = 0;
+	const char *redir = get_git_redirection_dir();
+	if (redir) {
+		do {
+			if (!memcmp(req_path, redirection_paths[i], strlen(redirection_paths[i]))) {
+				/* fprintf(stderr, "Looking for '%s', returning in '%s' instead of workdir/.git '%s'\n", req_path, redir, get_git_dir()); */
+				return redir;
+			}
+		} while(redirection_paths[++i]);
+	}
+	return 0;
+}
+
 char *git_path(const char *fmt, ...)
 {
 	const char *git_dir = get_git_dir();
 	char *pathname = get_pathname();
 	va_list args;
-	unsigned len;
+	unsigned len, newlen, rdlen, minlen;
 
 	len = strlen(git_dir);
 	if (len > PATH_MAX-100)
@@ -60,10 +76,25 @@ char *git_path(const char *fmt, ...)
 	if (len && git_dir[len-1] != '/')
 		pathname[len++] = '/';
 	va_start(args, fmt);
-	len += vsnprintf(pathname + len, PATH_MAX - len, fmt, args);
+	newlen = len + vsnprintf(pathname + len, PATH_MAX - len, fmt, args);
 	va_end(args);
-	if (len >= PATH_MAX)
+	if (newlen >= PATH_MAX)
 		return bad_path;
+
+	{ /* Redirect certain files when in a workdir */
+		const char *redir = get_redirected_dir(pathname + len);
+		if (redir) {
+			char *pathname_redir = get_pathname();
+			rdlen = strlen(redir);
+			memcpy(pathname_redir, redir, rdlen);
+			if (rdlen && redir[rdlen-1] != '/')
+				pathname_redir[rdlen++] = '/';
+			minlen = MIN(PATH_MAX-rdlen, strlen(pathname+len));
+			memcpy(pathname_redir+rdlen, pathname+len, minlen);
+			pathname_redir[rdlen + minlen] = '\0';
+			pathname = pathname_redir;
+		}
+	}
 	return cleanup_path(pathname);
 }
 
diff --git a/setup.c b/setup.c
index 89c81e5..2c507d5 100644
--- a/setup.c
+++ b/setup.c
@@ -221,7 +221,7 @@ const char **get_pathspec(const char *prefix, const char **pathspec)
  *    a proper "ref:", or a regular file HEAD that has a properly
  *    formatted sha1 object name.
  */
-static int is_git_directory(const char *suspect)
+int is_git_directory(const char *suspect)
 {
 	char path[PATH_MAX];
 	size_t len = strlen(suspect);
-- 
1.5.4.3.394.ga38b