git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[EGIT PATCH 2/6] Add ref rename support to JGit

From
Robin Rosenberg <robin.rosenberg@dewire.com>
Date
May 19, 2009, 23:13 UTC
Message-ID
<1242774798-23639-3-git-send-email-robin.rosenberg@dewire.com>
In-Reply-To
<1242774798-23639-2-git-send-email-robin.rosenberg@dewire.com>

Now refs can be renamed. The intent is that should be safe. Only the named refs and associated logs are updated. Any symbolic refs referring to the renames branches are unaffected, except HEAD.

Signed-off-by: Robin Rosenberg <robin.rosenberg@dewire.com>
---
 .../tst/org/spearce/jgit/lib/RefUpdateTest.java    |  152 ++++++++++++++++++++
 .../src/org/spearce/jgit/lib/RefDatabase.java      |   28 ++++
 .../src/org/spearce/jgit/lib/RefLogWriter.java     |   20 +++-
 .../src/org/spearce/jgit/lib/RefRename.java        |  101 +++++++++++++
 .../src/org/spearce/jgit/lib/RefUpdate.java        |   29 +++-
 .../src/org/spearce/jgit/lib/Repository.java       |   16 ++
 6 files changed, 339 insertions(+), 7 deletions(-)
 create mode 100644 org.spearce.jgit/src/org/spearce/jgit/lib/RefRename.java
diff --git a/org.spearce.jgit.test/tst/org/spearce/jgit/lib/RefUpdateTest.java b/org.spearce.jgit.test/tst/org/spearce/jgit/lib/RefUpdateTest.java
index 55d7441..cd66eb1 100644
--- a/org.spearce.jgit.test/tst/org/spearce/jgit/lib/RefUpdateTest.java
+++ b/org.spearce.jgit.test/tst/org/spearce/jgit/lib/RefUpdateTest.java
@@ -155,4 +155,156 @@ public void testRefKeySameAsOrigName() {
 
 		}
 	}
