Commits

Anonymous committed f0679f4 Merge

Merge branch 'sp/reflog'

* sp/reflog:
fetch.c: do not pass uninitialized lock to unlock_ref().
Test that git-branch -l works.
Verify git-commit provides a reflog message.
Enable ref log creation in git checkout -b.
Create/delete branch ref logs.
Include ref log detail in commit, reset, etc.
Change order of -m option to update-ref.
Correct force_write bug in refs.c
Change 'master@noon' syntax to 'master@{noon}'.
Log ref updates made by fetch.
Force writing ref if it doesn't exist.
Added logs/ directory to repository layout.
General ref log reading improvements.
Fix ref log parsing so it works properly.
Support 'master@2 hours ago' syntax
Log ref updates to logs/refs/<ref>
Convert update-ref to use ref_lock API.
Improve abstraction of ref lock/write.

Comments (0)

Files changed (26)

Documentation/config.txt

 	This is sometimes needed to work with old scripts that
 	expect HEAD to be a symbolic link.
 
+core.logAllRefUpdates::
+	If true, `git-update-ref` will append a line to
+	"$GIT_DIR/logs/<ref>" listing the new SHA1 and the date/time
+	of the update.	If the file does not exist it will be
+	created automatically.	This information can be used to
+	determine what commit was the tip of a branch "2 days ago".
+	This value is false by default (no logging).
+
 core.repositoryFormatVersion::
 	Internal variable identifying the repository format and layout
 	version.

Documentation/git-branch.txt

 --------
 [verse]
 'git-branch' [-r]
-'git-branch' [-f] <branchname> [<start-point>]
+'git-branch' [-l] [-f] <branchname> [<start-point>]
 'git-branch' (-d | -D) <branchname>...
 
 DESCRIPTION
 equal to that of the currently checked out branch.
 
 With a `-d` or `-D` option, `<branchname>` will be deleted.  You may
-specify more than one branch for deletion.
+specify more than one branch for deletion.  If the branch currently
+has a ref log then the ref log will also be deleted.
 
 
 OPTIONS
 -D::
 	Delete a branch irrespective of its index status.
 
+-l::
+	Create the branch's ref log.  This activates recording of
+	all changes to made the branch ref, enabling use of date
+	based sha1 expressions such as "<branchname>@{yesterday}".
+
 -f::
 	Force the creation of a new branch even if it means deleting
 	a branch that already exists with the same name.

Documentation/git-checkout.txt

 SYNOPSIS
 --------
 [verse]
-'git-checkout' [-f] [-b <new_branch>] [-m] [<branch>]
+'git-checkout' [-f] [-b <new_branch> [-l]] [-m] [<branch>]
 'git-checkout' [-m] [<branch>] <paths>...
 
 DESCRIPTION
 	by gitlink:git-check-ref-format[1].  Some of these checks
 	may restrict the characters allowed in a branch name.
 
+-l::
+	Create the new branch's ref log.  This activates recording of
+	all changes to made the branch ref, enabling use of date
+	based sha1 expressions such as "<branchname>@{yesterday}".
+
 -m::
 	If you have local modifications to one or more files that
 	are different between the current branch and the branch to

Documentation/git-rev-parse.txt

   happen to have both heads/master and tags/master, you can
   explicitly say 'heads/master' to tell git which one you mean.
 
+* A suffix '@' followed by a date specification enclosed in a brace
+  pair (e.g. '\{yesterday\}', '\{1 month 2 weeks 3 days 1 hour 1
+  second ago\}' or '\{1979-02-26 18:30:00\}') to specify the value
+  of the ref at a prior point in time.  This suffix may only be
+  used immediately following a ref name and the ref must have an
+  existing log ($GIT_DIR/logs/<ref>).
+
 * A suffix '{caret}' to a revision parameter means the first parent of
   that commit object.  '{caret}<n>' means the <n>th parent (i.e.
   'rev{caret}'

Documentation/git-update-ref.txt

 
 SYNOPSIS
 --------
-'git-update-ref' <ref> <newvalue> [<oldvalue>]
+'git-update-ref' [-m <reason>] <ref> <newvalue> [<oldvalue>]
 
 DESCRIPTION
 -----------
 ref symlink to some other tree, if you have copied a whole
 archive by creating a symlink tree).
 
+Logging Updates
+---------------
+If config parameter "core.logAllRefUpdates" is true or the file
+"$GIT_DIR/logs/<ref>" exists then `git-update-ref` will append
+a line to the log file "$GIT_DIR/logs/<ref>" (dereferencing all
+symbolic refs before creating the log name) describing the change
+in ref value.  Log lines are formatted as:
+
+    . oldsha1 SP newsha1 SP committer LF
++
+Where "oldsha1" is the 40 character hexadecimal value previously
+stored in <ref>, "newsha1" is the 40 character hexadecimal value of
+<newvalue> and "committer" is the committer's name, email address
+and date in the standard GIT committer ident format.
+
+Optionally with -m:
+
+    . oldsha1 SP newsha1 SP committer TAB message LF
++
+Where all fields are as described above and "message" is the
+value supplied to the -m option.
+
+An update will fail (without changing <ref>) if the current user is
+unable to create a new log file, append to the existing log file
+or does not have committer information available.
+
 Author
 ------
 Written by Linus Torvalds <torvalds@osdl.org>.

Documentation/repository-layout.txt

 	Stores shorthands to be used to give URL and default
 	refnames to interact with remote repository to `git
 	fetch`, `git pull` and `git push` commands.
+
+logs::
+	Records of changes made to refs are stored in this
+	directory.  See the documentation on git-update-ref
+	for more information.
+
+logs/refs/heads/`name`::
+	Records all changes made to the branch tip named `name`.
+
+logs/refs/tags/`name`::
+	Records all changes made to the tag named `name`.
 extern int trust_executable_bit;
 extern int assume_unchanged;
 extern int prefer_symlink_refs;
+extern int log_all_ref_updates;
 extern int warn_ambiguous_refs;
 extern int diff_rename_limit_default;
 extern int shared_repository;
 		return 0;
 	}
 
