Add "git-update-ref" to update the HEAD (or other) ref

Subsystems: kernel build + files below scripts/ (unless maintained elsewhere), the rest

2 messages, 1 author, 2016-06-15 · open the first message on its own page

Add "git-update-ref" to update the HEAD (or other) ref

From: Linus Torvalds <torvalds@osdl.org>
Date: 2016-06-15 22:42:07

This is a careful version of the script stuff that currently just
blindly writes HEAD with a new value.

You can use

	git-update-ref HEAD <newhead>

or

	git-update-ref HEAD <newhead> <oldhead>

where the latter version verifies that the old value of HEAD matches
oldhead.

It basically allows a "ref" file to be a symbolic pointer to another ref
file by starting with the four-byte header sequence of "ref:".

More importantly, it allows the update of a ref file to follow these 
symbolic pointers, whether they are symlinks or these "regular file 
symbolic refs".

NOTE! It follows _real_ symlinks only if they start with "refs/": 
otherwise it will just try to read them and update them as a regular file 
(ie it will allow the filesystem to follow them, but will overwrite such a 
symlink to somewhere else with a regular filename).

In general, using

	git-update-ref HEAD "$head"

should be a _lot_ safer than doing

	echo "$head" > "$GIT_DIR/HEAD"

both from a symlink following standpoint _and_ an error checking
standpoint.  The "refs/" rule for symlinks means that symlinks that point
to "outside" the tree are safe: they'll be followed for reading but not 
for writing (so we'll never write through a ref symlink to some other 
tree, if you have copied a whole archive by creating a symlink tree).

Signed-off-by: Linus Torvalds <torvalds@osdl.org>
---

This is independent of my previous patch, but in the same basic series. It 
is useful regardless of whether you use the new symbolic refs or not 
because of the much improved error checking.
diff --git a/Makefile b/Makefile
--- a/Makefile
+++ b/Makefile
@@ -114,6 +114,7 @@ PROGRAMS = \
 	git-ssh-upload git-tar-tree git-unpack-file \
 	git-unpack-objects git-update-index git-update-server-info \
 	git-upload-pack git-verify-pack git-write-tree \
+	git-update-ref \
 	$(SIMPLE_PROGRAMS)
 
 # Backward compatibility -- to be removed in 0.99.8
diff --git a/update-ref.c b/update-ref.c
new file mode 100644
--- /dev/null
+++ b/update-ref.c
@@ -0,0 +1,124 @@
+#include "cache.h"
+#include "refs.h"
+
+static const char git_update_ref_usage[] = "git-update-ref <refname> <value> [<oldval>]";
+
+#define MAXDEPTH 5
+
+const char *resolve_ref(const char *path, unsigned char *sha1)
+{
+	int depth = MAXDEPTH, len;
+	char buffer[256];
+
+	for (;;) {
+		struct stat st;
+		char *buf;
+		int fd;
+
+		if (--depth < 0)
+			return NULL;
+
+		/* Special case: non-existing file */
+		if (lstat(path, &st) < 0) {
+			if (errno != ENOENT)
+				return NULL;
+			memset(sha1, 0, 20);
+			return path;
+		}
+
+		/* Follow "normalized" - ie "refs/.." symlinks by hand */
+		if (S_ISLNK(st.st_mode)) {
+			len = readlink(path, buffer, sizeof(buffer)-1);
+			if (len >= 5 && !memcmp("refs/", buffer, 5)) {
+				path = git_path("%.*s", len, buffer);
+				continue;
+			}
+		}
+
+		/*
+		 * Anything else, just open it and try to use it as
+		 * a ref
+		 */
+		fd = open(path, O_RDONLY);
+		if (fd < 0)
+			return NULL;
+		len = read(fd, buffer, sizeof(buffer)-1);
+		close(fd);
+
+		/*
+		 * Is it a symbolic ref?
+		 */
+		if (len < 4 || memcmp("ref:", buffer, 4))
+			break;
+		buf = buffer + 4;
+		len -= 4;
+		while (len && isspace(*buf))
+			buf++, len--;
+		while (len && isspace(buf[len-1]))
+			buf[--len] = 0;
+		path = git_path("%.*s", len, buf);
+	}
+	if (len < 40 || get_sha1_hex(buffer, sha1))
+		return NULL;
+	return path;
+}
+
+int main(int argc, char **argv)
+{
+	char *hex;
+	const char *refname, *value, *oldval, *path, *lockpath;
+	unsigned char sha1[20], oldsha1[20], currsha1[20];
+	int fd, written;
+
+	setup_git_directory();
+	if (argc < 3 || argc > 4)
+		usage(git_update_ref_usage);
+
+	refname = argv[1];
+	value = argv[2];
+	oldval = argv[3];
+	if (get_sha1(value, sha1) < 0)
+		die("%s: not a valid SHA1", value);
+	memset(oldsha1, 0, 20);
+	if (oldval && get_sha1(oldval, oldsha1) < 0)
+		die("%s: not a valid old SHA1", oldval);
+
+	path = resolve_ref(git_path("%s", refname), currsha1);
+	if (!path)
+		die("No such ref: %s", refname);
+
+	if (oldval) {
+		if (memcmp(currsha1, oldsha1, 20))
+			die("Ref %s changed to %s", refname, sha1_to_hex(currsha1));
+		/* Nothing to do? */
+		if (!memcmp(oldsha1, sha1, 20))
+			exit(0);
+	}
+	path = strdup(path);
+	lockpath = mkpath("%s.lock", path);
+
+	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);
+	}
+		
+	/*
+	 * FIXME!
+	 *
+	 * We should re-read the old ref here, and re-verify that it
+	 * matches "oldsha1". Otherwise there's a small race.
+	 */
+
+	if (rename(lockpath, path) < 0) {
+		unlink(lockpath);
+		die("Unable to create %s", path);
+	}
+	return 0;
+}

Re: Add "git-update-ref" to update the HEAD (or other) ref

From: Linus Torvalds <torvalds@osdl.org>
Date: 2016-06-15 22:42:07


On Sun, 25 Sep 2005, Linus Torvalds wrote:
This is a careful version of the script stuff that currently just
blindly writes HEAD with a new value.
Btw, in case it wasn't clear from the patch, this only introduced 
mechanism, it didn't actually use it in any script.

But the point of the exercise is to move towards using git-update-ref in 
places like git-fetch.sh, which currently are doing some of it by hand 
(and not handling symlinks etc).

So you should be able to do something like

	..
	old=$(git-rev-parse --verify "$refname^0") >& /dev/null
	if [ "$old" ]; then
		mb=$(git-merge-base $new $old)
		if [ "$mb" != "$old" ]; then
			echo "$new is not a fast-forward of $old"
			[ "$force" ] || exit 1
			old=
		fi
	fi
	git-update-ref "$refname" $new $old

and the actual update phase will re-verify that "old" is still valid (if 
it exists at all).

Doing it by hand works, of course, but for example, if we have two
symlinks pointing to the same ref, the current locking in git-fetch.sh is
broken - it may lock the _symlink_, but since the other one _also_ points 
to the same thing, there's no locking of the _target_.

git-update-ref should do things like that right. Famous last words ;)

			Linus
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help