ReceivePack: reject creation of one level refs such as refs/main
Cgit describes valid reference names in the `git-check-ref-format(1)`
manual page which lists the constraints. Excluding the `refs/` prefix,
the name must contain at least one slash to enforce a category such as
`heads/`, `tags/`.
On Gerrit, jGit accepts a user pushing to `refs/main` but when
replicating the reference using ssh and cgit, receive-pack on the remote
rejects it with:
Error: Failed replicate of refs/main to X, reason: funny refname
It is a special case in cgit `builtin/receive-pack.c`:
--------8<-----------8<-----------8<-----------8<-----------8<-----------
static const char *update(struct command *cmd, struct shallow_info *si)
{
...
/* only refs/... are allowed */
if (!starts_with(name, "refs/") || check_refname_format(name +
5, 0)) {
rp_error("refusing to create funny ref '%s' remotely",
name);
return "funny refname";
}
--------8<-----------8<-----------8<-----------8<-----------8<-----------
So that if the part coming after `refs/` has a single component
(`master`) it is rejected.
Bug: 582000
Change-Id: Ica2bdc58e81459f08cfe42c2961a3c06622c30df
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RepositoryResolveTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RepositoryResolveTest.java
index 7f1dfc3..2773715 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RepositoryResolveTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RepositoryResolveTest.java
@@ -277,6 +277,7 @@ public void invalidNames() throws AmbiguousObjectException, IOException {
assertTrue(Repository.isValidRefName("x/a]b")); // odd, yes..
assertTrue(Repository.isValidRefName("x/\u00a0")); // unicode is fine,
// even hard space
+ assertFalse(Repository.isValidRefName("single-component"));
assertFalse(Repository.isValidRefName("x/.a"));
assertFalse(Repository.isValidRefName("x/a."));
assertFalse(Repository.isValidRefName("x/a..b"));
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ReceivePackTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ReceivePackTest.java
index 595b970..bcc7082 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ReceivePackTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ReceivePackTest.java
@@ -42,22 +42,57 @@
package org.eclipse.jgit.transport;
+import java.util.ArrayList;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
-
-import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.eclipse.jgit.errors.PackProtocolException;
import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.junit.TestRepository.BranchBuilder;
+import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.transport.ReceiveCommand.Result;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.After;
+import org.junit.Before;
import org.junit.Test;
/** Tests for receive-pack utilities. */
public class ReceivePackTest {
+ private URIish uri;
+ private TestProtocol<Object> testProtocol;
+ private InMemoryRepository client;
+ private InMemoryRepository server;
+ private TestRepository clientTestRepo;
+ private TestRepository serverTestRepo;
+
+ @Before
+ public void setUp() throws Exception {
+ client = new InMemoryRepository(new DfsRepositoryDescription("client"));
+ server = new InMemoryRepository(new DfsRepositoryDescription("server"));
+ clientTestRepo = new TestRepository<>(client);
+ serverTestRepo = new TestRepository<>(server);
+
+ testProtocol = new TestProtocol<>(null, (Object req, Repository db) -> {
+ ReceivePack rp = new ReceivePack(db);
+ return rp;
+ });
+ uri = testProtocol.register(new Object(), server);
+ }
+
+ @After
+ public void tearDown() {
+ Transport.unregister(testProtocol);
+ }
+
@Test
public void parseCommand() throws Exception {
String o = "0000000000000000000000000000000000000000";
@@ -81,6 +116,54 @@ public void parseCommand() throws Exception {
assertParseCommandFails(o + " " + "X" + n.substring(1) + " " + r);
}
+ /**
+ * Ensure receive-pack rejects a push to a one level ref (refs/master).
+ */
+ @Test
+ public void rejectsCreationOfOneLevelRef() throws Exception {
+ // Create refs/master on the client so we can attempt to push it
+ RevCommit clientCommit;
+ BranchBuilder bb = clientTestRepo.branch("refs/master");
+ clientCommit = bb.commit().noFiles().message("Testing").create();
+
+ ArrayList<RemoteRefUpdate> refUpdates = new ArrayList<>();
+ refUpdates.add(new RemoteRefUpdate(
+ clientTestRepo.getRepository(), "refs/master", clientCommit,
+ "refs/master", false, /* force update */
+ null, /* no local tracking ref */
+ ObjectId.zeroId() // expected advertisement
+ ));
+ Transport tn = testProtocol.open(uri, client, "server");
+ PushResult result = tn.push(NullProgressMonitor.INSTANCE, refUpdates);
+ RemoteRefUpdate update = result.getRemoteUpdate("refs/master");
+
+ assertEquals(RemoteRefUpdate.Status.REJECTED_OTHER_REASON,
+ update.getStatus());
+ assertEquals(JGitText.get().funnyRefname, update.getMessage());
+ }
+
+ /**
+ * Allow deletion of one level refs (refs/master).
+ */
+ @Test
+ public void acceptsDeletionOfOneLevelRef() throws Exception {
+ BranchBuilder bb = serverTestRepo.branch("refs/master");
+ bb.update(bb.commit().noFiles().message("Testing").create());
+
+ ArrayList<RemoteRefUpdate> refUpdates = new ArrayList<>();
+ refUpdates.add(new RemoteRefUpdate(null, null, ObjectId.zeroId(),
+ "refs/master", false, // force update
+ null, // no local tracking ref
+ null // expected advertisement
+ ));
+ Transport tn = testProtocol.open(uri, client, "server");
+ PushResult result = tn.push(NullProgressMonitor.INSTANCE, refUpdates);
+ RemoteRefUpdate update = result.getRemoteUpdate("refs/master");
+
+ assertEquals(RemoteRefUpdate.Status.OK, update.getStatus());
+ assertEquals(null, update.getMessage());
+ }
+
private void assertParseCommandFails(String input) {
try {
ReceivePack.parseCommand(input);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java
index c9dc6da..01a4361 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java
@@ -1375,6 +1375,8 @@ public RepositoryState getRepositoryState() {
*
* For portability reasons '\' is excluded
*
+ * @see <a href="http://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html"
+ * >git-check-ref-format(1) manual page</a>
* @param refName a {@link java.lang.String} object.
* @return true if refName is a valid ref name
*/
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java
index 8b05d41..ec7d4c1 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java
@@ -1658,6 +1658,18 @@ private void validateCommands() {
continue;
}
+ if (cmd.getRefName().startsWith(Constants.R_REFS)
+ && !Repository.isValidRefName(
+ cmd.getRefName().substring(Constants.R_REFS.length())
+ )
+ ) {
+ // Reject the creation of one level references such as
+ // refs/master to match cgit implementation.
+ cmd.setResult(Result.REJECTED_OTHER_REASON,
+ JGitText.get().funnyRefname);
+ continue;
+ }
+
if (ref != null) {
// A well behaved client shouldn't have sent us a
// create command for a ref we advertised to it.
@@ -1753,8 +1765,18 @@ private void validateCommands() {
}
}
- if (!cmd.getRefName().startsWith(Constants.R_REFS)
- || !Repository.isValidRefName(cmd.getRefName())) {
+ if (!cmd.getRefName().startsWith(Constants.R_REFS)) {
+ cmd.setResult(Result.REJECTED_OTHER_REASON,
+ JGitText.get().funnyRefname);
+ }
+
+ if (!Repository.isValidRefName(cmd.getRefName())
+ // But accept deletion of one level refs (refs/main)
+ && !(
+ cmd.getType() == ReceiveCommand.Type.DELETE
+ && Repository.isValidRefName(cmd.getRefName().substring(Constants.R_REFS.length()))
+ )
+ ) {
cmd.setResult(Result.REJECTED_OTHER_REASON,
JGitText.get().funnyRefname);
}