Subject: [PATCH/RFC] add: support saving the last <n> versions of the index

From: Nguyễn Thái Ngọc Duy <[email-masked]>
Date: Sun, 4 Aug 2013 13:28:40 +0700

When you do "git add foo", change "foo" and "git add foo" again, the
previous foo version is still in the repository, but its SHA-1 may be
lost. "git fsck --lost-found" may help, but it may take more time to
find out which blob is the old "foo".

This patch adds support for saving old index files, so that people
could look back more easily. In the case of "old foo", the user could
use ls-files with the previous of the index to determine SHA-1 of "old
foo".

When core.indexlogsize is defined to <n>, every time the index
changes(**), the old version is stored as $GIT_DIR/index-<i>, where
<i> is from 0..<n>-1. <i> increased after every change and wrapped
around at <n>-1. The current <i> is stored in $GIT_DIR/index as new
ILOG extension. ILOG extension also contains HEAD's tree SHA-1 and a
line describing the change.

(**) The index can change in many ways, but only changes by "git add"
(and maybe "git update-index" as well) are backed up. Non-SHA1 updates
are not worth watching. SHA-1 changes that end up in a commit
are tracked by reflog already. "git mv" and "git rm" may be worth
watching too.
---
 http://thread.gmane.org/gmane.comp.version-control.git/231594 reminds
 me that I have had something like this for a long time but never
 finished it. So here we go again.

 I think the basic idea is ok (new index versions like v5 may have
 problems though). The UI is harder. We need something like "git
 reflog" and perhaps something like @{N} syntax to access old indexes.
 Being able to diff between two indexes are even better but that might
 be a lot of work, diff-ing "git ls-files --stage" output should be
 ok. Hmm?

 builtin/add.c |  9 ++++++++-
 cache.h       |  5 +++++
 config.c      |  5 +++++
 environment.c |  1 +
 lockfile.c    | 12 +++++++++++-
 read-cache.c  | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 6 files changed, 85 insertions(+), 3 deletions(-)

diff --git a/builtin/add.c b/builtin/add.c
index 8266a9c..90e580c 100644
--- a/builtin/add.c
+++ b/builtin/add.c
@@ -15,6 +15,7 @@
 #include "diffcore.h"
 #include "revision.h"
 #include "bulk-checkin.h"
+#include "quote.h"
 
 static const char * const builtin_add_usage[] = {
 	N_("git add [options] [--] <pathspec>..."),
@@ -454,9 +455,15 @@ int cmd_add(int argc, const char **argv, const char *prefix)
 	char *seen = NULL;
 	int implicit_dot = 0;
 	struct update_callback_data update_data;
+	struct strbuf log = STRBUF_INIT;
 
 	git_config(add_config, NULL);
 
+	if (index_log_size) {
+		strbuf_addf(&log, "[%s] add ", prefix ? prefix : "");
+		sq_quote_argv(&log, argv, 0);
+	}
+
 	argc = parse_options(argc, argv, prefix, builtin_add_options,
 			  builtin_add_usage, PARSE_OPT_KEEP_ARGV0);
 	if (patch_interactive)
@@ -600,7 +607,7 @@ int cmd_add(int argc, const char **argv, const char *prefix)
 
  finish:
 	if (active_cache_changed) {
-		if (write_cache(newfd, active_cache, active_nr) ||
+		if (write_index_with_log(&the_index, newfd, log.buf) ||
 		    commit_locked_index(&lock_file))
 			die(_("Unable to write new index file"));
 	}
diff --git a/cache.h b/cache.h
index 85b544f..7655c4b 100644
--- a/cache.h
+++ b/cache.h
@@ -274,9 +274,12 @@ struct index_state {
 	struct cache_tree *cache_tree;
 	struct cache_time timestamp;
 	unsigned name_hash_initialized : 1,
+		 log_updated : 1,
 		 initialized : 1;
 	struct hash_table name_hash;
 	struct hash_table dir_hash;
+	unsigned int log_index;
+	struct strbuf *log;
 };
 
 extern struct index_state the_index;
@@ -454,6 +457,7 @@ extern int read_index_from(struct index_state *, const char *path);
 extern int is_index_unborn(struct index_state *);
 extern int read_index_unmerged(struct index_state *);
 extern int write_index(struct index_state *, int newfd);
+extern int write_index_with_log(struct index_state *, int newfd, const char *log);
 extern int discard_index(struct index_state *);
 extern int unmerged_index(const struct index_state *);
 extern int verify_path(const char *path);
@@ -595,6 +599,7 @@ extern int fsync_object_files;
 extern int core_preload_index;
 extern int core_apply_sparse_checkout;
 extern int precomposed_unicode;
+extern unsigned long index_log_size;
 
 /*
  * The character that begins a commented line in user-editable file
diff --git a/config.c b/config.c
index e13a7b6..f5e40fb 100644
--- a/config.c
+++ b/config.c
@@ -831,6 +831,11 @@ static int git_default_core_config(const char *var, const char *value)
 		return 0;
 	}
 
+	if (!strcmp(var, "core.indexlogsize")) {
+		index_log_size = git_config_ulong(var, value);
+		return 0;
+	}
+
 	/* Add other config variables here and to Documentation/config.txt. */
 	return 0;
 }
diff --git a/environment.c b/environment.c
index 5398c36..12f01f7 100644
--- a/environment.c
+++ b/environment.c
@@ -62,6 +62,7 @@ int merge_log_config = -1;
 int precomposed_unicode = -1; /* see probe_utf8_pathname_composition() */
 struct startup_info *startup_info;
 unsigned long pack_size_limit_cfg;
+unsigned long index_log_size;
 
 /*
  * The character that begins a commented line in user-editable file
diff --git a/lockfile.c b/lockfile.c
index 8fbcb6a..880a4e4 100644
--- a/lockfile.c
+++ b/lockfile.c
@@ -267,8 +267,18 @@ int commit_locked_index(struct lock_file *lk)
 		lk->filename[0] = 0;
 		return 0;
 	}
-	else
+	else {
+		if (the_index.log_updated) {
+			char src[PATH_MAX], backup[PATH_MAX];
+			int len = strlen(lk->filename) - 5; /* .lock */
+			memcpy(src, lk->filename, len);
+			src[len] = 0;
+			memcpy(backup, lk->filename, len);
+			sprintf(backup+len, "-%u", the_index.log_index);
+			rename(src, backup); /* does it matter if it fails? */
+		}
 		return commit_lock_file(lk);