+	if (!strcmp(var, "core.logallrefupdates")) {
+		log_all_ref_updates = git_config_bool(var, value);
+		return 0;
+	}
+
 	if (!strcmp(var, "core.warnambiguousrefs")) {
 		warn_ambiguous_refs = git_config_bool(var, value);
 		return 0;
 int trust_executable_bit = 1;
 int assume_unchanged = 0;
 int prefer_symlink_refs = 0;
+int log_all_ref_updates = 0;
 int warn_ambiguous_refs = 1;
 int repository_format_version = 0;
 char git_commit_encoding[MAX_ENCODING_LENGTH] = "utf-8";
 #include "refs.h"
 
 const char *write_ref = NULL;
+const char *write_ref_log_details = NULL;
 
 int get_tree = 0;
 int get_history = 0;
 
 int pull(char *target)
 {
+	struct ref_lock *lock = NULL;
 	unsigned char sha1[20];
+	char *msg;
+	int ret;
 
 	save_commit_buffer = 0;
 	track_object_refs = 0;
+	if (write_ref) {
+		lock = lock_ref_sha1(write_ref, NULL, 0);
+		if (!lock) {
+			error("Can't lock ref %s", write_ref);
+			return -1;
+		}
+	}
 
 	if (!get_recover)
 		for_each_ref(mark_complete);
 
-	if (interpret_target(target, sha1))
-		return error("Could not interpret %s as something to pull",
-			     target);
-	if (process(lookup_unknown_object(sha1)))
+	if (interpret_target(target, sha1)) {
+		error("Could not interpret %s as something to pull", target);
+		if (lock)
+			unlock_ref(lock);
 		return -1;
-	if (loop())
+	}
+	if (process(lookup_unknown_object(sha1))) {
+		if (lock)
+			unlock_ref(lock);
 		return -1;
-	
-	if (write_ref)
-		write_ref_sha1_unlocked(write_ref, sha1);
+	}
+	if (loop()) {
+		if (lock)
+			unlock_ref(lock);
+		return -1;
+	}
+
+	if (write_ref) {
+		if (write_ref_log_details) {
+			msg = xmalloc(strlen(write_ref_log_details) + 12);
+			sprintf(msg, "fetch from %s", write_ref_log_details);
+		} else
+			msg = NULL;
+		ret = write_ref_sha1(lock, sha1, msg ? msg : "fetch (unknown)");
+		if (msg)
+			free(msg);
+		return ret;
+	}
 	return 0;
 }
 /* If set, the ref filename to write the target value to. */
 extern const char *write_ref;
 
+/* If set additional text will appear in the ref log. */
+extern const char *write_ref_log_details;
+
 /* Set to fetch the target tree. */
 extern int get_tree;
 
 	parent=$(git-rev-parse --verify HEAD) &&
 	commit=$(git-commit-tree $tree -p $parent <"$dotest/final-commit") &&
 	echo Committed: $commit &&
-	git-update-ref HEAD $commit $parent ||
+	git-update-ref -m "am: $SUBJECT" HEAD $commit $parent ||
 	stop_here $this
 
 	if test -x "$GIT_DIR"/hooks/post-applypatch

git-applypatch.sh

 parent=$(git-rev-parse --verify HEAD) &&
 commit=$(git-commit-tree $tree -p $parent <"$final") || exit 1
 echo Committed: $commit
-git-update-ref HEAD $commit $parent || exit
+git-update-ref -m "applypatch: $SUBJECT" HEAD $commit $parent || exit
 
 if test -x "$GIT_DIR"/hooks/post-applypatch
 then
 #!/bin/sh
 
-USAGE='[(-d | -D) <branchname>] | [[-f] <branchname> [<start-point>]] | -r'
+USAGE='[-l] [(-d | -D) <branchname>] | [[-f] <branchname> [<start-point>]] | -r'
 LONG_USAGE='If no arguments, show available branches and mark current branch with a star.
 If one argument, create a new branch <branchname> based off of current HEAD.
 If two arguments, create a new branch <branchname> based off of <start-point>.'
 	    esac
 	    ;;
 	esac
+	rm -f "$GIT_DIR/logs/refs/heads/$branch_name"
 	rm -f "$GIT_DIR/refs/heads/$branch_name"
 	echo "Deleted branch $branch_name."
     done
 }
 
 force=
+create_log=
 while case "$#,$1" in 0,*) break ;; *,-*) ;; *) break ;; esac
 do
 	case "$1" in
 	-f)
 		force="$1"
 		;;
+	-l)
+		create_log="yes"
+		;;
 	--)
 		shift
 		break
 		die "cannot force-update the current branch."
 	fi
 fi
-git update-ref "refs/heads/$branchname" $rev
+if test "$create_log" = 'yes'
+then
+	mkdir -p $(dirname "$GIT_DIR/logs/refs/heads/$branchname")
+	touch "$GIT_DIR/logs/refs/heads/$branchname"
+fi
+git update-ref -m "branch: Created from $head" "refs/heads/$branchname" $rev
 . git-sh-setup
 
 old=$(git-rev-parse HEAD)
+old_name=HEAD
 new=
+new_name=
 force=
 branch=
 newbranch=
+newbranch_log=
 merge=
 while [ "$#" != "0" ]; do
     arg="$1"
 		git-check-ref-format "heads/$newbranch" ||
 			die "git checkout: we do not like '$newbranch' as a branch name."
 		;;
+	"-l")
+		newbranch_log=1
+		;;
 	"-f")
 		force=1
 		;;
 				exit 1
 			fi
 			new="$rev"
+			new_name="$arg^0"
 			if [ -f "$GIT_DIR/refs/heads/$arg" ]; then
 				branch="$arg"
 			fi
 		then
 			# checking out selected paths from a tree-ish.
 			new="$rev"
+			new_name="$arg^{tree}"
 			branch=
 		else
 			new=
+			new_name=
 			branch=
 			set x "$arg" "$@"
 			shift
 	cd "$cdup"
 fi
 
-[ -z "$new" ] && new=$old
+[ -z "$new" ] && new=$old && new_name="$old_name"
 
 # If we don't have an old branch that we're switching to,
 # and we don't have a new branch name for the target we
 #
 if [ "$?" -eq 0 ]; then
 	if [ "$newbranch" ]; then