+
+	public void testRenameBranchNoPreviousLog() throws IOException {
+		assertFalse("precondition, no log on old branchg", new File(db
+				.getDirectory(), "logs/refs/heads/b").exists());
+		ObjectId rb = db.resolve("refs/heads/b");
+		ObjectId oldHead = db.resolve(Constants.HEAD);
+		assertFalse(rb.equals(oldHead)); // assumption for this test
+		RefRename renameRef = db.renameRef("refs/heads/b",
+				"refs/heads/new/name");
+		Result result = renameRef.rename();
+		assertEquals(Result.RENAMED, result);
+		assertEquals(rb, db.resolve("refs/heads/new/name"));
+		assertNull(db.resolve("refs/heads/b"));
+		assertTrue(new File(db.getDirectory(), "logs/refs/heads/new/name")
+				.exists());
+		assertFalse(new File(db.getDirectory(), "logs/refs/heads/b").exists());
+		assertEquals(oldHead, db.resolve(Constants.HEAD));
+	}
+
+	public void testRenameBranchHasPreviousLog() throws IOException {
+		ObjectId rb = db.resolve("refs/heads/b");
+		ObjectId oldHead = db.resolve(Constants.HEAD);
+		assertFalse("precondition for this test, branch b != HEAD", rb
+				.equals(oldHead));
+		RefLogWriter.writeReflog(db, rb, rb, "Just a message", "refs/heads/b");
+		assertTrue("no log on old branch", new File(db.getDirectory(),
+				"logs/refs/heads/b").exists());
+		RefRename renameRef = db.renameRef("refs/heads/b",
+				"refs/heads/new/name");
+		Result result = renameRef.rename();
+		assertEquals(Result.RENAMED, result);
+		assertEquals(rb, db.resolve("refs/heads/new/name"));
+		assertNull(db.resolve("refs/heads/b"));
+		assertTrue(new File(db.getDirectory(), "logs/refs/heads/new/name")
+				.exists());
+		assertFalse(new File(db.getDirectory(), "logs/refs/heads/b").exists());
+		assertEquals(oldHead, db.resolve(Constants.HEAD));
+		// TODO: test content of log file
+	}
+
+	public void testRenameCurrentBranch() throws IOException {
+		ObjectId rb = db.resolve("refs/heads/b");
+		db.writeSymref(Constants.HEAD, "refs/heads/b");
+		ObjectId oldHead = db.resolve(Constants.HEAD);
+		assertTrue("internal test condition, b == HEAD", rb.equals(oldHead));
+		RefLogWriter.writeReflog(db, rb, rb, "Just a message", "refs/heads/b");
+		assertTrue("no log on old branch", new File(db.getDirectory(),
+				"logs/refs/heads/b").exists());
+		RefRename renameRef = db.renameRef("refs/heads/b",
+				"refs/heads/new/name");
+		Result result = renameRef.rename();
+		assertEquals(Result.RENAMED, result);
+		assertEquals(rb, db.resolve("refs/heads/new/name"));
+		assertNull(db.resolve("refs/heads/b"));
+		assertTrue(new File(db.getDirectory(), "logs/refs/heads/new/name")
+				.exists());
+		assertFalse(new File(db.getDirectory(), "logs/refs/heads/b").exists());
+		assertEquals(rb, db.resolve(Constants.HEAD));
+		// TODO: test content of log file
+	}
+
+	public void testRenameBranchAlsoInPack() throws IOException {
+		ObjectId rb = db.resolve("refs/heads/b");
+		ObjectId rb2 = db.resolve("refs/heads/b~1");
+		assertEquals(Ref.Storage.PACKED, db.getRef("refs/heads/b").getStorage());
+		RefUpdate updateRef = db.updateRef("refs/heads/b");
+		updateRef.setNewObjectId(rb2);
+		updateRef.setForceUpdate(true);
+		Result update = updateRef.update();
+		assertEquals("internal check new ref is loose", Result.FORCED, update);
+		assertEquals(Ref.Storage.LOOSE_PACKED, db.getRef("refs/heads/b").getStorage());
+		RefLogWriter.writeReflog(db, rb, rb, "Just a message", "refs/heads/b");
+		assertTrue("no log on old branch", new File(db.getDirectory(),
+				"logs/refs/heads/b").exists());
+		RefRename renameRef = db.renameRef("refs/heads/b",
+				"refs/heads/new/name");
+		Result result = renameRef.rename();
+		assertEquals(Result.RENAMED, result);
+		assertEquals(rb2, db.resolve("refs/heads/new/name"));
+		assertNull(db.resolve("refs/heads/b"));
+		assertTrue(new File(db.getDirectory(), "logs/refs/heads/new/name")
+				.exists());
+		assertFalse(new File(db.getDirectory(), "logs/refs/heads/b").exists());
+
+		// Create new Repository instance, to reread caches and make sure our
+		// assumptions are persistent.
+		Repository ndb = new Repository(db.getDirectory());
+		assertEquals(rb2, ndb.resolve("refs/heads/new/name"));
+		assertNull(ndb.resolve("refs/heads/b"));
+	}
+
+	public void testRenameBranchCannotLockFirstBranch() throws IOException {
+		// "someone" has branch b locked
+		assertTrue(new LockFile(new File(db.getDirectory(), "refs/heads/b"))
+				.lock());
+
+		// setup
+		ObjectId rb = db.resolve("refs/heads/b");
+		db.writeSymref(Constants.HEAD, "refs/heads/b");
+		ObjectId oldHead = db.resolve(Constants.HEAD);
+		assertTrue(rb.equals(oldHead)); // assumption for this test
+		RefLogWriter.writeReflog(db, rb, rb, "Just a message", "refs/heads/b");
+		assertTrue("internal check, we have a log", new File(db.getDirectory(),
+				"logs/refs/heads/b").exists());
+
+		// Now this is our test
+		RefRename renameRef = db.renameRef("refs/heads/b",
+				"refs/heads/new/name");
+		Result result = renameRef.rename();
+		assertEquals(Result.LOCK_FAILURE, result);
+
+		// Check that the involved refs are sane despite the failure
+		assertFalse(new File(db.getDirectory(), "refs/heads/new/name").exists());
+		assertFalse(new File(db.getDirectory(), "refs/heads/new/name.lock")
+				.exists());
+		assertFalse(new File(db.getDirectory(), "logs/refs/heads/b/lock")
+				.exists());
+		assertFalse(new File(db.getDirectory(), "logs/refs/heads/new/name.lock")
+				.exists());
+		assertEquals(rb, db.resolve(Constants.HEAD));
+	}
+
+	public void testRenameBranchCannotLockHEAD() throws IOException {
+		// setup
+		ObjectId rb = db.resolve("refs/heads/b");
+		db.writeSymref(Constants.HEAD, "refs/heads/b");
+
+		// "someone" has branch b locked
+		assertTrue(new LockFile(new File(db.getDirectory(), "HEAD")).lock());
+
+		ObjectId oldHead = db.resolve(Constants.HEAD);
+		assertTrue("internal test branch b == HEAD", rb.equals(oldHead));
+		RefLogWriter.writeReflog(db, rb, rb, "Just a message", "refs/heads/b");
+		assertTrue("no log on old branch", new File(db.getDirectory(),
+				"logs/refs/heads/b").exists());
+
+		// Now this is our test
+		RefRename renameRef = db.renameRef("refs/heads/b",
+				"refs/heads/new/name");
+		Result result = renameRef.rename();
+		assertEquals(Result.LOCK_FAILURE, result);
+
+		// Check that the involved refs are sane despite the failure
+		assertFalse(new File(db.getDirectory(), "refs/heads/new/name").exists());
+		assertFalse(new File(db.getDirectory(), "refs/heads/new/name.lock")
+				.exists());
+		assertFalse(new File(db.getDirectory(), "logs/refs/heads/b/lock")
+				.exists());
+		assertFalse(new File(db.getDirectory(), "logs/refs/heads/new/name.lock")
+				.exists());
+		assertEquals(rb, db.resolve(Constants.HEAD));
+	}
 }