+	}
 }
 
 void rollback_lock_file(struct lock_file *lk)
diff --git a/read-cache.c b/read-cache.c
index c3d5e35..9d8300a 100644
--- a/read-cache.c
+++ b/read-cache.c
@@ -33,6 +33,7 @@ static struct cache_entry *refresh_cache_entry(struct cache_entry *ce, int reall
 #define CACHE_EXT(s) ( (s[0]<<24)|(s[1]<<16)|(s[2]<<8)|(s[3]) )
 #define CACHE_EXT_TREE 0x54524545	/* "TREE" */
 #define CACHE_EXT_RESOLVE_UNDO 0x52455543 /* "REUC" */
+#define CACHE_EXT_INDEX_LOG 0x494C4F47	  /* "ILOG" */
 
 struct index_state the_index;
 
@@ -1297,6 +1298,18 @@ static int read_index_extension(struct index_state *istate,
 	case CACHE_EXT_RESOLVE_UNDO:
 		istate->resolve_undo = resolve_undo_read(data, sz);
 		break;
+	case CACHE_EXT_INDEX_LOG:
+		if (!istate->log) {
+			istate->log = xmalloc(sizeof(struct strbuf *));
+			strbuf_init(istate->log, 128);
+		}
+		strbuf_add(istate->log, data, sz);
+		if (sscanf(istate->log->buf, "%u", &istate->log_index) != 1) {
+			strbuf_release(istate->log);
+			free(istate->log);
+			istate->log = NULL;
+		}
+		break;
 	default:
 		if (*ext < 'A' || 'Z' < *ext)
 			return error("index uses %.4s extension, which we do not understand",
@@ -1538,6 +1551,15 @@ int discard_index(struct index_state *istate)
 	free(istate->cache);
 	istate->cache = NULL;
 	istate->cache_alloc = 0;
+	istate->log_updated = 0;
+	istate->log_index = 0;
+	if (istate->log) {
+		strbuf_release(istate->log);
+		free(istate->log);
+		istate->log = NULL;
+	}
+
+	/* no need to throw away allocated active_cache */
 	return 0;
 }
 
@@ -1771,7 +1793,8 @@ void update_index_if_able(struct index_state *istate, struct lock_file *lockfile
 		rollback_lock_file(lockfile);
 }
 
-int write_index(struct index_state *istate, int newfd)
+int write_index_with_log(struct index_state *istate, int newfd,
+			 const char *log)
 {
 	git_SHA_CTX c;
 	struct cache_header hdr;
@@ -1847,6 +1870,32 @@ int write_index(struct index_state *istate, int newfd)
 			return -1;
 	}
 
+	if (index_log_size) {
+		unsigned char sha1[20];
+		if (log) {
+			istate->log_index = (istate->log_index + 1) % index_log_size;
+			istate->log_updated = 1;
+		} else
+			log = "";
+		if (get_sha1("HEAD^{tree}", sha1))
+			hashclr(sha1);
+		if (!istate->log) {
+			istate->log = xmalloc(sizeof(struct strbuf *));
+			strbuf_init(istate->log, 128);
+		} else
+			strbuf_reset(istate->log);
+		strbuf_addf(istate->log, "%u %s %s %s",
+			    istate->log_index, sha1_to_hex(sha1),
+			    git_committer_info(0), log);
+
+		err = write_index_ext_header(&c, newfd, CACHE_EXT_INDEX_LOG,
+					     istate->log->len) < 0
+			|| ce_write(&c, newfd, istate->log->buf,
+				    istate->log->len) < 0;
+		if (err)
+			return -1;
+	}
+
 	if (ce_flush(&c, newfd) || fstat(newfd, &st))
 		return -1;
 	istate->timestamp.sec = (unsigned int)st.st_mtime;
@@ -1854,6 +1903,11 @@ int write_index(struct index_state *istate, int newfd)
 	return 0;
 }
 
+int write_index(struct index_state *istate, int newfd)
+{
+	return write_index_with_log(istate, newfd, NULL);
+}
+
 /*
  * Read the index file that is potentially unmerged into given
  * index_state, dropping any unmerged entries.  Returns true if
-- 
1.8.2.83.gc99314b