-		leading=`expr "refs/heads/$newbranch" : '\(.*\)/'` &&
-		mkdir -p "$GIT_DIR/$leading" &&
-		echo $new >"$GIT_DIR/refs/heads/$newbranch" || exit
+		if [ "$newbranch_log" ]; then
+			mkdir -p $(dirname "$GIT_DIR/logs/refs/heads/$newbranch")
+			touch "$GIT_DIR/logs/refs/heads/$newbranch"
+		fi
+		git-update-ref -m "checkout: Created from $new_name" "refs/heads/$newbranch" $new || exit
 		branch="$newbranch"
 	fi
 	[ "$branch" ] &&
 		rm -f "$TMP_INDEX"
 	fi &&
 	commit=$(cat "$GIT_DIR"/COMMIT_MSG | git-commit-tree $tree $PARENTS) &&
-	git-update-ref HEAD $commit $current &&
+	rlogm=$(sed -e 1q "$GIT_DIR"/COMMIT_MSG) &&
+	git-update-ref -m "commit: $rlogm" HEAD $commit $current &&
 	rm -f -- "$GIT_DIR/MERGE_HEAD" &&
 	if test -f "$NEXT_INDEX"
 	then
 else
 	rm -f "$GIT_DIR/ORIG_HEAD"
 fi
-git-update-ref HEAD "$rev"
+git-update-ref -m "reset $reset_type $@" HEAD "$rev"
 
 case "$reset_type" in
 --hard )
 	int rc = 0;
 
 	setup_git_directory();