diff --git a/org.spearce.jgit/src/org/spearce/jgit/lib/RefDatabase.java b/org.spearce.jgit/src/org/spearce/jgit/lib/RefDatabase.java
index 573fcac..9c0d114 100644
--- a/org.spearce.jgit/src/org/spearce/jgit/lib/RefDatabase.java
+++ b/org.spearce.jgit/src/org/spearce/jgit/lib/RefDatabase.java
@@ -146,6 +146,26 @@ synchronized (this) {
 	}
 
 	/**
+	 * An set of update operations for renaming a ref
+	 *
+	 * @param fromRef Old ref name
+	 * @param toRef New ref name
+	 * @return a RefUpdate operation to rename a ref
+	 * @throws IOException
+	 */
+	RefRename newRename(String fromRef, String toRef) throws IOException {
+		refreshPackedRefs();
+		Ref f = readRefBasic(fromRef, 0);
+		Ref t = readRefBasic(toRef, 0);
+		if (t != null)
+			throw new IOException("Ref rename target exists: " + t.getName());
+		t = new Ref(Ref.Storage.NEW, toRef, null);
+		RefUpdate refUpdateFrom = new RefUpdate(this, f, fileForRef(f.getName()));
+		RefUpdate refUpdateTo = new RefUpdate(this, t, fileForRef(t.getName()));
+		return new RefRename(refUpdateTo, refUpdateFrom);
+	}
+
+	/**
 	 * Writes a symref (e.g. HEAD) to disk
 	 * 
 	 * @param name
@@ -158,11 +178,19 @@ void link(final String name, final String target) throws IOException {
 		final byte[] content = Constants.encode("ref: " + target + "\n");
 		lockAndWriteFile(fileForRef(name), content);
 		synchronized (this) {
+			looseSymRefs.remove(name);
 			setModified();
 		}
 		db.fireRefsMaybeChanged();
 	}
 
+	void uncacheSymRef(String name) {
+		synchronized(this) {
+			looseSymRefs.remove(name);
+			setModified();
+		}
+	}
+
 	private void setModified() {
 		lastRefModification = refModificationCounter++;
 	}
diff --git a/org.spearce.jgit/src/org/spearce/jgit/lib/RefLogWriter.java b/org.spearce.jgit/src/org/spearce/jgit/lib/RefLogWriter.java
index a077051..2efa12d 100644
--- a/org.spearce.jgit/src/org/spearce/jgit/lib/RefLogWriter.java
+++ b/org.spearce.jgit/src/org/spearce/jgit/lib/RefLogWriter.java
@@ -44,7 +44,7 @@
 import java.io.IOException;
 
 /**
- * Utility class to add reflog entries
+ * Utility class to work with reflog files
  * 
  * @author Dave Watson
  */
