Subject: Re: Commit ID in exported Tar Ball

From: René Scharfe <[email-masked]>
Date: Wed, 23 May 2007 00:26:58 +0200

[I'm quoting myself in full because I somehow sent my reply to everyone
but Shawn.  A patch can be found at the end.]

René Scharfe schrieb:
> Shawn O. Pearce schrieb:
>> Ren?? Scharfe <[email-masked]> wrote:
>>> Shawn O. Pearce schrieb:
>>>> git-describe is more human-friendly than a SHA-1...
>>> Yes, and the Makefile does even more than that: it adds a version
>>> file, a spec file and another version file for git-gui.
>>> 
>>> The first two are probably useful for most projects that actually
>>> do versioned releases.  We could have a simple parser that reads
>>> a template, replaces @@VERSION@@ with a git-describe output
>>> string and adds the result as a synthetic file to the archive.
>>> It's not exactly trivial -- e.g., how to specify git-describe
>>> options, template file and synthetic name, all in one command
>>> line parameter? -- but it's doable.
>> Maybe something just as simple as allowing the user to specify a 
>> shell script in-tree that we unpack and run for them?  That script 
>> prints to stdout the content of the file to include.
> 
> I doubt executing a shell script is simple. :-D  You'd possibly get 
> different results on different platforms (dare I mention Windows?).
> 
> The template system I mentioned would be a kind of scripting language
>  itself, but in this case we define its syntax and can guarantee 
> consistency everywhere git runs.  And since it would only have four 
> types of tokens (@@VERSION@@, @@COMMITID@@, @@@@ and string literals)
> it could be fast and simple.
> 
> We could implement it as a checkout converter, preferably one that is
> only applied by git-archive.  Then we'd rename git.spec.in to
> git.spec, assign the "specfile" attribute to it and let git-archive
> replace the string @@VERSION@@ with git-describe's output.
> git-checkout would not expand the special strings, so you can simply
> edit and version the file as you can do with git.spec.in now.
> Michael would have a file containing only @@COMMITID@@ to solve his
> original problem.  Make sense?
> 
>> So now we're also really talking about, what should git-archive do
>> for a subproject?  Sometimes you really do want to repackage and
>> redistribute the subproject as part of the superproject's tarball.
>> Sometimes you don't.  I think in the case of git.git and 
>> git-gui.git we want to include the subproject.  ;-)
> 
> Oh, yes, subprojects.  git-archive currently exports them as empty 
> directories.  Using tar's append command you could simply build the 
> project+subproject archive in the Makefile.  That wouldn't work well 
> with gitweb, though.  Perhaps a --include-subproject=<path> option is
>  needed?