+	git_config(git_default_config);
 
 	while (arg < argc && argv[arg][0] == '-') {
 		if (argv[arg][1] == 't') {
 	}
 	commit_id = argv[arg];
 	url = argv[arg + 1];
+	write_ref_log_details = url;
 
 	http_init();
 
 	int arg = 1;
 
 	setup_git_directory();
+	git_config(git_default_config);
 
 	while (arg < argc && argv[arg][0] == '-') {
 		if (argv[arg][1] == 't')
 		usage(local_pull_usage);
 	commit_id = argv[arg];
 	path = argv[arg + 1];
+	write_ref_log_details = path;
 
 	if (pull(commit_id))
 		return 1;
 			namelen = strlen(de->d_name);
 			if (namelen > 255)
 				continue;
+			if (namelen>5 && !strcmp(de->d_name+namelen-5,".lock"))
+				continue;
 			memcpy(path + baselen, de->d_name, namelen+1);
 			if (stat(git_path("%s", path), &st) < 0)
 				continue;
 	return do_for_each_ref("refs/remotes", fn, 13);
 }
 
-static char *ref_file_name(const char *ref)
-{
-	char *base = get_refs_directory();
-	int baselen = strlen(base);
-	int reflen = strlen(ref);
-	char *ret = xmalloc(baselen + 2 + reflen);
-	sprintf(ret, "%s/%s", base, ref);
-	return ret;
-}
-
-static char *ref_lock_file_name(const char *ref)
-{
-	char *base = get_refs_directory();
-	int baselen = strlen(base);
-	int reflen = strlen(ref);
-	char *ret = xmalloc(baselen + 7 + reflen);
-	sprintf(ret, "%s/%s.lock", base, ref);
-	return ret;
-}
-
 int get_ref_sha1(const char *ref, unsigned char *sha1)
 {
 	if (check_ref_format(ref))
 	return read_ref(git_path("refs/%s", ref), sha1);
 }
 
-static int lock_ref_file(const char *filename, const char *lock_filename,
-			 const unsigned char *old_sha1)
-{
-	int fd = open(lock_filename, O_WRONLY | O_CREAT | O_EXCL, 0666);
-	unsigned char current_sha1[20];
-	int retval;
-	if (fd < 0) {
-		return error("Couldn't open lock file for %s: %s",
-			     filename, strerror(errno));
-	}
-	retval = read_ref(filename, current_sha1);
-	if (old_sha1) {
-		if (retval) {
-			close(fd);
-			unlink(lock_filename);
-			return error("Could not read the current value of %s",
-				     filename);
-		}
-		if (memcmp(current_sha1, old_sha1, 20)) {
-			close(fd);
-			unlink(lock_filename);
-			error("The current value of %s is %s",
-			      filename, sha1_to_hex(current_sha1));
-			return error("Expected %s",
-				     sha1_to_hex(old_sha1));
-		}
-	} else {
-		if (!retval) {
-			close(fd);
-			unlink(lock_filename);
-			return error("Unexpectedly found a value of %s for %s",
-				     sha1_to_hex(current_sha1), filename);
-		}
-	}
-	return fd;
-}
-
-int lock_ref_sha1(const char *ref, const unsigned char *old_sha1)
-{
-	char *filename;
-	char *lock_filename;
-	int retval;
-	if (check_ref_format(ref))
-		return -1;
-	filename = ref_file_name(ref);
-	lock_filename = ref_lock_file_name(ref);
-	retval = lock_ref_file(filename, lock_filename, old_sha1);
-	free(filename);
-	free(lock_filename);
-	return retval;
-}
-
-static int write_ref_file(const char *filename,
-			  const char *lock_filename, int fd,
-			  const unsigned char *sha1)
-{
-	char *hex = sha1_to_hex(sha1);
-	char term = '\n';
-	if (write(fd, hex, 40) < 40 ||
-	    write(fd, &term, 1) < 1) {
-		error("Couldn't write %s", filename);
-		close(fd);
-		return -1;
-	}
-	close(fd);
-	rename(lock_filename, filename);
-	return 0;
-}
-
-int write_ref_sha1(const char *ref, int fd, const unsigned char *sha1)
-{
-	char *filename;
-	char *lock_filename;
-	int retval;
-	if (fd < 0)
-		return -1;
-	if (check_ref_format(ref))
-		return -1;
-	filename = ref_file_name(ref);
-	lock_filename = ref_lock_file_name(ref);
-	if (safe_create_leading_directories(filename))
-		die("unable to create leading directory for %s", filename);
-	retval = write_ref_file(filename, lock_filename, fd, sha1);
-	free(filename);
-	free(lock_filename);
-	return retval;
-}
-
 /*
  * Make sure "ref" is something reasonable to have under ".git/refs/";
  * We do not like it if:
 	}
 }
 
-int write_ref_sha1_unlocked(const char *ref, const unsigned char *sha1)
+static struct ref_lock* verify_lock(struct ref_lock *lock,
+	const unsigned char *old_sha1, int mustexist)
+{
+	char buf[40];
+	int nr, fd = open(lock->ref_file, O_RDONLY);
+	if (fd < 0 && (mustexist || errno != ENOENT)) {
+		error("Can't verify ref %s", lock->ref_file);
+		unlock_ref(lock);
+		return NULL;
+	}
+	nr = read(fd, buf, 40);
+	close(fd);
+	if (nr != 40 || get_sha1_hex(buf, lock->old_sha1) < 0) {
+		error("Can't verify ref %s", lock->ref_file);
+		unlock_ref(lock);
+		return NULL;
+	}
+	if (memcmp(lock->old_sha1, old_sha1, 20)) {
+		error("Ref %s is at %s but expected %s", lock->ref_file,
+			sha1_to_hex(lock->old_sha1), sha1_to_hex(old_sha1));
+		unlock_ref(lock);
+		return NULL;
+	}
+	return lock;
+}
+
+static struct ref_lock* lock_ref_sha1_basic(const char *path,
+	int plen,
+	const unsigned char *old_sha1, int mustexist)
+{
+	struct ref_lock *lock;
+	struct stat st;
+
+	lock = xcalloc(1, sizeof(struct ref_lock));
+	lock->lock_fd = -1;
+
+	plen = strlen(path) - plen;
+	path = resolve_ref(path, lock->old_sha1, mustexist);
+	if (!path) {
+		unlock_ref(lock);
+		return NULL;
+	}
+
+	lock->ref_file = strdup(path);
+	lock->lock_file = strdup(mkpath("%s.lock", lock->ref_file));
+	lock->log_file = strdup(git_path("logs/%s", lock->ref_file + plen));
+	lock->force_write = lstat(lock->ref_file, &st) && errno == ENOENT;
+
+	if (safe_create_leading_directories(lock->lock_file))
+		die("unable to create directory for %s", lock->lock_file);
+	lock->lock_fd = open(lock->lock_file,
+		O_WRONLY | O_CREAT | O_EXCL, 0666);
+	if (lock->lock_fd < 0) {
+		error("Couldn't open lock file %s: %s",
+			lock->lock_file, strerror(errno));
+		unlock_ref(lock);
+		return NULL;
+	}
+
+	return old_sha1 ? verify_lock(lock, old_sha1, mustexist) : lock;
+}
+
+struct ref_lock* lock_ref_sha1(const char *ref,
+	const unsigned char *old_sha1, int mustexist)
 {
-	char *filename;
-	char *lock_filename;
-	int fd;
-	int retval;
 	if (check_ref_format(ref))
+		return NULL;
+	return lock_ref_sha1_basic(git_path("refs/%s", ref),
+		5 + strlen(ref), old_sha1, mustexist);
+}
+
+struct ref_lock* lock_any_ref_for_update(const char *ref,
+	const unsigned char *old_sha1, int mustexist)
+{
+	return lock_ref_sha1_basic(git_path("%s", ref),
+		strlen(ref), old_sha1, mustexist);
+}
+
+void unlock_ref (struct ref_lock *lock)
+{
+	if (lock->lock_fd >= 0) {
+		close(lock->lock_fd);
+		unlink(lock->lock_file);
+	}
+	if (lock->ref_file)
+		free(lock->ref_file);
+	if (lock->lock_file)
+		free(lock->lock_file);
+	if (lock->log_file)
+		free(lock->log_file);
+	free(lock);
+}
+
+static int log_ref_write(struct ref_lock *lock,
+	const unsigned char *sha1, const char *msg)
+{
+	int logfd, written, oflags = O_APPEND | O_WRONLY;
+	unsigned maxlen, len;
+	char *logrec;
+	const char *comitter;
+
+	if (log_all_ref_updates) {
+		if (safe_create_leading_directories(lock->log_file) < 0)
+			return error("unable to create directory for %s",
+				lock->log_file);
+		oflags |= O_CREAT;
+	}
+
+	logfd = open(lock->log_file, oflags, 0666);
+	if (logfd < 0) {
+		if (!log_all_ref_updates && errno == ENOENT)
+			return 0;
+		return error("Unable to append to %s: %s",
+			lock->log_file, strerror(errno));
+	}
+
+	setup_ident();
+	comitter = git_committer_info(1);
+	if (msg) {
+		maxlen = strlen(comitter) + strlen(msg) + 2*40 + 5;
+		logrec = xmalloc(maxlen);
+		len = snprintf(logrec, maxlen, "%s %s %s\t%s\n",
+			sha1_to_hex(lock->old_sha1),
+			sha1_to_hex(sha1),
+			comitter,
+			msg);
+	} else {
+		maxlen = strlen(comitter) + 2*40 + 4;
+		logrec = xmalloc(maxlen);
+		len = snprintf(logrec, maxlen, "%s %s %s\n",
+			sha1_to_hex(lock->old_sha1),
+			sha1_to_hex(sha1),
+			comitter);
+	}
+	written = len <= maxlen ? write(logfd, logrec, len) : -1;
+	free(logrec);
+	close(logfd);
+	if (written != len)
+		return error("Unable to append to %s", lock->log_file);
+	return 0;
+}
+
+int write_ref_sha1(struct ref_lock *lock,
+	const unsigned char *sha1, const char *logmsg)
+{
+	static char term = '\n';
+
+	if (!lock)
 		return -1;
-	filename = ref_file_name(ref);
-	lock_filename = ref_lock_file_name(ref);
-	if (safe_create_leading_directories(filename))
-		die("unable to create leading directory for %s", filename);
-	fd = open(lock_filename, O_WRONLY | O_CREAT | O_EXCL, 0666);
-	if (fd < 0) {
-		error("Writing %s", lock_filename);
-		perror("Open");
+	if (!lock->force_write && !memcmp(lock->old_sha1, sha1, 20)) {
+		unlock_ref(lock);
+		return 0;
 	}
-	retval = write_ref_file(filename, lock_filename, fd, sha1);
-	free(filename);
-	free(lock_filename);
-	return retval;
+	if (write(lock->lock_fd, sha1_to_hex(sha1), 40) != 40 ||
+	    write(lock->lock_fd, &term, 1) != 1
+		|| close(lock->lock_fd) < 0) {
+		error("Couldn't write %s", lock->lock_file);
+		unlock_ref(lock);
+		return -1;
+	}
+	if (log_ref_write(lock, sha1, logmsg) < 0) {
+		unlock_ref(lock);
+		return -1;
+	}
+	if (rename(lock->lock_file, lock->ref_file) < 0) {
+		error("Couldn't set %s", lock->ref_file);
+		unlock_ref(lock);
+		return -1;
+	}
+	lock->lock_fd = -1;
+	unlock_ref(lock);
+	return 0;
+}
+
+int read_ref_at(const char *ref, unsigned long at_time, unsigned char *sha1)
+{
+	const char *logfile, *logdata, *logend, *rec, *lastgt, *lastrec;
+	char *tz_c;
+	int logfd, tz;
+	struct stat st;
+	unsigned long date;
+	unsigned char logged_sha1[20];
+
+	logfile = git_path("logs/%s", ref);
+	logfd = open(logfile, O_RDONLY, 0);
+	if (logfd < 0)
+		die("Unable to read log %s: %s", logfile, strerror(errno));
+	fstat(logfd, &st);
+	if (!st.st_size)
+		die("Log %s is empty.", logfile);
+	logdata = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, logfd, 0);
+	close(logfd);
+
+	lastrec = NULL;
+	rec = logend = logdata + st.st_size;
+	while (logdata < rec) {
+		if (logdata < rec && *(rec-1) == '\n')
+			rec--;
+		lastgt = NULL;
+		while (logdata < rec && *(rec-1) != '\n') {
+			rec--;
+			if (*rec == '>')
+				lastgt = rec;
+		}
+		if (!lastgt)
+			die("Log %s is corrupt.", logfile);
+		date = strtoul(lastgt + 1, &tz_c, 10);
+		if (date <= at_time) {
+			if (lastrec) {
+				if (get_sha1_hex(lastrec, logged_sha1))
+					die("Log %s is corrupt.", logfile);
+				if (get_sha1_hex(rec + 41, sha1))
+					die("Log %s is corrupt.", logfile);
+				if (memcmp(logged_sha1, sha1, 20)) {
+					tz = strtoul(tz_c, NULL, 10);
+					fprintf(stderr,
+						"warning: Log %s has gap after %s.\n",
+						logfile, show_rfc2822_date(date, tz));
+				}
+			} else if (date == at_time) {
+				if (get_sha1_hex(rec + 41, sha1))
+					die("Log %s is corrupt.", logfile);
+			} else {
+				if (get_sha1_hex(rec + 41, logged_sha1))
+					die("Log %s is corrupt.", logfile);
+				if (memcmp(logged_sha1, sha1, 20)) {
+					tz = strtoul(tz_c, NULL, 10);
+					fprintf(stderr,
+						"warning: Log %s unexpectedly ended on %s.\n",
+						logfile, show_rfc2822_date(date, tz));
+				}
+			}
+			munmap((void*)logdata, st.st_size);
+			return 0;
+		}
+		lastrec = rec;
+	}
+
+	rec = logdata;
+	while (rec < logend && *rec != '>' && *rec != '\n')
+		rec++;
+	if (rec == logend || *rec == '\n')
+		die("Log %s is corrupt.", logfile);
+	date = strtoul(rec + 1, &tz_c, 10);
+	tz = strtoul(tz_c, NULL, 10);
+	if (get_sha1_hex(logdata, sha1))
+		die("Log %s is corrupt.", logfile);
+	munmap((void*)logdata, st.st_size);
+	fprintf(stderr, "warning: Log %s only goes back to %s.\n",
+		logfile, show_rfc2822_date(date, tz));
+	return 0;
 }
 #ifndef REFS_H
 #define REFS_H
 
+struct ref_lock {
+	char *ref_file;
+	char *lock_file;
+	char *log_file;
+	unsigned char old_sha1[20];
+	int lock_fd;
+	int force_write;
+};
+
 /*
  * Calls the specified function for each ref file until it returns nonzero,
  * and returns the value
 /** Reads the refs file specified into sha1 **/
 extern int get_ref_sha1(const char *ref, unsigned char *sha1);
 
-/** Locks ref and returns the fd to give to write_ref_sha1() if the ref
- * has the given value currently; otherwise, returns -1.
- **/
-extern int lock_ref_sha1(const char *ref, const unsigned char *old_sha1);
+/** Locks a "refs/" ref returning the lock on success and NULL on failure. **/
+extern struct ref_lock* lock_ref_sha1(const char *ref, const unsigned char *old_sha1, int mustexist);
+
+/** Locks any ref (for 'HEAD' type refs). */
+extern struct ref_lock* lock_any_ref_for_update(const char *ref, const unsigned char *old_sha1, int mustexist);
+
+/** Release any lock taken but not written. **/
+extern void unlock_ref (struct ref_lock *lock);
 
-/** Writes sha1 into the refs file specified, locked with the given fd. **/
-extern int write_ref_sha1(const char *ref, int fd, const unsigned char *sha1);
+/** Writes sha1 into the ref specified by the lock. **/
+extern int write_ref_sha1(struct ref_lock *lock, const unsigned char *sha1, const char *msg);
 
-/** Writes sha1 into the refs file specified. **/
-extern int write_ref_sha1_unlocked(const char *ref, const unsigned char *sha1);
+/** Reads log for the value of ref during at_time. **/
+extern int read_ref_at(const char *ref, unsigned long at_time, unsigned char *sha1);
 
 /** Returns 0 if target has the right format for a ref. **/
 extern int check_ref_format(const char *target);
 #include "tree.h"
 #include "blob.h"
 #include "tree-walk.h"
+#include "refs.h"
 
 static int find_short_object_filename(int len, const char *name, unsigned char *sha1)
 {
 		"refs/remotes/%.*s/HEAD",
 		NULL
 	};
-	const char **p;
-	const char *warning = "warning: refname '%.*s' is ambiguous.\n";
-	char *pathname;
-	int already_found = 0;
+	static const char *warning = "warning: refname '%.*s' is ambiguous.\n";
+	const char **p, *pathname;
+	char *real_path = NULL;
+	int refs_found = 0, am;
+	unsigned long at_time = (unsigned long)-1;
 	unsigned char *this_result;
 	unsigned char sha1_from_ref[20];
 
 	if (len == 40 && !get_sha1_hex(str, sha1))
 		return 0;
 
+	/* At a given period of time? "@{2 hours ago}" */
+	for (am = 1; am < len - 1; am++) {
+		if (str[am] == '@' && str[am+1] == '{' && str[len-1] == '}') {
+			int date_len = len - am - 3;
+			char *date_spec = xmalloc(date_len + 1);
+			strncpy(date_spec, str + am + 2, date_len);
+			date_spec[date_len] = 0;
+			at_time = approxidate(date_spec);
+			free(date_spec);
+			len = am;
+			break;
+		}
+	}
+
 	/* Accept only unambiguous ref paths. */
 	if (ambiguous_path(str, len))
 		return -1;
 
 	for (p = fmt; *p; p++) {
-		this_result = already_found ? sha1_from_ref : sha1;
-		pathname = git_path(*p, len, str);
-		if (!read_ref(pathname, this_result)) {
-			if (warn_ambiguous_refs) {
-				if (already_found)
-					fprintf(stderr, warning, len, str);
-				already_found++;
-			}
-			else
-				return 0;
+		this_result = refs_found ? sha1_from_ref : sha1;
+		pathname = resolve_ref(git_path(*p, len, str), this_result, 1);
+		if (pathname) {
+			if (!refs_found++)
+				real_path = strdup(pathname);
+			if (!warn_ambiguous_refs)
+				break;
 		}
 	}
-	if (already_found)
-		return 0;
-	return -1;
+
+	if (!refs_found)
+		return -1;
+
+	if (warn_ambiguous_refs && refs_found > 1)
+		fprintf(stderr, warning, len, str);
+
+	if (at_time != (unsigned long)-1) {
+		read_ref_at(
+			real_path + strlen(git_path(".")) - 1,
+			at_time,
+			sha1);
+	}
+
+	free(real_path);
+	return 0;
 }
 
 static int get_sha1_1(const char *name, int len, unsigned char *sha1);
  */
 int get_sha1(const char *name, unsigned char *sha1)
 {
-	int ret;
+	int ret, bracket_depth;
 	unsigned unused;
 	int namelen = strlen(name);
 	const char *cp;
 		}
 		return -1;
 	}