@@ -58,6 +58,24 @@ static void append(final RefUpdate u, final String msg) throws IOException {
 		appendOneRecord(oldId, newId, ident, msg, db, u.getName());
 	}
 
+	static void renameTo(final Repository db, final RefUpdate from,
+			final RefUpdate to) throws IOException {
+		final File logdir = new File(db.getDirectory(), Constants.LOGS);
+		final File reflogFrom = new File(logdir, from.getName());
+		if (!reflogFrom.exists())
+			return;
+		final File reflogTo = new File(logdir, to.getName());
+		final File refdirTo = reflogTo.getParentFile();
+		if (!refdirTo.exists() && !refdirTo.mkdirs()) {
+			throw new IOException("Cannot create directory " + refdirTo);
+		}
+		if (!reflogFrom.renameTo(reflogTo)) {
+			reflogTo.delete(); // try
+			throw new IOException("Cannot rename " + reflogFrom + " to "
+					+ reflogTo);
+		}
+	}
+
 	private static void appendOneRecord(final ObjectId oldId,
 			final ObjectId newId, PersonIdent ident, final String msg,
 			final Repository db, final String refName) throws IOException {
diff --git a/org.spearce.jgit/src/org/spearce/jgit/lib/RefRename.java b/org.spearce.jgit/src/org/spearce/jgit/lib/RefRename.java
new file mode 100644
index 0000000..4ea9cfa
--- /dev/null
+++ b/org.spearce.jgit/src/org/spearce/jgit/lib/RefRename.java
@@ -0,0 +1,101 @@
+package org.spearce.jgit.lib;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.spearce.jgit.lib.RefUpdate.DeleteStore;
+import org.spearce.jgit.lib.RefUpdate.Result;
+import org.spearce.jgit.lib.RefUpdate.UpdateStore;
+
+/**
+ * A RefUpdate combination for renaming a ref
+ */
+public class RefRename {
+	private RefUpdate newToUpdate;
+
+	private RefUpdate oldFromDelete;
+
+	private Result renameResult;
+
+	RefRename(final RefUpdate toUpdate, final RefUpdate fromUpdate) {
+		newToUpdate = toUpdate;
+		oldFromDelete = fromUpdate;
+	}
+
+	/**
+	 * @return result of rename operation
+	 */
+	public Result getResult() {
+		return renameResult;
+	}
+
+	/**
+	 * @return the result of the new ref update
+	 * @throws IOException
+	 */
+	public Result rename() throws IOException {
+		List<LockFile> lockFiles = new ArrayList<LockFile>();
+		LockFile lockFileFrom = new LockFile(oldFromDelete.looseFile);
+		LockFile lockFileTo = new LockFile(newToUpdate.looseFile);
+		LockFile lockFileHEAD = new LockFile(new File(oldFromDelete.db
+				.getRepository().getDirectory(), Constants.HEAD));
+		lockFiles.add(lockFileTo);
+		lockFiles.add(lockFileFrom);
+		lockFiles.add(lockFileHEAD);
+		try {
+			for (LockFile l : lockFiles) {
+				if (!l.lock()) {
+					unlock(lockFiles);
+					return Result.LOCK_FAILURE;
+				}
+			}
+		} catch (RuntimeException e) {
+			unlock(lockFiles);
+			throw e;
+		} catch (IOException e) {
+			unlock(lockFiles);
+			throw e;
+		}
+		boolean renameHEADtoo = oldFromDelete.db.readRef(Constants.HEAD)
+				.getName().equals(oldFromDelete.getName());
+		try {
+			UpdateStore toStore = newToUpdate.newUpdateStore();
+			RefLogWriter.renameTo(oldFromDelete.getRepository(), oldFromDelete,
+					newToUpdate);
+			newToUpdate.setNewObjectId(oldFromDelete.getOldObjectId());
+			newToUpdate.setExpectedOldObjectId(oldFromDelete.getOldObjectId());
+			newToUpdate.setRefLogMessage("jgit branch: renamed "
+					+ oldFromDelete.getName() + " to "
+					+ oldFromDelete.getName(), false);
+			newToUpdate.result = toStore.store(lockFileTo, Result.RENAMED);
+			DeleteStore fromStore = oldFromDelete.newDeleteStore();
+			Result store = fromStore.store(lockFileFrom, Result.RENAMED);
+			if (store == Result.RENAMED && renameHEADtoo) {
+				final byte[] content = Constants.encode("ref: "
+						+ newToUpdate.getName() + "\n");
+				lockFileHEAD.write(content);
+				oldFromDelete.db.uncacheSymRef(Constants.HEAD);
+				if (!lockFileHEAD.commit())
+					throw new IOException("Failed to commit HEAD during rename");
+			}
+			oldFromDelete.db.getRepository().fireRefsMaybeChanged();
+			return store;
+		} catch (RuntimeException e) {
+			unlock(lockFiles);
+			throw e;
+		} catch (IOException e) {
+			System.err.println(e);
+			return Result.IO_FAILURE;
+		} finally {
+			unlock(lockFiles);
+		}
+	}
+
+	private void unlock(List<LockFile> lockFiles) {
+		for (LockFile lockFile : lockFiles) {
+			lockFile.unlock();
+		}
+	}
+}
\ No newline at end of file
diff --git a/org.spearce.jgit/src/org/spearce/jgit/lib/RefUpdate.java b/org.spearce.jgit/src/org/spearce/jgit/lib/RefUpdate.java
index a9ab73b..7a1074d 100644
--- a/org.spearce.jgit/src/org/spearce/jgit/lib/RefUpdate.java
+++ b/org.spearce.jgit/src/org/spearce/jgit/lib/RefUpdate.java
@@ -125,14 +125,20 @@
 		 * This kind of error doesn't include {@link #LOCK_FAILURE}, which is a
 		 * different case.
 		 */
-		IO_FAILURE
+		IO_FAILURE,
+
+		/**
+		 * The ref was renamed from another name
+		 * <p>
+		 */
+		RENAMED
 	}
 
 	/** Repository the ref is stored in. */