OK, so here's a first shot at the mentioned parser.  It only understands
@@COMMITID@@ and @@@@, but it's easily extendible.  The internals of
git-describe would need to be converted to library functions, preferably
offering every piece of version info separately (see thread "[PATCH]
Make sure an autogenerated version has at least four parts" for why).

Before doing that, we should determine if this is the way to, though.

René

 Documentation/gitattributes.txt |   19 ++++++-
 archive-tar.c                   |    5 ++-
 archive-zip.c                   |    5 ++-
 cache.h                         |    1 +
 convert.c                       |  112 +++++++++++++++++++++++++++++++++++++++
 5 files changed, 139 insertions(+), 3 deletions(-)

diff --git a/Documentation/gitattributes.txt b/Documentation/gitattributes.txt
index d3ac9c7..84c414c 100644
--- a/Documentation/gitattributes.txt
+++ b/Documentation/gitattributes.txt
@@ -72,7 +72,7 @@ EFFECTS
 -------
 
 Certain operations by git can be influenced by assigning
-particular attributes to a path.  Currently, three operations
+particular attributes to a path.  Currently, four operations
 are attributes-aware.
 
 Checking-out and checking-in
@@ -374,6 +374,23 @@ frotz	unspecified
 ----------------------------------------------------------------

+Creating an archive
+~~~~~~~~~~~~~~~~~~~
+
+
+`specfile`
+^^^^^^^^^^
+
+If the attribute `specfile` is set for a file then git will expand
+several placeholders when adding this file to an archive.  The
+expansion depends on the availability of a commit ID, i.e. if
+`git-archive` has been given a tree instead of a commit or a tag
+then no replacement will be done.
+
+`@@COMMITID@@`:: is replaced by the commit hash.
+`@@@@`:: is replaced by `@@`.
+
+
 GIT
 ---
 Part of the gitlink:git[7] suite
diff --git a/archive-tar.c b/archive-tar.c
index 66fe3e3..eba24cb 100644
--- a/archive-tar.c
+++ b/archive-tar.c
@@ -17,6 +17,7 @@ static unsigned long offset;
 static time_t archive_time;
 static int tar_umask = 002;
 static int verbose;
+static const unsigned char *commit_sha1;
 
 /* writes out the whole block, but only if it is full */
 static void write_if_needed(void)
@@ -285,7 +286,8 @@ static int write_tar_entry(const unsigned char *sha1,
 		buffer = NULL;
 		size = 0;
 	} else {
-		buffer = convert_sha1_file(path.buf, sha1, mode, &type, &size);
+		buffer = sha1_file_to_archive(path.buf, sha1, mode, &type,
+		                              &size, commit_sha1);
 		if (!buffer)
 			die("cannot read %s", sha1_to_hex(sha1));
 	}
@@ -304,6 +306,7 @@ int write_tar_archive(struct archiver_args *args)
 
 	archive_time = args->time;
 	verbose = args->verbose;
+	commit_sha1 = args->commit_sha1;
 
 	if (args->commit_sha1)
 		write_global_extended_header(args->commit_sha1);
diff --git a/archive-zip.c b/archive-zip.c
index 444e162..93a5ab3 100644
--- a/archive-zip.c
+++ b/archive-zip.c
@@ -12,6 +12,7 @@
 static int verbose;
 static int zip_date;
 static int zip_time;
+static const unsigned char *commit_sha1;
 
 static unsigned char *zip_dir;
 static unsigned int zip_dir_size;
@@ -195,7 +196,8 @@ static int write_zip_entry(const unsigned char *sha1,
 		if (S_ISREG(mode) && zlib_compression_level != 0)
 			method = 8;
 		result = 0;
-		buffer = convert_sha1_file(path, sha1, mode, &type, &size);
+		buffer = sha1_file_to_archive(path, sha1, mode, &type, &size,
+		                              commit_sha1);
 		if (!buffer)
 			die("cannot read %s", sha1_to_hex(sha1));
 		crc = crc32(crc, buffer, size);
@@ -316,6 +318,7 @@ int write_zip_archive(struct archiver_args *args)
 	zip_dir = xmalloc(ZIP_DIRECTORY_MIN_SIZE);
 	zip_dir_size = ZIP_DIRECTORY_MIN_SIZE;
 	verbose = args->verbose;
+	commit_sha1 = args->commit_sha1;
 
 	if (args->base && plen > 0 && args->base[plen - 1] == '/') {
 		char *base = xstrdup(args->base);
diff --git a/cache.h b/cache.h
index cd875bc..0484904 100644
--- a/cache.h
+++ b/cache.h
@@ -550,6 +550,7 @@ extern void trace_argv_printf(const char **argv, int count, const char *format,
 extern char *convert_to_git(const char *path, const char *src, unsigned long *sizep);
 extern char *convert_to_working_tree(const char *path, const char *src, unsigned long *sizep);
 extern void *convert_sha1_file(const char *path, const unsigned char *sha1, unsigned int mode, enum object_type *type, unsigned long *size);
+extern void *sha1_file_to_archive(const char *path, const unsigned char *sha1, unsigned int mode, enum object_type *type, unsigned long *size, const unsigned char *commit_sha1);
 
 /* match-trees.c */
 void shift_tree(const unsigned char *, const unsigned char *, unsigned char *, int);
diff --git a/convert.c b/convert.c
index 4b26b1a..1cdaec5 100644
--- a/convert.c
+++ b/convert.c
@@ -1,6 +1,7 @@
 #include "cache.h"
 #include "attr.h"
 #include "run-command.h"
+#include "strbuf.h"
 
 /*
  * convert.c - convert a file when checking it out and checking it in.
@@ -667,3 +668,114 @@ void *convert_sha1_file(const char *path, const unsigned char *sha1,
 	}
 	return buffer;
 }
+
+static void strbuf_append(struct strbuf *sb, const void *s, size_t len)
+{
+	if (sb->alloc < sb->len + len) {
+		sb->alloc = (sb->len + len) * 3 / 2 + 16;
+		sb->buf = xrealloc(sb->buf, sb->alloc);
+	}
+	memcpy(sb->buf + sb->len, s, len);
+	sb->len += len;
+}
+
+static unsigned int match_keyword(const char *data, unsigned int datalen,
+                                  const char *keyword)
+{
+	unsigned int keylen = strlen(keyword);
+	if (keylen > datalen)
+		return 0;
+	if (memcmp(data, keyword, keylen))
+		return 0;
+	return keylen;
+}
+
+static void *convert_to_archive(const char *path, const void *src,
+                                unsigned long *sizep,
+                                const unsigned char *commit_sha1)
+{
+	static struct git_attr *attr_specfile;
+	struct git_attr_check check[1];
+	const char *p = src;
+	unsigned long srcsize = *sizep;
+	int at_signs = 0;
+	struct strbuf dst;
+	unsigned int match;
+	int replaced_something = 0;
+
+	if (!commit_sha1)
+		return NULL;
+
+        if (!attr_specfile)
+                attr_specfile = git_attr("specfile", 8);
+
+	check[0].attr = attr_specfile;
+	if (git_checkattr(path, ARRAY_SIZE(check), check))
+		return NULL;
+	if (!ATTR_TRUE(check[0].value))
+		return NULL;
+
+	dst.alloc = srcsize + 128;
+	dst.buf = xmalloc(dst.alloc);
+	dst.len = dst.eof = 0;
+
+	while (srcsize > 0) {
+		if ((at_signs == 0 || at_signs == 1) && *p == '@') {
+			at_signs++;
+			p++;
+			srcsize--;
+			continue;
+		}
+		if (at_signs == 1) {
+			at_signs = 0;
+			strbuf_append(&dst, "@", 1);
+		}
+		if (at_signs == 0) {
+			strbuf_append(&dst, p, 1);
+			p++;
+			srcsize--;
+			continue;
+		}
+
+		if ((match = match_keyword(p, srcsize, "@@")))
+			strbuf_append(&dst, "@@", 2);
+		else if ((match = match_keyword(p, srcsize, "COMMITID@@")))
+			strbuf_append(&dst, sha1_to_hex(commit_sha1), 40);
+		else
+			strbuf_append(&dst, "@@", 2);
+		at_signs = 0;
+		p += match;
+		srcsize -= match;
+		replaced_something = 1;
+	}
+
+	if (!replaced_something) {
+		free(dst.buf);
+		return NULL;
+	}
+
+	*sizep = dst.len;
+	return dst.buf;
+}
+
+void *sha1_file_to_archive(const char *path, const unsigned char *sha1,
+                           unsigned int mode, enum object_type *type,
+                           unsigned long *size,
+                           const unsigned char *commit_sha1)
+{
+	void *buffer = read_sha1_file(sha1, type, size);
+	if (S_ISREG(mode) && buffer) {
+		void *converted = convert_to_working_tree(path, buffer, size);
+		if (converted) {
+			free(buffer);
+			buffer = converted;
+		}
+
+		converted = convert_to_archive(path, buffer, size, commit_sha1);
+		if (converted) {
+			free(buffer);
+			buffer = converted;
+		}
+	}
+	return buffer;
+}