-	cp = strchr(name, ':');
-	if (cp) {
+	for (cp = name, bracket_depth = 0; *cp; cp++) {
+		if (*cp == '{')
+			bracket_depth++;
+		else if (bracket_depth && *cp == '}')
+			bracket_depth--;
+		else if (!bracket_depth && *cp == ':')
+			break;
+	}
+	if (*cp == ':') {
 		unsigned char tree_sha1[20];
 		if (!get_sha1_1(name, cp-name, tree_sha1))
 			return get_tree_entry(tree_sha1, cp+1, sha1,
 	if (!prog) prog = "git-ssh-upload";
 
 	setup_git_directory();
+	git_config(git_default_config);
 
 	while (arg < argc && argv[arg][0] == '-') {
 		if (argv[arg][1] == 't') {
 	}
 	commit_id = argv[arg];
 	url = argv[arg + 1];
+	write_ref_log_details = url;
 
 	if (setup_connection(&fd_in, &fd_out, prog, url, arg, argv + 1))
 		return 1;

t/t1400-update-ref.sh

+#!/bin/sh
+#
+# Copyright (c) 2006 Shawn Pearce
+#
+
+test_description='Test git-update-ref and basic ref logging'
+. ./test-lib.sh
+
+Z=0000000000000000000000000000000000000000
+A=1111111111111111111111111111111111111111
+B=2222222222222222222222222222222222222222
+C=3333333333333333333333333333333333333333
+D=4444444444444444444444444444444444444444
+E=5555555555555555555555555555555555555555
+F=6666666666666666666666666666666666666666
+m=refs/heads/master
+
+test_expect_success \
+	"create $m" \
+	'git-update-ref $m $A &&
+	 test $A = $(cat .git/$m)'
+test_expect_success \
+	"create $m" \
+	'git-update-ref $m $B $A &&
+	 test $B = $(cat .git/$m)'
+rm -f .git/$m
+
+test_expect_success \
+	"create $m (by HEAD)" \
+	'git-update-ref HEAD $A &&
+	 test $A = $(cat .git/$m)'
+test_expect_success \
+	"create $m (by HEAD)" \
+	'git-update-ref HEAD $B $A &&
+	 test $B = $(cat .git/$m)'
+rm -f .git/$m
+
+test_expect_failure \
+	'(not) create HEAD with old sha1' \
+	'git-update-ref HEAD $A $B'
+test_expect_failure \
+	"(not) prior created .git/$m" \
+	'test -f .git/$m'
+rm -f .git/$m
+
+test_expect_success \
+	"create HEAD" \
+	'git-update-ref HEAD $A'
+test_expect_failure \
+	'(not) change HEAD with wrong SHA1' \
+	'git-update-ref HEAD $B $Z'
+test_expect_failure \
+	"(not) changed .git/$m" \
+	'test $B = $(cat .git/$m)'
+rm -f .git/$m
+
+mkdir -p .git/logs/refs/heads
+touch .git/logs/refs/heads/master
+test_expect_success \
+	"create $m (logged by touch)" \
+	'GIT_COMMITTER_DATE="2005-05-26 23:30" \
+	 git-update-ref HEAD $A -m "Initial Creation" &&
+	 test $A = $(cat .git/$m)'
+test_expect_success \
+	"update $m (logged by touch)" \
+	'GIT_COMMITTER_DATE="2005-05-26 23:31" \
+	 git-update-ref HEAD $B $A -m "Switch" &&
+	 test $B = $(cat .git/$m)'
+test_expect_success \
+	"set $m (logged by touch)" \
+	'GIT_COMMITTER_DATE="2005-05-26 23:41" \
+	 git-update-ref HEAD $A &&
+	 test $A = $(cat .git/$m)'
+
+cat >expect <<EOF
+$Z $A $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150200 +0000	Initial Creation
+$A $B $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150260 +0000	Switch
+$B $A $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150860 +0000
+EOF
+test_expect_success \
+	"verifying $m's log" \
+	'diff expect .git/logs/$m'
+rm -rf .git/$m .git/logs expect
+
+test_expect_success \
+	'enable core.logAllRefUpdates' \
+	'git-repo-config core.logAllRefUpdates true &&
+	 test true = $(git-repo-config --bool --get core.logAllRefUpdates)'
+
+test_expect_success \
+	"create $m (logged by config)" \
+	'GIT_COMMITTER_DATE="2005-05-26 23:32" \
+	 git-update-ref HEAD $A -m "Initial Creation" &&
+	 test $A = $(cat .git/$m)'
+test_expect_success \
+	"update $m (logged by config)" \
+	'GIT_COMMITTER_DATE="2005-05-26 23:33" \
+	 git-update-ref HEAD $B $A -m "Switch" &&
+	 test $B = $(cat .git/$m)'
+test_expect_success \
+	"set $m (logged by config)" \
+	'GIT_COMMITTER_DATE="2005-05-26 23:43" \
+	 git-update-ref HEAD $A &&
+	 test $A = $(cat .git/$m)'
+
+cat >expect <<EOF
+$Z $A $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150320 +0000	Initial Creation
+$A $B $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150380 +0000	Switch
+$B $A $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150980 +0000
+EOF
+test_expect_success \
+	"verifying $m's log" \
+	'diff expect .git/logs/$m'
+rm -f .git/$m .git/logs/$m expect
+
+git-update-ref $m $D
+cat >.git/logs/$m <<EOF
+$C $A $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150320 -0500
+$A $B $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150380 -0500
+$F $Z $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150680 -0500
+$Z $E $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150980 -0500
+EOF
+
+ed="Thu, 26 May 2005 18:32:00 -0500"
+gd="Thu, 26 May 2005 18:33:00 -0500"
+ld="Thu, 26 May 2005 18:43:00 -0500"
+test_expect_success \
+	'Query "master@{May 25 2005}" (before history)' \
+	'rm -f o e
+	 git-rev-parse --verify "master@{May 25 2005}" >o 2>e &&
+	 test $C = $(cat o) &&
+	 test "warning: Log .git/logs/$m only goes back to $ed." = "$(cat e)"'
+test_expect_success \
+	"Query master@{2005-05-25} (before history)" \
+	'rm -f o e
+	 git-rev-parse --verify master@{2005-05-25} >o 2>e &&
+	 test $C = $(cat o) &&
+	 echo test "warning: Log .git/logs/$m only goes back to $ed." = "$(cat e)"'
+test_expect_success \
+	'Query "master@{May 26 2005 23:31:59}" (1 second before history)' \
+	'rm -f o e
+	 git-rev-parse --verify "master@{May 26 2005 23:31:59}" >o 2>e &&
+	 test $C = $(cat o) &&
+	 test "warning: Log .git/logs/$m only goes back to $ed." = "$(cat e)"'
+test_expect_success \
+	'Query "master@{May 26 2005 23:32:00}" (exactly history start)' \
+	'rm -f o e
+	 git-rev-parse --verify "master@{May 26 2005 23:32:00}" >o 2>e &&
+	 test $A = $(cat o) &&
+	 test "" = "$(cat e)"'
+test_expect_success \
+	'Query "master@{2005-05-26 23:33:01}" (middle of history with gap)' \
+	'rm -f o e
+	 git-rev-parse --verify "master@{2005-05-26 23:33:01}" >o 2>e &&
+	 test $B = $(cat o) &&
+	 test "warning: Log .git/logs/$m has gap after $gd." = "$(cat e)"'
+test_expect_success \
+	'Query "master@{2005-05-26 23:38:00}" (middle of history)' \
+	'rm -f o e
+	 git-rev-parse --verify "master@{2005-05-26 23:38:00}" >o 2>e &&
+	 test $Z = $(cat o) &&
+	 test "" = "$(cat e)"'
+test_expect_success \
+	'Query "master@{2005-05-26 23:43:00}" (exact end of history)' \
+	'rm -f o e
+	 git-rev-parse --verify "master@{2005-05-26 23:43:00}" >o 2>e &&
+	 test $E = $(cat o) &&
+	 test "" = "$(cat e)"'
+test_expect_success \
+	'Query "master@{2005-05-28}" (past end of history)' \
+	'rm -f o e
+	 git-rev-parse --verify "master@{2005-05-28}" >o 2>e &&
+	 test $D = $(cat o) &&
+	 test "warning: Log .git/logs/$m unexpectedly ended on $ld." = "$(cat e)"'
+
+
+rm -f .git/$m .git/logs/$m expect
+
+test_expect_success \
+    'creating initial files' \
+    'echo TEST >F &&
+     git-add F &&
+	 GIT_AUTHOR_DATE="2005-05-26 23:30" \
+	 GIT_COMMITTER_DATE="2005-05-26 23:30" git-commit -m add -a &&
+	 h_TEST=$(git-rev-parse --verify HEAD)
+	 echo The other day this did not work. >M &&
+	 echo And then Bob told me how to fix it. >>M &&
+	 echo OTHER >F &&
+	 GIT_AUTHOR_DATE="2005-05-26 23:41" \
+	 GIT_COMMITTER_DATE="2005-05-26 23:41" git-commit -F M -a &&
+	 h_OTHER=$(git-rev-parse --verify HEAD)
+	 rm -f M'
+
+cat >expect <<EOF
+$Z $h_TEST $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150200 +0000	commit: add
+$h_TEST $h_OTHER $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150860 +0000	commit: The other day this did not work.
+EOF
+test_expect_success \
+	'git-commit logged updates' \
+	'diff expect .git/logs/$m'
+unset h_TEST h_OTHER
+
+test_expect_success \
+	'git-cat-file blob master:F (expect OTHER)' \
+	'test OTHER = $(git-cat-file blob master:F)'
+test_expect_success \
+	'git-cat-file blob master@{2005-05-26 23:30}:F (expect TEST)' \
+	'test TEST = $(git-cat-file blob "master@{2005-05-26 23:30}:F")'
+test_expect_success \
+	'git-cat-file blob master@{2005-05-26 23:42}:F (expect OTHER)' \
+	'test OTHER = $(git-cat-file blob "master@{2005-05-26 23:42}:F")'
+
+test_done

t/t3200-branch.sh

     'prepare an trivial repository' \
     'echo Hello > A &&
      git-update-index --add A &&
-     git-commit -m "Initial commit."'
+     git-commit -m "Initial commit." &&
+     HEAD=$(git-rev-parse --verify HEAD)'
 
 test_expect_success \
     'git branch --help should return success now.' \
     'git branch a/b/c should create a branch' \
     'git-branch a/b/c && test -f .git/refs/heads/a/b/c'
 
+cat >expect <<EOF
+0000000000000000000000000000000000000000 $HEAD $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150200 +0000	branch: Created from HEAD
+EOF
+test_expect_success \
+    'git branch -l d/e/f should create a branch and a log' \
+	'GIT_COMMITTER_DATE="2005-05-26 23:30" \
+     git-branch -l d/e/f &&
+	 test -f .git/refs/heads/d/e/f &&
+	 test -f .git/logs/refs/heads/d/e/f &&
+	 diff expect .git/logs/refs/heads/d/e/f'
+
+test_expect_success \
+    'git branch -d d/e/f should delete a branch and a log' \
+	'git-branch -d d/e/f &&
+	 test ! -f .git/refs/heads/d/e/f &&
+	 test ! -f .git/logs/refs/heads/d/e/f'
+
+cat >expect <<EOF
+0000000000000000000000000000000000000000 $HEAD $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1117150200 +0000	checkout: Created from master^0
+EOF
+test_expect_success \
+    'git checkout -b g/h/i -l should create a branch and a log' \
+	'GIT_COMMITTER_DATE="2005-05-26 23:30" \
+     git-checkout -b g/h/i -l master &&
+	 test -f .git/refs/heads/g/h/i &&
+	 test -f .git/logs/refs/heads/g/h/i &&
+	 diff expect .git/logs/refs/heads/g/h/i'
+
 test_done
 #include "cache.h"
 #include "refs.h"
 
-static const char git_update_ref_usage[] = "git-update-ref <refname> <value> [<oldval>]";
-
-static int re_verify(const char *path, unsigned char *oldsha1, unsigned char *currsha1)
-{
-	char buf[40];
-	int fd = open(path, O_RDONLY), nr;
-	if (fd < 0)
-		return -1;
-	nr = read(fd, buf, 40);
-	close(fd);
-	if (nr != 40 || get_sha1_hex(buf, currsha1) < 0)
-		return -1;
-	return memcmp(oldsha1, currsha1, 20) ? -1 : 0;
-}
+static const char git_update_ref_usage[] =
+"git-update-ref <refname> <value> [<oldval>] [-m <reason>]";
 
 int main(int argc, char **argv)
 {
-	char *hex;
-	const char *refname, *value, *oldval, *path;
-	char *lockpath;
-	unsigned char sha1[20], oldsha1[20], currsha1[20];
-	int fd, written;
+	const char *refname=NULL, *value=NULL, *oldval=NULL, *msg=NULL;
+	struct ref_lock *lock;
+	unsigned char sha1[20], oldsha1[20];
+	int i;
 
 	setup_git_directory();
 	git_config(git_default_config);
-	if (argc < 3 || argc > 4)
+
+	for (i = 1; i < argc; i++) {
+		if (!strcmp("-m", argv[i])) {
+			if (i+1 >= argc)
+				usage(git_update_ref_usage);
+			msg = argv[++i];
+			if (!*msg)
+				die("Refusing to perform update with empty message.");
+			if (strchr(msg, '\n'))
+				die("Refusing to perform update with \\n in message.");
+			continue;
+		}
+		if (!refname) {
+			refname = argv[i];
+			continue;
+		}
+		if (!value) {
+			value = argv[i];
+			continue;
+		}
+		if (!oldval) {
+			oldval = argv[i];
+			continue;
+		}
+	}
+	if (!refname || !value)
 		usage(git_update_ref_usage);
 
-	refname = argv[1];
-	value = argv[2];
-	oldval = argv[3];
 	if (get_sha1(value, sha1))
 		die("%s: not a valid SHA1", value);
 	memset(oldsha1, 0, 20);
 	if (oldval && get_sha1(oldval, oldsha1))
 		die("%s: not a valid old SHA1", oldval);
 
-	path = resolve_ref(git_path("%s", refname), currsha1, !!oldval);
-	if (!path)
-		die("No such ref: %s", refname);
-
-	if (oldval) {
-		if (memcmp(currsha1, oldsha1, 20))
-			die("Ref %s is at %s but expected %s", refname, sha1_to_hex(currsha1), sha1_to_hex(oldsha1));
-		/* Nothing to do? */
-		if (!memcmp(oldsha1, sha1, 20))
-			exit(0);
-	}
-	path = strdup(path);
-	lockpath = mkpath("%s.lock", path);
-	if (safe_create_leading_directories(lockpath) < 0)
-		die("Unable to create all of %s", lockpath);
-
-	fd = open(lockpath, O_CREAT | O_EXCL | O_WRONLY, 0666);
-	if (fd < 0)
-		die("Unable to create %s", lockpath);
-	hex = sha1_to_hex(sha1);
-	hex[40] = '\n';
-	written = write(fd, hex, 41);
-	close(fd);
-	if (written != 41) {
-		unlink(lockpath);
-		die("Unable to write to %s", lockpath);
-	}
-
-	/*
-	 * Re-read the ref after getting the lock to verify
-	 */
-	if (oldval && re_verify(path, oldsha1, currsha1) < 0) {
-		unlink(lockpath);
-		die("Ref lock failed");
-	}
-
-	/*
-	 * Finally, replace the old ref with the new one
-	 */
-	if (rename(lockpath, path) < 0) {
-		unlink(lockpath);
-		die("Unable to create %s", path);
-	}
+	lock = lock_any_ref_for_update(refname, oldval ? oldsha1 : NULL, 0);
+	if (!lock)
+		return 1;
+	if (write_ref_sha1(lock, sha1, msg) < 0)
+		return 1;
 	return 0;
 }