-	private final RefDatabase db;
+	final RefDatabase db;
 
 	/** Location of the loose file holding the value of this ref. */
-	private final File looseFile;
+	final File looseFile;
 
 	/** New value the caller wants this ref to have. */
 	private ObjectId newValue;
@@ -156,7 +162,7 @@
 	private ObjectId expValue;
 
 	/** Result of the update operation. */
-	private Result result = Result.NOT_ATTEMPTED;
+	Result result = Result.NOT_ATTEMPTED;
 
 	private final Ref ref;
 
@@ -478,6 +484,9 @@ else if (status == Result.FAST_FORWARD)
 				msg += ": fast forward";
 			else if (status == Result.NEW)
 				msg += ": created";
+			else if (status == Result.RENAMED) {
+				// don't amend the message here
+			}
 		}
 		RefLogWriter.append(this, msg);
 		if (!lock.commit())
@@ -495,7 +504,7 @@ abstract Result store(final LockFile lock, final Result status)
 				throws IOException;
 	}
 
-	private class UpdateStore extends Store {
+	class UpdateStore extends Store {
 
 		@Override
 		Result store(final LockFile lock, final Result status)
@@ -504,7 +513,7 @@ Result store(final LockFile lock, final Result status)
 		}
 	}
 
-	private class DeleteStore extends Store {
+	class DeleteStore extends Store {
 
 		@Override
 		Result store(LockFile lock, Result status) throws IOException {
@@ -553,4 +562,12 @@ private static int count(final String s, final char c) {
 		}
 		return count;
 	}
+
+	UpdateStore newUpdateStore() {
+		return new UpdateStore();
+	}
+
+	DeleteStore newDeleteStore() {
+		return new DeleteStore();
+	}
 }
diff --git a/org.spearce.jgit/src/org/spearce/jgit/lib/Repository.java b/org.spearce.jgit/src/org/spearce/jgit/lib/Repository.java
index 9bed1b7..08539fa 100644
--- a/org.spearce.jgit/src/org/spearce/jgit/lib/Repository.java
+++ b/org.spearce.jgit/src/org/spearce/jgit/lib/Repository.java
@@ -468,6 +468,22 @@ public RefUpdate updateRef(final String ref) throws IOException {
 	}
 
 	/**
+	 * Create a command to rename a ref in this repository
+	 *
+	 * @param fromRef
+	 *            name of ref to rename from
+	 * @param toRef
+	 *            name of ref to rename to
+	 * @return an update command that knows how to rename a branch to another.
+	 * @throws IOException
+	 *             the rename could not be performed.
+	 *
+	 */
+	public RefRename renameRef(final String fromRef, final String toRef) throws IOException {
+		return refs.newRename(fromRef, toRef);
+	}
+
+	/**
 	 * Parse a git revision string and return an object id.
 	 *
 	 * Currently supported is combinations of these.
-- 
1.6.3.dirty
Previous: Robin RosenbergNext: Robin Rosenberg
Message 8 of 43 in “Rename refs”
  1. 0/3 Rename refsRobin Rosenberg, May 6, 2009
  2. 1/3 Add ref rename support to JGitRobin Rosenberg, May 6, 2009
  3. 2/3 Use Constants.R_* in Branch dialogRobin Rosenberg, May 6, 2009
  4. 3/3 Add ref rename support to the branch dialogRobin Rosenberg, May 6, 2009
  5. Shawn O. PearceMay 7, 2009
  6. 0/6 Ref renameRobin Rosenberg, May 19, 2009
  7. 1/6 Make sure we get the right storage for loose/pack/loose and packed refsRobin Rosenberg, May 19, 2009
  8. 2/6 Add ref rename support to JGitRobin Rosenberg, May 19, 2009
  9. 3/6 Add ref rename support to the branch dialogRobin Rosenberg, May 19, 2009
  10. 4/6 Allow non-ASCII ref names when writing the packed-refs fileRobin Rosenberg, May 19, 2009
  11. 5/6 Use Constants.PACKED_REFS in RefWriterRobin Rosenberg, May 19, 2009
  12. 6/6 Improve error reporting in the branch dialogRobin Rosenberg, May 19, 2009
  13. Shawn O. PearceMay 20, 2009
  14. 00/10 Rename supportRobin Rosenberg, May 27, 2009
  15. 01/10 Make sure we get the right storage for loose/pack/loose and packed refsRobin Rosenberg, May 27, 2009
  16. 02/10 Add a toString for debugging to RemoteRefUpdateRobin Rosenberg, May 27, 2009
  17. 03/10 Add a toString to LockFileRobin Rosenberg, May 27, 2009
  18. 04/10 Add more tests for RefUpdateRobin Rosenberg, May 27, 2009
  19. 05/10 Do not write to the reflog unless the refupdate logmessage is setRobin Rosenberg, May 27, 2009
  20. 06/10 Add a utility method for shortening long ref names to short ones.Robin Rosenberg, May 27, 2009
  21. 07/10 Set a nice reflog message in the branch commandRobin Rosenberg, May 27, 2009
  22. 08/10 Add ref rename support to JGitRobin Rosenberg, May 27, 2009
  23. 09/10 Add ref rename support to the branch dialogRobin Rosenberg, May 27, 2009
  24. 10/10 Improve error reporting in the branch dialogRobin Rosenberg, May 27, 2009
  25. Shawn O. PearceJun 3, 2009
  26. Shawn O. PearceJun 3, 2009
  27. Robin RosenbergJun 7, 2009
  28. Shawn O. PearceJun 7, 2009
  29. Shawn O. PearceMay 20, 2009
  30. Robin RosenbergMay 21, 2009
  31. Shawn O. PearceMay 21, 2009
  32. 0/7 Ref rename support againRobin Rosenberg, Jun 10, 2009
  33. 1/7 Add methods to RawParseUtils for scanning backwards.Robin Rosenberg, Jun 10, 2009
  34. 2/7 Add a ref log reader classRobin Rosenberg, Jun 10, 2009
  35. 3/7 Do not write to the reflog unless the refupdate logmessage is setRobin Rosenberg, Jun 10, 2009
  36. 4/7 Add ref rename support to JGitRobin Rosenberg, Jun 10, 2009
  37. 5/7 Add ref rename support to the branch dialogRobin Rosenberg, Jun 10, 2009
  38. 6/7 Improve error reporting in the branch dialogRobin Rosenberg, Jun 10, 2009
  39. 7/7 Remove a TAB from the message Egit generates into the reflog on commitRobin Rosenberg, Jun 10, 2009
  40. Shawn O. PearceJun 12, 2009
  41. Warn for unlocalized string literals in ui plugin.Robin Rosenberg, Jun 14, 2009
  42. Add ref rename support to the branch dialogRobin Rosenberg, Jun 14, 2009
  43. 6/7 Improve error reporting in the branch dialogRobin Rosenberg, Jun 14, 2009

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.