diff --git a/.buckconfig b/.buckconfig new file mode 100644 index 000000000..b2e07accc --- /dev/null +++ b/.buckconfig @@ -0,0 +1,15 @@ +[buildfile] + includes = //tools/default.defs + +[java] + src_roots = src, resources, tst + +[project] + ignore = .git + +[cache] + mode = dir + +[download] + maven_repo = http://repo1.maven.org/maven2 + in_build = true diff --git a/.buckversion b/.buckversion new file mode 100644 index 000000000..9daac2cea --- /dev/null +++ b/.buckversion @@ -0,0 +1 @@ +1b03b4313b91b634bd604fc3487a05f877e59dee diff --git a/.gitignore b/.gitignore index 139e5aee6..6c6219948 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /target /.project +/buck-cache +/buck-out diff --git a/BUCK b/BUCK new file mode 100644 index 000000000..f19b7bdc5 --- /dev/null +++ b/BUCK @@ -0,0 +1,45 @@ +java_library( + name = 'jgit', + exported_deps = ['//org.eclipse.jgit:jgit'], + visibility = ['PUBLIC'], +) + +genrule( + name = 'jgit_src', + cmd = 'ln -s $(location //org.eclipse.jgit:jgit_src) $OUT', + out = 'jgit_src.zip', + visibility = ['PUBLIC'], +) + +java_library( + name = 'jgit-servlet', + exported_deps = [ + ':jgit', + '//org.eclipse.jgit.http.server:jgit-servlet' + ], + visibility = ['PUBLIC'], +) + +java_library( + name = 'jgit-archive', + exported_deps = [ + ':jgit', + '//org.eclipse.jgit.archive:jgit-archive' + ], + visibility = ['PUBLIC'], +) + +java_library( + name = 'junit', + exported_deps = [ + ':jgit', + '//org.eclipse.jgit.junit:junit' + ], + visibility = ['PUBLIC'], +) + +genrule( + name = 'jgit_bin', + cmd = 'ln -s $(location //org.eclipse.jgit.pgm:jgit) $OUT', + out = 'jgit_bin', +) diff --git a/lib/BUCK b/lib/BUCK new file mode 100644 index 000000000..524612bde --- /dev/null +++ b/lib/BUCK @@ -0,0 +1,125 @@ +maven_jar( + name = 'jsch', + bin_sha1 = '658b682d5c817b27ae795637dfec047c63d29935', + src_sha1 = '791359d94d6edcace686a56d0727ee093a2f7c33', + group = 'com.jcraft', + artifact = 'jsch', + version = '0.1.53', +) + +maven_jar( + name = 'javaewah', + bin_sha1 = 'eceaf316a8faf0e794296ebe158ae110c7d72a5a', + src_sha1 = 'a50d78eb630e05439461f3130b94b3bcd1ea6f03', + group = 'com.googlecode.javaewah', + artifact = 'JavaEWAH', + version = '0.7.9', +) + +maven_jar( + name = 'httpcomponents', + bin_sha1 = '4c47155e3e6c9a41a28db36680b828ced53b8af4', + src_sha1 = 'af4d76be0c46ee26b0d9d1d4a34d244a633cac84', + group = 'org.apache.httpcomponents', + artifact = 'httpclient', + version = '4.3.6', +) + +maven_jar( + name = 'httpcore', + bin_sha1 = 'f91b7a4aadc5cf486df6e4634748d7dd7a73f06d', + src_sha1 = '1b0aa62a6a91e9fa00c16f0a4a2c874804ed3b1e', + group = 'org.apache.httpcomponents', + artifact = 'httpcore', + version = '4.3.3', +) + +maven_jar( + name = 'commons-logging', + bin_sha1 = 'f6f66e966c70a83ffbdb6f17a0919eaf7c8aca7f', + src_sha1 = '28bb0405fddaf04f15058fbfbe01fe2780d7d3b6', + group = 'commons-logging', + artifact = 'commons-logging', + version = '1.1.3', +) + +maven_jar( + name = 'slf4j-api', + bin_sha1 = '0081d61b7f33ebeab314e07de0cc596f8e858d97', + src_sha1 = '58d38f68d4a867d4552ae27960bb348d7eaa1297', + group = 'org.slf4j', + artifact = 'slf4j-api', + version = '1.7.2', +) + +maven_jar( + name = 'slf4j-simple', + bin_sha1 = '760055906d7353ba4f7ce1b8908bc6b2e91f39fa', + src_sha1 = '09474919128b3a7fcf21a5f9c907f5251f234544', + group = 'org.slf4j', + artifact = 'slf4j-simple', + version = '1.7.2', +) + +maven_jar( + name = 'servlet-api', + bin_sha1 = '3cd63d075497751784b2fa84be59432f4905bf7c', + src_sha1 = 'ab3976d4574c48d22dc1abf6a9e8bd0fdf928223', + group = 'javax.servlet', + artifact = 'javax.servlet-api', + version = '3.1.0', +) + +maven_jar( + name = 'commons-compress', + bin_sha1 = 'c7d9b580aff9e9f1998361f16578e63e5c064699', + src_sha1 = '396b81bdfd0fb617178e1707ef64832215307c78', + group = 'org.apache.commons', + artifact = 'commons-compress', + version = '1.6', +) + +maven_jar( + name = 'tukaani-xz', + bin_sha1 = '66db21c8484120cb6a51b5b3ea47b6f383942bec', + src_sha1 = '6396220725701d767c553902c41120d7bf38e9f5', + group = 'org.tukaani', + artifact = 'xz', + version = '1.3', +) + +maven_jar( + name = 'args4j', + bin_sha1 = '139441471327b9cc6d56436cb2a31e60eb6ed2ba', + src_sha1 = '22631b78cc8f60a6918557e8cbdb33e90f63a77f', + group = 'args4j', + artifact = 'args4j', + version = '2.0.15', +) + +maven_jar( + name = 'junit', + bin_sha1 = '4e031bb61df09069aeb2bffb4019e7a5034a4ee0', + src_sha1 = '28e0ad201304e4a4abf999ca0570b7cffc352c3c', + group = 'junit', + artifact = 'junit', + version = '4.11', +) + +maven_jar( + name = 'hamcrest-library', + bin_sha1 = '4785a3c21320980282f9f33d0d1264a69040538f', + src_sha1 = '047a7ee46628ab7133129cd7cef1e92657bc275e', + group = 'org.hamcrest', + artifact = 'hamcrest-library', + version = '1.3', +) + +maven_jar( + name = 'hamcrest-core', + bin_sha1 = '42a25dc3219429f0e5d060061f71acb49bf010a0', + src_sha1 = '1dc37250fbc78e23a65a67fbbaf71d2e9cbc3c0b', + group = 'org.hamcrest', + artifact = 'hamcrest-core', + version = '1.3', +) diff --git a/lib/jetty/BUCK b/lib/jetty/BUCK new file mode 100644 index 000000000..6e7dec306 --- /dev/null +++ b/lib/jetty/BUCK @@ -0,0 +1,56 @@ +VERSION = '9.2.13.v20150730' +GROUP = 'org.eclipse.jetty' + +maven_jar( + name = 'servlet', + bin_sha1 = '5ad6e38015a97ae9a60b6c2ad744ccfa9cf93a50', + src_sha1 = '78fbec19321150552d91f9e079c2f2ca33222b01', + group = GROUP, + artifact = 'jetty-servlet', + version = VERSION, +) + +maven_jar( + name = 'security', + bin_sha1 = 'cc7c7f27ec4cc279253be1675d9e47e58b995943', + src_sha1 = '75632ebdf8bd651faafb97106c92496db59e165d', + group = GROUP, + artifact = 'jetty-security', + version = VERSION, +) + +maven_jar( + name = 'server', + bin_sha1 = '5be7d1da0a7abffd142de3091d160717c120b6ab', + src_sha1 = '203e123f83efe2a5b8a9c74854c7897fe3563302', + group = GROUP, + artifact = 'jetty-server', + version = VERSION, +) + +maven_jar( + name = 'http', + bin_sha1 = '23a745d9177ef67ef53cc46b9b70c5870082efc2', + src_sha1 = '5f87f7ff2057cd4b0995bc4fffe17b2aff64c130', + group = GROUP, + artifact = 'jetty-http', + version = VERSION, +) + +maven_jar( + name = 'io', + bin_sha1 = '7a351e6a1b63dfd56b6632623f7ca2793ffb67ad', + src_sha1 = 'bbd61a84b748fc295456e1c5c3070aaf40a68f62', + group = GROUP, + artifact = 'jetty-io', + version = VERSION, +) + +maven_jar( + name = 'util', + bin_sha1 = 'c101476360a7cdd0670462de04053507d5e70c97', + src_sha1 = '15ceecce141971b4e0facb861b3d10120ad6ce03', + group = GROUP, + artifact = 'jetty-util', + version = VERSION, +) diff --git a/org.eclipse.jgit.archive/BUCK b/org.eclipse.jgit.archive/BUCK new file mode 100644 index 000000000..ae170324e --- /dev/null +++ b/org.eclipse.jgit.archive/BUCK @@ -0,0 +1,13 @@ +java_library( + name = 'jgit-archive', + srcs = glob( + ['src/**'], + excludes = ['src/org/eclipse/jgit/archive/FormatActivator.java'], + ), + resources = glob(['resources/**']), + provided_deps = [ + '//org.eclipse.jgit:jgit', + '//lib:commons-compress', + ], + visibility = ['PUBLIC'], +) diff --git a/org.eclipse.jgit.http.apache/BUCK b/org.eclipse.jgit.http.apache/BUCK new file mode 100644 index 000000000..f48f33a1a --- /dev/null +++ b/org.eclipse.jgit.http.apache/BUCK @@ -0,0 +1,12 @@ +java_library( + name = 'http-apache', + srcs = glob(['src/**']), + resources = glob(['resources/**']), + deps = [ + '//org.eclipse.jgit:jgit', + '//lib:commons-logging', + '//lib:httpcomponents', + '//lib:httpcore', + ], + visibility = ['PUBLIC'], +) diff --git a/org.eclipse.jgit.http.apache/META-INF/MANIFEST.MF b/org.eclipse.jgit.http.apache/META-INF/MANIFEST.MF index 07f364c53..8058f309f 100644 --- a/org.eclipse.jgit.http.apache/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.http.apache/META-INF/MANIFEST.MF @@ -7,7 +7,8 @@ Bundle-RequiredExecutionEnvironment: JavaSE-1.7 Bundle-Localization: plugin Bundle-Vendor: %Provider-Name Bundle-ActivationPolicy: lazy -Import-Package: org.apache.http;version="[4.1.0,5.0.0)", +Import-Package: org.apache.commons.logging;version="[1.1.1,2.0.0)", + org.apache.http;version="[4.1.0,5.0.0)", org.apache.http.client;version="[4.1.0,5.0.0)", org.apache.http.client.methods;version="[4.1.0,5.0.0)", org.apache.http.client.params;version="[4.1.0,5.0.0)", diff --git a/org.eclipse.jgit.http.apache/src/org/eclipse/jgit/transport/http/apache/HttpClientConnection.java b/org.eclipse.jgit.http.apache/src/org/eclipse/jgit/transport/http/apache/HttpClientConnection.java index d42d6f29e..de81bf82b 100644 --- a/org.eclipse.jgit.http.apache/src/org/eclipse/jgit/transport/http/apache/HttpClientConnection.java +++ b/org.eclipse.jgit.http.apache/src/org/eclipse/jgit/transport/http/apache/HttpClientConnection.java @@ -100,7 +100,7 @@ import org.eclipse.jgit.util.TemporaryBuffer.LocalFile; public class HttpClientConnection implements HttpConnection { HttpClient client; - String urlStr; + URL url; HttpUriRequest req; @@ -176,16 +176,19 @@ public class HttpClientConnection implements HttpConnection { /** * @param urlStr + * @throws MalformedURLException */ - public HttpClientConnection(String urlStr) { + public HttpClientConnection(String urlStr) throws MalformedURLException { this(urlStr, null); } /** * @param urlStr * @param proxy + * @throws MalformedURLException */ - public HttpClientConnection(String urlStr, Proxy proxy) { + public HttpClientConnection(String urlStr, Proxy proxy) + throws MalformedURLException { this(urlStr, proxy, null); } @@ -193,10 +196,12 @@ public class HttpClientConnection implements HttpConnection { * @param urlStr * @param proxy * @param cl + * @throws MalformedURLException */ - public HttpClientConnection(String urlStr, Proxy proxy, HttpClient cl) { + public HttpClientConnection(String urlStr, Proxy proxy, HttpClient cl) + throws MalformedURLException { this.client = cl; - this.urlStr = urlStr; + this.url = new URL(urlStr); this.proxy = proxy; } @@ -206,11 +211,7 @@ public class HttpClientConnection implements HttpConnection { } public URL getURL() { - try { - return new URL(urlStr); - } catch (MalformedURLException e) { - return null; - } + return url; } public String getResponseMessage() throws IOException { @@ -250,11 +251,11 @@ public class HttpClientConnection implements HttpConnection { public void setRequestMethod(String method) throws ProtocolException { this.method = method; if ("GET".equalsIgnoreCase(method)) //$NON-NLS-1$ - req = new HttpGet(urlStr); + req = new HttpGet(url.toString()); else if ("PUT".equalsIgnoreCase(method)) //$NON-NLS-1$ - req = new HttpPut(urlStr); + req = new HttpPut(url.toString()); else if ("POST".equalsIgnoreCase(method)) //$NON-NLS-1$ - req = new HttpPost(urlStr); + req = new HttpPost(url.toString()); else { this.method = null; throw new UnsupportedOperationException(); diff --git a/org.eclipse.jgit.http.server/BUCK b/org.eclipse.jgit.http.server/BUCK new file mode 100644 index 000000000..3743557aa --- /dev/null +++ b/org.eclipse.jgit.http.server/BUCK @@ -0,0 +1,10 @@ +java_library( + name = 'jgit-servlet', + srcs = glob(['src/**']), + resources = glob(['resources/**']), + provided_deps = [ + '//org.eclipse.jgit:jgit', + '//lib:servlet-api', + ], + visibility = ['PUBLIC'], +) diff --git a/org.eclipse.jgit.http.test/BUCK b/org.eclipse.jgit.http.test/BUCK new file mode 100644 index 000000000..d2ced7a24 --- /dev/null +++ b/org.eclipse.jgit.http.test/BUCK @@ -0,0 +1,40 @@ +TESTS = glob(['tst/**/*.java']) + +for t in TESTS: + n = t[len('tst/'):len(t)-len('.java')].replace('/', '.') + java_test( + name = n, + labels = ['http'], + srcs = [t], + deps = [ + ':helpers', + '//org.eclipse.jgit:jgit', + '//org.eclipse.jgit.http.apache:http-apache', + '//org.eclipse.jgit.http.server:jgit-servlet', + '//org.eclipse.jgit.junit:junit', + '//org.eclipse.jgit.junit.http:junit-http', + '//lib:hamcrest-core', + '//lib:hamcrest-library', + '//lib:junit', + '//lib:servlet-api', + '//lib/jetty:http', + '//lib/jetty:io', + '//lib/jetty:server', + '//lib/jetty:servlet', + '//lib/jetty:security', + '//lib/jetty:util', + ], + source_under_test = ['//org.eclipse.jgit.http.server:jgit-servlet'], + ) + +java_library( + name = 'helpers', + srcs = glob(['src/**/*.java']), + deps = [ + '//org.eclipse.jgit:jgit', + '//org.eclipse.jgit.http.server:jgit-servlet', + '//org.eclipse.jgit.junit:junit', + '//org.eclipse.jgit.junit.http:junit-http', + '//lib:junit', + ], +) diff --git a/org.eclipse.jgit.http.test/pom.xml b/org.eclipse.jgit.http.test/pom.xml index dd52a89e6..0af30f659 100644 --- a/org.eclipse.jgit.http.test/pom.xml +++ b/org.eclipse.jgit.http.test/pom.xml @@ -134,6 +134,10 @@ maven-surefire-plugin -Djava.io.tmpdir=${project.build.directory} -Xmx300m + + **/*Test.java + **/*Tests.java + diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/DumbClientDumbServerTest.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/DumbClientDumbServerTest.java index 362a09d64..677132d73 100644 --- a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/DumbClientDumbServerTest.java +++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/DumbClientDumbServerTest.java @@ -140,8 +140,7 @@ public class DumbClientDumbServerTest extends HttpTestCase { assertEquals("http", remoteURI.getScheme()); Map map; - Transport t = Transport.open(dst, remoteURI); - try { + try (Transport t = Transport.open(dst, remoteURI)) { // I didn't make up these public interface names, I just // approved them for inclusion into the code base. Sorry. // --spearce @@ -149,14 +148,9 @@ public class DumbClientDumbServerTest extends HttpTestCase { assertTrue("isa TransportHttp", t instanceof TransportHttp); assertTrue("isa HttpTransport", t instanceof HttpTransport); - FetchConnection c = t.openFetch(); - try { + try (FetchConnection c = t.openFetch()) { map = c.getRefsMap(); - } finally { - c.close(); } - } finally { - t.close(); } assertNotNull("have map of refs", map); @@ -201,11 +195,8 @@ public class DumbClientDumbServerTest extends HttpTestCase { Repository dst = createBareRepository(); assertFalse(dst.hasObject(A_txt)); - Transport t = Transport.open(dst, remoteURI); - try { + try (Transport t = Transport.open(dst, remoteURI)) { t.fetch(NullProgressMonitor.INSTANCE, mirror(master)); - } finally { - t.close(); } assertTrue(dst.hasObject(A_txt)); @@ -226,11 +217,8 @@ public class DumbClientDumbServerTest extends HttpTestCase { Repository dst = createBareRepository(); assertFalse(dst.hasObject(A_txt)); - Transport t = Transport.open(dst, remoteURI); - try { + try (Transport t = Transport.open(dst, remoteURI)) { t.fetch(NullProgressMonitor.INSTANCE, mirror(master)); - } finally { - t.close(); } assertTrue(dst.hasObject(A_txt)); @@ -265,8 +253,7 @@ public class DumbClientDumbServerTest extends HttpTestCase { final RevCommit Q = src.commit().create(); final Repository db = src.getRepository(); - Transport t = Transport.open(db, remoteURI); - try { + try (Transport t = Transport.open(db, remoteURI)) { try { t.push(NullProgressMonitor.INSTANCE, push(src, Q)); fail("push incorrectly completed against a dumb server"); @@ -274,8 +261,6 @@ public class DumbClientDumbServerTest extends HttpTestCase { String exp = "remote does not support smart HTTP push"; assertEquals(exp, nse.getMessage()); } - } finally { - t.close(); } } } diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/GitServletResponseTests.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/GitServletResponseTests.java index fba1a5264..4b15d4b53 100644 --- a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/GitServletResponseTests.java +++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/GitServletResponseTests.java @@ -60,6 +60,7 @@ import org.eclipse.jgit.http.server.GitServlet; import org.eclipse.jgit.http.server.resolver.DefaultReceivePackFactory; import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.junit.http.HttpTestCase; +import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectChecker; @@ -221,8 +222,9 @@ public class GitServletResponseTests extends HttpTestCase { preHook = null; oc = new ObjectChecker() { @Override - public void checkCommit(byte[] raw) throws CorruptObjectException { - throw new IllegalStateException(); + public void checkCommit(AnyObjectId id, byte[] raw) + throws CorruptObjectException { + throw new CorruptObjectException("refusing all commits"); } }; diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/HttpClientTests.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/HttpClientTests.java index 6fb130231..ce7844278 100644 --- a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/HttpClientTests.java +++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/HttpClientTests.java @@ -157,8 +157,7 @@ public class HttpClientTests extends HttpTestCase { public void testRepositoryNotFound_Dumb() throws Exception { URIish uri = toURIish("/dumb.none/not-found"); Repository dst = createBareRepository(); - Transport t = Transport.open(dst, uri); - try { + try (Transport t = Transport.open(dst, uri)) { try { t.openFetch(); fail("connection opened to not found repository"); @@ -167,8 +166,6 @@ public class HttpClientTests extends HttpTestCase { + "/info/refs?service=git-upload-pack not found"; assertEquals(exp, err.getMessage()); } - } finally { - t.close(); } } @@ -176,8 +173,7 @@ public class HttpClientTests extends HttpTestCase { public void testRepositoryNotFound_Smart() throws Exception { URIish uri = toURIish("/smart.none/not-found"); Repository dst = createBareRepository(); - Transport t = Transport.open(dst, uri); - try { + try (Transport t = Transport.open(dst, uri)) { try { t.openFetch(); fail("connection opened to not found repository"); @@ -186,8 +182,6 @@ public class HttpClientTests extends HttpTestCase { + "/info/refs?service=git-upload-pack not found"; assertEquals(exp, err.getMessage()); } - } finally { - t.close(); } } @@ -201,16 +195,9 @@ public class HttpClientTests extends HttpTestCase { Repository dst = createBareRepository(); Ref head; - Transport t = Transport.open(dst, dumbAuthNoneURI); - try { - FetchConnection c = t.openFetch(); - try { - head = c.getRef(Constants.HEAD); - } finally { - c.close(); - } - } finally { - t.close(); + try (Transport t = Transport.open(dst, dumbAuthNoneURI); + FetchConnection c = t.openFetch()) { + head = c.getRef(Constants.HEAD); } assertNotNull("has " + Constants.HEAD, head); assertEquals(Q, head.getObjectId()); @@ -225,16 +212,9 @@ public class HttpClientTests extends HttpTestCase { Repository dst = createBareRepository(); Ref head; - Transport t = Transport.open(dst, dumbAuthNoneURI); - try { - FetchConnection c = t.openFetch(); - try { - head = c.getRef(Constants.HEAD); - } finally { - c.close(); - } - } finally { - t.close(); + try (Transport t = Transport.open(dst, dumbAuthNoneURI); + FetchConnection c = t.openFetch()) { + head = c.getRef(Constants.HEAD); } assertNull("has no " + Constants.HEAD, head); } @@ -249,16 +229,9 @@ public class HttpClientTests extends HttpTestCase { Repository dst = createBareRepository(); Ref head; - Transport t = Transport.open(dst, smartAuthNoneURI); - try { - FetchConnection c = t.openFetch(); - try { - head = c.getRef(Constants.HEAD); - } finally { - c.close(); - } - } finally { - t.close(); + try (Transport t = Transport.open(dst, smartAuthNoneURI); + FetchConnection c = t.openFetch()) { + head = c.getRef(Constants.HEAD); } assertNotNull("has " + Constants.HEAD, head); assertEquals(Q, head.getObjectId()); @@ -268,16 +241,13 @@ public class HttpClientTests extends HttpTestCase { public void testListRemote_Smart_WithQueryParameters() throws Exception { URIish myURI = toURIish("/snone/do?r=1&p=test.git"); Repository dst = createBareRepository(); - Transport t = Transport.open(dst, myURI); - try { + try (Transport t = Transport.open(dst, myURI)) { try { t.openFetch(); fail("test did not fail to find repository as expected"); } catch (NoRemoteRepositoryException err) { // expected } - } finally { - t.close(); } List requests = getRequests(); @@ -296,62 +266,52 @@ public class HttpClientTests extends HttpTestCase { @Test public void testListRemote_Dumb_NeedsAuth() throws Exception { Repository dst = createBareRepository(); - Transport t = Transport.open(dst, dumbAuthBasicURI); - try { + try (Transport t = Transport.open(dst, dumbAuthBasicURI)) { try { t.openFetch(); fail("connection opened even info/refs needs auth basic"); } catch (TransportException err) { String exp = dumbAuthBasicURI + ": " - + JGitText.get().notAuthorized; + + JGitText.get().noCredentialsProvider; assertEquals(exp, err.getMessage()); } - } finally { - t.close(); } } @Test public void testListRemote_Dumb_Auth() throws Exception { Repository dst = createBareRepository(); - Transport t = Transport.open(dst, dumbAuthBasicURI); - t.setCredentialsProvider(new UsernamePasswordCredentialsProvider( - AppServer.username, AppServer.password)); - try { - t.openFetch(); - } finally { - t.close(); + try (Transport t = Transport.open(dst, dumbAuthBasicURI)) { + t.setCredentialsProvider(new UsernamePasswordCredentialsProvider( + AppServer.username, AppServer.password)); + t.openFetch().close(); } - t = Transport.open(dst, dumbAuthBasicURI); - t.setCredentialsProvider(new UsernamePasswordCredentialsProvider( - AppServer.username, "")); - try { - t.openFetch(); - fail("connection opened even info/refs needs auth basic and we provide wrong password"); - } catch (TransportException err) { - String exp = dumbAuthBasicURI + ": " - + JGitText.get().notAuthorized; - assertEquals(exp, err.getMessage()); - } finally { - t.close(); + try (Transport t = Transport.open(dst, dumbAuthBasicURI)) { + t.setCredentialsProvider(new UsernamePasswordCredentialsProvider( + AppServer.username, "")); + try { + t.openFetch(); + fail("connection opened even info/refs needs auth basic and we provide wrong password"); + } catch (TransportException err) { + String exp = dumbAuthBasicURI + ": " + + JGitText.get().notAuthorized; + assertEquals(exp, err.getMessage()); + } } } @Test public void testListRemote_Smart_UploadPackNeedsAuth() throws Exception { Repository dst = createBareRepository(); - Transport t = Transport.open(dst, smartAuthBasicURI); - try { + try (Transport t = Transport.open(dst, smartAuthBasicURI)) { try { t.openFetch(); fail("connection opened even though service disabled"); } catch (TransportException err) { String exp = smartAuthBasicURI + ": " - + JGitText.get().notAuthorized; + + JGitText.get().noCredentialsProvider; assertEquals(exp, err.getMessage()); } - } finally { - t.close(); } } @@ -363,33 +323,24 @@ public class HttpClientTests extends HttpTestCase { cfg.save(); Repository dst = createBareRepository(); - Transport t = Transport.open(dst, smartAuthNoneURI); - try { + try (Transport t = Transport.open(dst, smartAuthNoneURI)) { try { t.openFetch(); fail("connection opened even though service disabled"); } catch (TransportException err) { - String exp = smartAuthNoneURI + ": Git access forbidden"; + String exp = smartAuthNoneURI + ": " + + JGitText.get().serviceNotEnabledNoName; assertEquals(exp, err.getMessage()); } - } finally { - t.close(); } } @Test public void testListRemoteWithoutLocalRepository() throws Exception { - Transport t = Transport.open(smartAuthNoneURI); - try { - FetchConnection c = t.openFetch(); - try { - Ref head = c.getRef(Constants.HEAD); - assertNotNull(head); - } finally { - c.close(); - } - } finally { - t.close(); + try (Transport t = Transport.open(smartAuthNoneURI); + FetchConnection c = t.openFetch()) { + Ref head = c.getRef(Constants.HEAD); + assertNotNull(head); } } } diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java index 9ca0789e2..82861ed9b 100644 --- a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java +++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java @@ -211,8 +211,7 @@ public class SmartClientSmartServerTest extends HttpTestCase { assertEquals("http", remoteURI.getScheme()); Map map; - Transport t = Transport.open(dst, remoteURI); - try { + try (Transport t = Transport.open(dst, remoteURI)) { // I didn't make up these public interface names, I just // approved them for inclusion into the code base. Sorry. // --spearce @@ -226,8 +225,6 @@ public class SmartClientSmartServerTest extends HttpTestCase { } finally { c.close(); } - } finally { - t.close(); } assertNotNull("have map of refs", map); @@ -257,8 +254,7 @@ public class SmartClientSmartServerTest extends HttpTestCase { public void testListRemote_BadName() throws IOException, URISyntaxException { Repository dst = createBareRepository(); URIish uri = new URIish(this.remoteURI.toString() + ".invalid"); - Transport t = Transport.open(dst, uri); - try { + try (Transport t = Transport.open(dst, uri)) { try { t.openFetch(); fail("fetch connection opened"); @@ -266,8 +262,6 @@ public class SmartClientSmartServerTest extends HttpTestCase { assertEquals(uri + ": Git repository not found", notFound.getMessage()); } - } finally { - t.close(); } List requests = getRequests(); @@ -288,11 +282,8 @@ public class SmartClientSmartServerTest extends HttpTestCase { Repository dst = createBareRepository(); assertFalse(dst.hasObject(A_txt)); - Transport t = Transport.open(dst, remoteURI); - try { + try (Transport t = Transport.open(dst, remoteURI)) { t.fetch(NullProgressMonitor.INSTANCE, mirror(master)); - } finally { - t.close(); } assertTrue(dst.hasObject(A_txt)); @@ -331,11 +322,8 @@ public class SmartClientSmartServerTest extends HttpTestCase { // Bootstrap by doing the clone. // TestRepository dst = createTestRepository(); - Transport t = Transport.open(dst.getRepository(), remoteURI); - try { + try (Transport t = Transport.open(dst.getRepository(), remoteURI)) { t.fetch(NullProgressMonitor.INSTANCE, mirror(master)); - } finally { - t.close(); } assertEquals(B, dst.getRepository().exactRef(master).getObjectId()); List cloneRequests = getRequests(); @@ -352,11 +340,8 @@ public class SmartClientSmartServerTest extends HttpTestCase { // Now incrementally update. // - t = Transport.open(dst.getRepository(), remoteURI); - try { + try (Transport t = Transport.open(dst.getRepository(), remoteURI)) { t.fetch(NullProgressMonitor.INSTANCE, mirror(master)); - } finally { - t.close(); } assertEquals(Z, dst.getRepository().exactRef(master).getObjectId()); @@ -394,11 +379,8 @@ public class SmartClientSmartServerTest extends HttpTestCase { // Bootstrap by doing the clone. // TestRepository dst = createTestRepository(); - Transport t = Transport.open(dst.getRepository(), remoteURI); - try { + try (Transport t = Transport.open(dst.getRepository(), remoteURI)) { t.fetch(NullProgressMonitor.INSTANCE, mirror(master)); - } finally { - t.close(); } assertEquals(B, dst.getRepository().exactRef(master).getObjectId()); List cloneRequests = getRequests(); @@ -418,11 +400,8 @@ public class SmartClientSmartServerTest extends HttpTestCase { // Now incrementally update. // - t = Transport.open(dst.getRepository(), remoteURI); - try { + try (Transport t = Transport.open(dst.getRepository(), remoteURI)) { t.fetch(NullProgressMonitor.INSTANCE, mirror(master)); - } finally { - t.close(); } assertEquals(Z, dst.getRepository().exactRef(master).getObjectId()); @@ -474,8 +453,7 @@ public class SmartClientSmartServerTest extends HttpTestCase { Repository dst = createBareRepository(); assertFalse(dst.hasObject(A_txt)); - Transport t = Transport.open(dst, brokenURI); - try { + try (Transport t = Transport.open(dst, brokenURI)) { try { t.fetch(NullProgressMonitor.INSTANCE, mirror(master)); fail("fetch completed despite upload-pack being broken"); @@ -485,8 +463,6 @@ public class SmartClientSmartServerTest extends HttpTestCase { + " received Content-Type text/plain; charset=UTF-8"; assertEquals(exp, err.getMessage()); } - } finally { - t.close(); } List requests = getRequests(); @@ -517,12 +493,10 @@ public class SmartClientSmartServerTest extends HttpTestCase { final RevCommit Q = src.commit().add("Q", Q_txt).create(); final Repository db = src.getRepository(); final String dstName = Constants.R_HEADS + "new.branch"; - Transport t; // push anonymous shouldn't be allowed. // - t = Transport.open(db, remoteURI); - try { + try (Transport t = Transport.open(db, remoteURI)) { final String srcExpr = Q.name(); final boolean forceUpdate = false; final String localName = null; @@ -538,8 +512,6 @@ public class SmartClientSmartServerTest extends HttpTestCase { + JGitText.get().authenticationNotSupported; assertEquals(exp, e.getMessage()); } - } finally { - t.close(); } List requests = getRequests(); @@ -560,12 +532,10 @@ public class SmartClientSmartServerTest extends HttpTestCase { final RevCommit Q = src.commit().add("Q", Q_txt).create(); final Repository db = src.getRepository(); final String dstName = Constants.R_HEADS + "new.branch"; - Transport t; enableReceivePack(); - t = Transport.open(db, remoteURI); - try { + try (Transport t = Transport.open(db, remoteURI)) { final String srcExpr = Q.name(); final boolean forceUpdate = false; final String localName = null; @@ -574,8 +544,6 @@ public class SmartClientSmartServerTest extends HttpTestCase { RemoteRefUpdate u = new RemoteRefUpdate(src.getRepository(), srcExpr, dstName, forceUpdate, localName, oldId); t.push(NullProgressMonitor.INSTANCE, Collections.singleton(u)); - } finally { - t.close(); } assertTrue(remoteRepository.hasObject(Q_txt)); @@ -633,7 +601,6 @@ public class SmartClientSmartServerTest extends HttpTestCase { final RevCommit Q = src.commit().add("Q", Q_bin).create(); final Repository db = src.getRepository(); final String dstName = Constants.R_HEADS + "new.branch"; - Transport t; enableReceivePack(); @@ -642,8 +609,7 @@ public class SmartClientSmartServerTest extends HttpTestCase { cfg.setInt("http", null, "postbuffer", 8 * 1024); cfg.save(); - t = Transport.open(db, remoteURI); - try { + try (Transport t = Transport.open(db, remoteURI)) { final String srcExpr = Q.name(); final boolean forceUpdate = false; final String localName = null; @@ -652,8 +618,6 @@ public class SmartClientSmartServerTest extends HttpTestCase { RemoteRefUpdate u = new RemoteRefUpdate(src.getRepository(), srcExpr, dstName, forceUpdate, localName, oldId); t.push(NullProgressMonitor.INSTANCE, Collections.singleton(u)); - } finally { - t.close(); } assertTrue(remoteRepository.hasObject(Q_bin)); diff --git a/org.eclipse.jgit.junit.http/BUCK b/org.eclipse.jgit.junit.http/BUCK new file mode 100644 index 000000000..68976a68a --- /dev/null +++ b/org.eclipse.jgit.junit.http/BUCK @@ -0,0 +1,18 @@ +java_library( + name = 'junit-http', + srcs = glob(['src/**']), + resources = glob(['resources/**']), + provided_deps = [ + '//org.eclipse.jgit:jgit', + '//org.eclipse.jgit.http.server:jgit-servlet', + '//org.eclipse.jgit.junit:junit', + '//lib:junit', + '//lib:servlet-api', + '//lib/jetty:http', + '//lib/jetty:server', + '//lib/jetty:servlet', + '//lib/jetty:security', + '//lib/jetty:util', + ], + visibility = ['PUBLIC'], +) diff --git a/org.eclipse.jgit.junit/BUCK b/org.eclipse.jgit.junit/BUCK new file mode 100644 index 000000000..7e2543220 --- /dev/null +++ b/org.eclipse.jgit.junit/BUCK @@ -0,0 +1,10 @@ +java_library( + name = 'junit', + srcs = glob(['src/**']), + resources = glob(['resources/**']), + provided_deps = [ + '//org.eclipse.jgit:jgit', + '//lib:junit', + ], + visibility = ['PUBLIC'], +) diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java index ac9685d37..8439c39c8 100644 --- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java +++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java @@ -822,7 +822,7 @@ public class TestRepository { break; final byte[] bin = db.open(o, o.getType()).getCachedBytes(); - oc.checkCommit(bin); + oc.checkCommit(o, bin); assertHash(o, bin); } @@ -832,7 +832,7 @@ public class TestRepository { break; final byte[] bin = db.open(o, o.getType()).getCachedBytes(); - oc.check(o.getType(), bin); + oc.check(o, o.getType(), bin); assertHash(o, bin); } } @@ -866,7 +866,7 @@ public class TestRepository { Set all = new HashSet(); for (Ref r : db.getAllRefs().values()) all.add(r.getObjectId()); - pw.preparePack(m, all, Collections. emptySet()); + pw.preparePack(m, all, PackWriter.NONE); final ObjectId name = pw.computeName(); @@ -1155,8 +1155,7 @@ public class TestRepository { return self; } - private void insertChangeId(org.eclipse.jgit.lib.CommitBuilder c) - throws IOException { + private void insertChangeId(org.eclipse.jgit.lib.CommitBuilder c) { if (changeId == null) return; int idx = ChangeIdUtil.indexOfChangeId(message, "\n"); diff --git a/org.eclipse.jgit.pgm.test/BUCK b/org.eclipse.jgit.pgm.test/BUCK new file mode 100644 index 000000000..a3859c9b4 --- /dev/null +++ b/org.eclipse.jgit.pgm.test/BUCK @@ -0,0 +1,38 @@ +TESTS = glob(['tst/**/*.java']) + +for t in TESTS: + n = t[len('tst/'):len(t)-len('.java')].replace('/', '.') + java_test( + name = n, + labels = ['pgm'], + srcs = [t], + deps = [ + ':helpers', + '//org.eclipse.jgit:jgit', + '//org.eclipse.jgit.archive:jgit-archive', + '//org.eclipse.jgit.junit:junit', + '//org.eclipse.jgit.pgm:pgm', + '//lib:hamcrest-core', + '//lib:hamcrest-library', + '//lib:javaewah', + '//lib:junit', + '//lib:slf4j-api', + '//lib:slf4j-simple', + '//lib:commons-compress', + '//lib:tukaani-xz', + ], + source_under_test = ['//org.eclipse.jgit.pgm:pgm'], + vm_args = ['-Xmx256m', '-Dfile.encoding=UTF-8'], + ) + +java_library( + name = 'helpers', + srcs = glob(['src/**/*.java']), + deps = [ + '//org.eclipse.jgit:jgit', + '//org.eclipse.jgit.pgm:pgm', + '//org.eclipse.jgit.junit:junit', + '//lib:args4j', + '//lib:junit', + ], +) diff --git a/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF index db23c3b55..2514fdff7 100644 --- a/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF @@ -11,6 +11,7 @@ Import-Package: org.eclipse.jgit.api;version="[4.2.0,4.3.0)", org.eclipse.jgit.api.errors;version="[4.2.0,4.3.0)", org.eclipse.jgit.diff;version="[4.2.0,4.3.0)", org.eclipse.jgit.dircache;version="[4.2.0,4.3.0)", + org.eclipse.jgit.internal.storage.file;version="4.2.0", org.eclipse.jgit.junit;version="[4.2.0,4.3.0)", org.eclipse.jgit.lib;version="[4.2.0,4.3.0)", org.eclipse.jgit.merge;version="[4.2.0,4.3.0)", diff --git a/org.eclipse.jgit.pgm.test/org.eclipse.jgit.pgm--All-Tests (Java8) (de).launch b/org.eclipse.jgit.pgm.test/org.eclipse.jgit.pgm--All-Tests (Java8) (de).launch new file mode 100644 index 000000000..5c137f28f --- /dev/null +++ b/org.eclipse.jgit.pgm.test/org.eclipse.jgit.pgm--All-Tests (Java8) (de).launch @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/org.eclipse.jgit.pgm.test/src/org/eclipse/jgit/lib/CLIRepositoryTestCase.java b/org.eclipse.jgit.pgm.test/src/org/eclipse/jgit/lib/CLIRepositoryTestCase.java index 559a6d5d4..a6af077aa 100644 --- a/org.eclipse.jgit.pgm.test/src/org/eclipse/jgit/lib/CLIRepositoryTestCase.java +++ b/org.eclipse.jgit.pgm.test/src/org/eclipse/jgit/lib/CLIRepositoryTestCase.java @@ -46,12 +46,16 @@ import static org.junit.Assert.assertEquals; import java.io.File; import java.io.IOException; +import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import org.eclipse.jgit.junit.JGitTestUtil; import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase; import org.eclipse.jgit.pgm.CLIGitCommand; +import org.eclipse.jgit.pgm.CLIGitCommand.Result; +import org.eclipse.jgit.pgm.TextBuiltin.TerminatedByHelpException; import org.junit.Before; public class CLIRepositoryTestCase extends LocalDiskRepositoryTestCase { @@ -69,13 +73,59 @@ public class CLIRepositoryTestCase extends LocalDiskRepositoryTestCase { trash = db.getWorkTree(); } + /** + * Executes specified git commands (with arguments) + * + * @param cmds + * each string argument must be a valid git command line, e.g. + * "git branch -h" + * @return command output + * @throws Exception + */ + protected String[] executeUnchecked(String... cmds) throws Exception { + List result = new ArrayList(cmds.length); + for (String cmd : cmds) { + result.addAll(CLIGitCommand.executeUnchecked(cmd, db)); + } + return result.toArray(new String[0]); + } + + /** + * Executes specified git commands (with arguments), throws exception and + * stops execution on first command which output contains a 'fatal:' error + * + * @param cmds + * each string argument must be a valid git command line, e.g. + * "git branch -h" + * @return command output + * @throws Exception + */ protected String[] execute(String... cmds) throws Exception { List result = new ArrayList(cmds.length); - for (String cmd : cmds) - result.addAll(CLIGitCommand.execute(cmd, db)); + for (String cmd : cmds) { + Result r = CLIGitCommand.executeRaw(cmd, db); + if (r.ex instanceof TerminatedByHelpException) { + result.addAll(r.errLines()); + } else if (r.ex != null) { + throw r.ex; + } + result.addAll(r.outLines()); + } return result.toArray(new String[0]); } + /** + * @param link + * the path of the symbolic link to create + * @param target + * the target of the symbolic link + * @return the path to the symbolic link + * @throws Exception + */ + protected Path writeLink(String link, String target) throws Exception { + return JGitTestUtil.writeLink(db, link, target); + } + protected File writeTrashFile(final String name, final String data) throws IOException { return JGitTestUtil.writeTrashFile(db, name, data); @@ -173,15 +223,36 @@ public class CLIRepositoryTestCase extends LocalDiskRepositoryTestCase { } protected void assertArrayOfLinesEquals(String[] expected, String[] actual) { - assertEquals(toText(expected), toText(actual)); + assertEquals(toString(expected), toString(actual)); + } + + public static String toString(String... lines) { + return toString(Arrays.asList(lines)); } - private static String toText(String[] lines) { + public static String toString(List lines) { StringBuilder b = new StringBuilder(); for (String s : lines) { - b.append(s); - b.append('\n'); + // trim indentation, to simplify tests + s = s.trim(); + if (s != null && !s.isEmpty()) { + b.append(s); + b.append('\n'); + } + } + // delete last line break to allow simpler tests with one line compare + if (b.length() > 0 && b.charAt(b.length() - 1) == '\n') { + b.deleteCharAt(b.length() - 1); } return b.toString(); } + + public static boolean contains(List lines, String str) { + for (String s : lines) { + if (s.contains(str)) { + return true; + } + } + return false; + } } diff --git a/org.eclipse.jgit.pgm.test/src/org/eclipse/jgit/pgm/CLIGitCommand.java b/org.eclipse.jgit.pgm.test/src/org/eclipse/jgit/pgm/CLIGitCommand.java index d77b1505a..3f396563c 100644 --- a/org.eclipse.jgit.pgm.test/src/org/eclipse/jgit/pgm/CLIGitCommand.java +++ b/org.eclipse.jgit.pgm.test/src/org/eclipse/jgit/pgm/CLIGitCommand.java @@ -42,71 +42,140 @@ */ package org.eclipse.jgit.pgm; +import static org.junit.Assert.assertNull; + import java.io.ByteArrayOutputStream; -import java.text.MessageFormat; +import java.io.File; + +import java.io.IOException; +import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; +import org.eclipse.jgit.internal.storage.file.FileRepository; import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.pgm.internal.CLIText; -import org.eclipse.jgit.pgm.opt.CmdLineParser; -import org.eclipse.jgit.pgm.opt.SubcommandHandler; +import org.eclipse.jgit.pgm.TextBuiltin.TerminatedByHelpException; import org.eclipse.jgit.util.IO; -import org.kohsuke.args4j.Argument; -public class CLIGitCommand { - @Argument(index = 0, metaVar = "metaVar_command", required = true, handler = SubcommandHandler.class) - private TextBuiltin subcommand; +public class CLIGitCommand extends Main { - @Argument(index = 1, metaVar = "metaVar_arg") - private List arguments = new ArrayList(); + private final Result result; - public TextBuiltin getSubcommand() { - return subcommand; + private final Repository db; + + public CLIGitCommand(Repository db) { + super(); + this.db = db; + result = new Result(); } - public List getArguments() { - return arguments; + /** + * Executes git commands (with arguments) specified on the command line. The + * git repository (same for all commands) can be specified via system + * property "-Dgit_work_tree=path_to_work_tree". If the property is not set, + * current directory is used. + * + * @param args + * each element in the array must be a valid git command line, + * e.g. "git branch -h" + * @throws Exception + */ + public static void main(String[] args) throws Exception { + String workDir = System.getProperty("git_work_tree"); + if (workDir == null) { + workDir = "."; + System.out.println( + "System property 'git_work_tree' not specified, using current directory: " + + new File(workDir).getAbsolutePath()); + } + try (Repository db = new FileRepository(workDir + "/.git")) { + for (String cmd : args) { + List result = execute(cmd, db); + for (String line : result) { + System.out.println(line); + } + } + } } public static List execute(String str, Repository db) throws Exception { + Result result = executeRaw(str, db); + return getOutput(result); + } + + public static Result executeRaw(String str, Repository db) + throws Exception { + CLIGitCommand cmd = new CLIGitCommand(db); + cmd.run(str); + return cmd.result; + } + + public static List executeUnchecked(String str, Repository db) + throws Exception { + CLIGitCommand cmd = new CLIGitCommand(db); + try { + cmd.run(str); + return getOutput(cmd.result); + } catch (Throwable e) { + return cmd.result.errLines(); + } + } + + private static List getOutput(Result result) { + if (result.ex instanceof TerminatedByHelpException) { + return result.errLines(); + } + return result.outLines(); + } + + private void run(String commandLine) throws Exception { + String[] argv = convertToMainArgs(commandLine); try { - return IO.readLines(new String(rawExecute(str, db))); - } catch (Die e) { - return IO.readLines(MessageFormat.format(CLIText.get().fatalError, - e.getMessage())); + super.run(argv); + } catch (TerminatedByHelpException e) { + // this is not a failure, super called exit() on help + } finally { + writer.flush(); } } - public static byte[] rawExecute(String str, Repository db) + private static String[] convertToMainArgs(String str) throws Exception { String[] args = split(str); - if (!args[0].equalsIgnoreCase("git") || args.length < 2) + if (!args[0].equalsIgnoreCase("git") || args.length < 2) { throw new IllegalArgumentException( "Expected 'git []', was:" + str); + } String[] argv = new String[args.length - 1]; System.arraycopy(args, 1, argv, 0, args.length - 1); + return argv; + } - CLIGitCommand bean = new CLIGitCommand(); - final CmdLineParser clp = new CmdLineParser(bean); - clp.parseArgument(argv); - - final TextBuiltin cmd = bean.getSubcommand(); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - cmd.outs = baos; - if (cmd.requiresRepository()) - cmd.init(db, null); - else - cmd.init(null, null); - try { - cmd.execute(bean.getArguments().toArray( - new String[bean.getArguments().size()])); - } finally { - if (cmd.outw != null) - cmd.outw.flush(); + @Override + PrintWriter createErrorWriter() { + return new PrintWriter(result.err); + } + + void init(final TextBuiltin cmd) throws IOException { + cmd.outs = result.out; + cmd.errs = result.err; + super.init(cmd); + } + + @Override + protected Repository openGitDir(String aGitdir) throws IOException { + assertNull(aGitdir); + return db; + } + + @Override + void exit(int status, Exception t) throws Exception { + if (t == null) { + t = new IllegalStateException(Integer.toString(status)); } - return baos.toByteArray(); + result.ex = t; + throw t; } /** @@ -164,4 +233,36 @@ public class CLIGitCommand { return list.toArray(new String[list.size()]); } + public static class Result { + public final ByteArrayOutputStream out = new ByteArrayOutputStream(); + + public final ByteArrayOutputStream err = new ByteArrayOutputStream(); + + public Exception ex; + + public byte[] outBytes() { + return out.toByteArray(); + } + + public byte[] errBytes() { + return err.toByteArray(); + } + + public String outString() { + return out.toString(); + } + + public List outLines() { + return IO.readLines(out.toString()); + } + + public String errString() { + return err.toString(); + } + + public List errLines() { + return IO.readLines(err.toString()); + } + } + } diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/AddTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/AddTest.java index 4253080a6..3edd9b88e 100644 --- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/AddTest.java +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/AddTest.java @@ -45,15 +45,12 @@ package org.eclipse.jgit.pgm; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; - -import java.lang.Exception; -import java.lang.String; +import static org.junit.Assert.fail; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.lib.CLIRepositoryTestCase; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; public class AddTest extends CLIRepositoryTestCase { @@ -66,14 +63,16 @@ public class AddTest extends CLIRepositoryTestCase { git = new Git(db); } - @Ignore("args4j exit()s on error instead of throwing, JVM goes down") @Test public void testAddNothing() throws Exception { - assertEquals("fatal: Argument \"filepattern\" is required", // - execute("git add")[0]); + try { + execute("git add"); + fail("Must die"); + } catch (Die e) { + // expected, requires argument + } } - @Ignore("args4j exit()s for --help, too") @Test public void testAddUsage() throws Exception { execute("git add --help"); diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ArchiveTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ArchiveTest.java index 4222a2dcc..a503ffdad 100644 --- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ArchiveTest.java +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ArchiveTest.java @@ -52,17 +52,15 @@ import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; -import java.io.InputStreamReader; import java.io.IOException; +import java.io.InputStreamReader; import java.io.OutputStream; -import java.lang.Object; -import java.lang.String; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.Callable; -import java.util.concurrent.Executors; import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; @@ -71,9 +69,7 @@ import org.eclipse.jgit.api.Git; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.lib.CLIRepositoryTestCase; import org.eclipse.jgit.lib.FileMode; -import org.eclipse.jgit.pgm.CLIGitCommand; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; public class ArchiveTest extends CLIRepositoryTestCase { @@ -89,25 +85,26 @@ public class ArchiveTest extends CLIRepositoryTestCase { emptyTree = db.resolve("HEAD^{tree}").abbreviate(12).name(); } - @Ignore("Some versions of java.util.zip refuse to write an empty ZIP") @Test public void testEmptyArchive() throws Exception { - byte[] result = CLIGitCommand.rawExecute( - "git archive --format=zip " + emptyTree, db); + byte[] result = CLIGitCommand.executeRaw( + "git archive --format=zip " + emptyTree, db).outBytes(); assertArrayEquals(new String[0], listZipEntries(result)); } @Test public void testEmptyTar() throws Exception { - byte[] result = CLIGitCommand.rawExecute( - "git archive --format=tar " + emptyTree, db); + byte[] result = CLIGitCommand.executeRaw( + "git archive --format=tar " + emptyTree, db).outBytes(); assertArrayEquals(new String[0], listTarEntries(result)); } @Test public void testUnrecognizedFormat() throws Exception { - String[] expect = new String[] { "fatal: Unknown archive format 'nonsense'" }; - String[] actual = execute("git archive --format=nonsense " + emptyTree); + String[] expect = new String[] { + "fatal: Unknown archive format 'nonsense'", "" }; + String[] actual = executeUnchecked( + "git archive --format=nonsense " + emptyTree); assertArrayEquals(expect, actual); } @@ -120,8 +117,8 @@ public class ArchiveTest extends CLIRepositoryTestCase { git.add().addFilepattern("c").call(); git.commit().setMessage("populate toplevel").call(); - byte[] result = CLIGitCommand.rawExecute( - "git archive --format=zip HEAD", db); + byte[] result = CLIGitCommand.executeRaw( + "git archive --format=zip HEAD", db).outBytes(); assertArrayEquals(new String[] { "a", "c" }, listZipEntries(result)); } @@ -135,8 +132,8 @@ public class ArchiveTest extends CLIRepositoryTestCase { @Test public void testDefaultFormatIsTar() throws Exception { commitGreeting(); - byte[] result = CLIGitCommand.rawExecute( - "git archive HEAD", db); + byte[] result = CLIGitCommand.executeRaw( + "git archive HEAD", db).outBytes(); assertArrayEquals(new String[] { "greeting" }, listTarEntries(result)); } @@ -302,8 +299,8 @@ public class ArchiveTest extends CLIRepositoryTestCase { git.add().addFilepattern("b").call(); git.commit().setMessage("add subdir").call(); - byte[] result = CLIGitCommand.rawExecute( - "git archive --format=zip master", db); + byte[] result = CLIGitCommand.executeRaw( + "git archive --format=zip master", db).outBytes(); String[] expect = { "a", "b.c", "b0c", "b/", "b/a", "b/b", "c" }; String[] actual = listZipEntries(result); @@ -328,8 +325,8 @@ public class ArchiveTest extends CLIRepositoryTestCase { git.add().addFilepattern("b").call(); git.commit().setMessage("add subdir").call(); - byte[] result = CLIGitCommand.rawExecute( - "git archive --format=tar master", db); + byte[] result = CLIGitCommand.executeRaw( + "git archive --format=tar master", db).outBytes(); String[] expect = { "a", "b.c", "b0c", "b/", "b/a", "b/b", "c" }; String[] actual = listTarEntries(result); @@ -349,8 +346,8 @@ public class ArchiveTest extends CLIRepositoryTestCase { @Test public void testArchivePrefixOption() throws Exception { commitBazAndFooSlashBar(); - byte[] result = CLIGitCommand.rawExecute( - "git archive --prefix=x/ --format=zip master", db); + byte[] result = CLIGitCommand.executeRaw( + "git archive --prefix=x/ --format=zip master", db).outBytes(); String[] expect = { "x/baz", "x/foo/", "x/foo/bar" }; String[] actual = listZipEntries(result); @@ -362,8 +359,8 @@ public class ArchiveTest extends CLIRepositoryTestCase { @Test public void testTarPrefixOption() throws Exception { commitBazAndFooSlashBar(); - byte[] result = CLIGitCommand.rawExecute( - "git archive --prefix=x/ --format=tar master", db); + byte[] result = CLIGitCommand.executeRaw( + "git archive --prefix=x/ --format=tar master", db).outBytes(); String[] expect = { "x/baz", "x/foo/", "x/foo/bar" }; String[] actual = listTarEntries(result); @@ -381,8 +378,8 @@ public class ArchiveTest extends CLIRepositoryTestCase { @Test public void testPrefixDoesNotNormalizeDoubleSlash() throws Exception { commitFoo(); - byte[] result = CLIGitCommand.rawExecute( - "git archive --prefix=x// --format=zip master", db); + byte[] result = CLIGitCommand.executeRaw( + "git archive --prefix=x// --format=zip master", db).outBytes(); String[] expect = { "x//foo" }; assertArrayEquals(expect, listZipEntries(result)); } @@ -390,8 +387,8 @@ public class ArchiveTest extends CLIRepositoryTestCase { @Test public void testPrefixDoesNotNormalizeDoubleSlashInTar() throws Exception { commitFoo(); - byte[] result = CLIGitCommand.rawExecute( - "git archive --prefix=x// --format=tar master", db); + byte[] result = CLIGitCommand.executeRaw( + "git archive --prefix=x// --format=tar master", db).outBytes(); String[] expect = { "x//foo" }; assertArrayEquals(expect, listTarEntries(result)); } @@ -408,8 +405,8 @@ public class ArchiveTest extends CLIRepositoryTestCase { @Test public void testPrefixWithoutTrailingSlash() throws Exception { commitBazAndFooSlashBar(); - byte[] result = CLIGitCommand.rawExecute( - "git archive --prefix=my- --format=zip master", db); + byte[] result = CLIGitCommand.executeRaw( + "git archive --prefix=my- --format=zip master", db).outBytes(); String[] expect = { "my-baz", "my-foo/", "my-foo/bar" }; String[] actual = listZipEntries(result); @@ -421,8 +418,8 @@ public class ArchiveTest extends CLIRepositoryTestCase { @Test public void testTarPrefixWithoutTrailingSlash() throws Exception { commitBazAndFooSlashBar(); - byte[] result = CLIGitCommand.rawExecute( - "git archive --prefix=my- --format=tar master", db); + byte[] result = CLIGitCommand.executeRaw( + "git archive --prefix=my- --format=tar master", db).outBytes(); String[] expect = { "my-baz", "my-foo/", "my-foo/bar" }; String[] actual = listTarEntries(result); @@ -441,8 +438,8 @@ public class ArchiveTest extends CLIRepositoryTestCase { git.submoduleAdd().setURI("./.").setPath("b").call().close(); git.commit().setMessage("add submodule").call(); - byte[] result = CLIGitCommand.rawExecute( - "git archive --format=zip master", db); + byte[] result = CLIGitCommand.executeRaw( + "git archive --format=zip master", db).outBytes(); String[] expect = { ".gitmodules", "a", "b/", "c" }; String[] actual = listZipEntries(result); @@ -461,8 +458,8 @@ public class ArchiveTest extends CLIRepositoryTestCase { git.submoduleAdd().setURI("./.").setPath("b").call().close(); git.commit().setMessage("add submodule").call(); - byte[] result = CLIGitCommand.rawExecute( - "git archive --format=tar master", db); + byte[] result = CLIGitCommand.executeRaw( + "git archive --format=tar master", db).outBytes(); String[] expect = { ".gitmodules", "a", "b/", "c" }; String[] actual = listTarEntries(result); @@ -491,8 +488,8 @@ public class ArchiveTest extends CLIRepositoryTestCase { git.commit().setMessage("three files with different modes").call(); - byte[] zipData = CLIGitCommand.rawExecute( - "git archive --format=zip master", db); + byte[] zipData = CLIGitCommand.executeRaw( + "git archive --format=zip master", db).outBytes(); writeRaw("zip-with-modes.zip", zipData); assertContainsEntryWithMode("zip-with-modes.zip", "-rw-", "plain"); assertContainsEntryWithMode("zip-with-modes.zip", "-rwx", "executable"); @@ -520,8 +517,8 @@ public class ArchiveTest extends CLIRepositoryTestCase { git.commit().setMessage("three files with different modes").call(); - byte[] archive = CLIGitCommand.rawExecute( - "git archive --format=tar master", db); + byte[] archive = CLIGitCommand.executeRaw( + "git archive --format=tar master", db).outBytes(); writeRaw("with-modes.tar", archive); assertTarContainsEntry("with-modes.tar", "-rw-r--r--", "plain"); assertTarContainsEntry("with-modes.tar", "-rwxr-xr-x", "executable"); @@ -543,8 +540,8 @@ public class ArchiveTest extends CLIRepositoryTestCase { git.add().addFilepattern("1234567890").call(); git.commit().setMessage("file with long name").call(); - byte[] result = CLIGitCommand.rawExecute( - "git archive --format=zip HEAD", db); + byte[] result = CLIGitCommand.executeRaw( + "git archive --format=zip HEAD", db).outBytes(); assertArrayEquals(l.toArray(new String[l.size()]), listZipEntries(result)); } @@ -563,8 +560,8 @@ public class ArchiveTest extends CLIRepositoryTestCase { git.add().addFilepattern("1234567890").call(); git.commit().setMessage("file with long name").call(); - byte[] result = CLIGitCommand.rawExecute( - "git archive --format=tar HEAD", db); + byte[] result = CLIGitCommand.executeRaw( + "git archive --format=tar HEAD", db).outBytes(); assertArrayEquals(l.toArray(new String[l.size()]), listTarEntries(result)); } @@ -576,8 +573,8 @@ public class ArchiveTest extends CLIRepositoryTestCase { git.add().addFilepattern("xyzzy").call(); git.commit().setMessage("add file with content").call(); - byte[] result = CLIGitCommand.rawExecute( - "git archive --format=zip HEAD", db); + byte[] result = CLIGitCommand.executeRaw( + "git archive --format=zip HEAD", db).outBytes(); assertArrayEquals(new String[] { payload }, zipEntryContent(result, "xyzzy")); } @@ -589,8 +586,8 @@ public class ArchiveTest extends CLIRepositoryTestCase { git.add().addFilepattern("xyzzy").call(); git.commit().setMessage("add file with content").call(); - byte[] result = CLIGitCommand.rawExecute( - "git archive --format=tar HEAD", db); + byte[] result = CLIGitCommand.executeRaw( + "git archive --format=tar HEAD", db).outBytes(); assertArrayEquals(new String[] { payload }, tarEntryContent(result, "xyzzy")); } diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/BranchTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/BranchTest.java index d1bd5bace..55f4d8b1b 100644 --- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/BranchTest.java +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/BranchTest.java @@ -43,11 +43,17 @@ package org.eclipse.jgit.pgm; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.File; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.lib.CLIRepositoryTestCase; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.pgm.internal.CLIText; import org.eclipse.jgit.revwalk.RevCommit; import org.junit.Before; import org.junit.Test; @@ -62,10 +68,20 @@ public class BranchTest extends CLIRepositoryTestCase { } } + @Test + public void testHelpAfterDelete() throws Exception { + String err = toString(executeUnchecked("git branch -d")); + String help = toString(executeUnchecked("git branch -h")); + String errAndHelp = toString(executeUnchecked("git branch -d -h")); + assertEquals(CLIText.fatalError(CLIText.get().branchNameRequired), err); + assertEquals(toString(err, help), errAndHelp); + } + @Test public void testList() throws Exception { + assertEquals("* master", toString(execute("git branch"))); assertEquals("* master 6fd41be initial commit", - execute("git branch -v")[0]); + toString(execute("git branch -v"))); } @Test @@ -73,26 +89,188 @@ public class BranchTest extends CLIRepositoryTestCase { RefUpdate updateRef = db.updateRef(Constants.HEAD, true); updateRef.setNewObjectId(db.resolve("6fd41be")); updateRef.update(); - assertEquals("* (no branch) 6fd41be initial commit", - execute("git branch -v")[0]); + assertEquals( + toString("* (no branch) 6fd41be initial commit", + "master 6fd41be initial commit"), + toString(execute("git branch -v"))); } @Test public void testListContains() throws Exception { try (Git git = new Git(db)) { - git.branchCreate().setName("initial").call(); + git.branchCreate().setName("initial").call(); RevCommit second = git.commit().setMessage("second commit") .call(); - assertArrayOfLinesEquals(new String[] { " initial", "* master", "" }, - execute("git branch --contains 6fd41be")); - assertArrayOfLinesEquals(new String[] { "* master", "" }, - execute("git branch --contains " + second.name())); + assertEquals(toString(" initial", "* master"), + toString(execute("git branch --contains 6fd41be"))); + assertEquals("* master", + toString(execute("git branch --contains " + second.name()))); } } @Test public void testExistingBranch() throws Exception { assertEquals("fatal: A branch named 'master' already exists.", - execute("git branch master")[0]); + toString(executeUnchecked("git branch master"))); + } + + @Test + public void testRenameSingleArg() throws Exception { + try { + toString(execute("git branch -m")); + fail("Must die"); + } catch (Die e) { + // expected, requires argument + } + String result = toString(execute("git branch -m slave")); + assertEquals("", result); + result = toString(execute("git branch -a")); + assertEquals("* slave", result); + } + + @Test + public void testRenameTwoArgs() throws Exception { + String result = toString(execute("git branch -m master slave")); + assertEquals("", result); + result = toString(execute("git branch -a")); + assertEquals("* slave", result); + } + + @Test + public void testCreate() throws Exception { + try { + toString(execute("git branch a b")); + fail("Must die"); + } catch (Die e) { + // expected, too many arguments + } + String result = toString(execute("git branch second")); + assertEquals("", result); + result = toString(execute("git branch")); + assertEquals(toString("* master", "second"), result); + result = toString(execute("git branch -v")); + assertEquals(toString("* master 6fd41be initial commit", + "second 6fd41be initial commit"), result); + } + + @Test + public void testDelete() throws Exception { + try { + toString(execute("git branch -d")); + fail("Must die"); + } catch (Die e) { + // expected, requires argument + } + String result = toString(execute("git branch second")); + assertEquals("", result); + result = toString(execute("git branch -d second")); + assertEquals("", result); + result = toString(execute("git branch")); + assertEquals("* master", result); + } + + @Test + public void testDeleteMultiple() throws Exception { + String result = toString(execute("git branch second", + "git branch third", "git branch fourth")); + assertEquals("", result); + result = toString(execute("git branch -d second third fourth")); + assertEquals("", result); + result = toString(execute("git branch")); + assertEquals("* master", result); + } + + @Test + public void testDeleteForce() throws Exception { + try { + toString(execute("git branch -D")); + fail("Must die"); + } catch (Die e) { + // expected, requires argument + } + String result = toString(execute("git branch second")); + assertEquals("", result); + result = toString(execute("git checkout second")); + assertEquals("Switched to branch 'second'", result); + + File a = writeTrashFile("a", "a"); + assertTrue(a.exists()); + execute("git add a", "git commit -m 'added a'"); + + result = toString(execute("git checkout master")); + assertEquals("Switched to branch 'master'", result); + + result = toString(execute("git branch")); + assertEquals(toString("* master", "second"), result); + + try { + toString(execute("git branch -d second")); + fail("Must die"); + } catch (Die e) { + // expected, the current HEAD is on second and not merged to master + } + result = toString(execute("git branch -D second")); + assertEquals("", result); + + result = toString(execute("git branch")); + assertEquals("* master", result); + } + + @Test + public void testDeleteForceMultiple() throws Exception { + String result = toString(execute("git branch second", + "git branch third", "git branch fourth")); + + assertEquals("", result); + result = toString(execute("git checkout second")); + assertEquals("Switched to branch 'second'", result); + + File a = writeTrashFile("a", "a"); + assertTrue(a.exists()); + execute("git add a", "git commit -m 'added a'"); + + result = toString(execute("git checkout master")); + assertEquals("Switched to branch 'master'", result); + + result = toString(execute("git branch")); + assertEquals(toString("fourth", "* master", "second", "third"), result); + + try { + toString(execute("git branch -d second third fourth")); + fail("Must die"); + } catch (Die e) { + // expected, the current HEAD is on second and not merged to master + } + result = toString(execute("git branch")); + assertEquals(toString("fourth", "* master", "second", "third"), result); + + result = toString(execute("git branch -D second third fourth")); + assertEquals("", result); + + result = toString(execute("git branch")); + assertEquals("* master", result); + } + + @Test + public void testCreateFromOldCommit() throws Exception { + File a = writeTrashFile("a", "a"); + assertTrue(a.exists()); + execute("git add a", "git commit -m 'added a'"); + File b = writeTrashFile("b", "b"); + assertTrue(b.exists()); + execute("git add b", "git commit -m 'added b'"); + String result = toString(execute("git log -n 1 --reverse")); + String firstCommitId = result.substring("commit ".length(), + result.indexOf('\n')); + + result = toString(execute("git branch -f second " + firstCommitId)); + assertEquals("", result); + + result = toString(execute("git branch")); + assertEquals(toString("* master", "second"), result); + + result = toString(execute("git checkout second")); + assertEquals("Switched to branch 'second'", result); + assertFalse(b.exists()); } } diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CheckoutTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CheckoutTest.java index cb36d057e..e690ad696 100644 --- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CheckoutTest.java +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CheckoutTest.java @@ -44,9 +44,14 @@ package org.eclipse.jgit.pgm; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; import java.util.List; import org.eclipse.jgit.api.Git; @@ -59,7 +64,9 @@ import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.treewalk.FileTreeIterator; import org.eclipse.jgit.treewalk.FileTreeIterator.FileEntry; import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FileUtils; +import org.junit.Assume; import org.junit.Test; public class CheckoutTest extends CLIRepositoryTestCase { @@ -109,14 +116,14 @@ public class CheckoutTest extends CLIRepositoryTestCase { assertStringArrayEquals( "fatal: A branch named 'master' already exists.", - execute("git checkout -b master")); + executeUnchecked("git checkout -b master")); } } @Test public void testCheckoutNewBranchOnBranchToBeBorn() throws Exception { assertStringArrayEquals("fatal: You are on a branch yet to be born", - execute("git checkout -b side")); + executeUnchecked("git checkout -b side")); } @Test @@ -599,4 +606,34 @@ public class CheckoutTest extends CLIRepositoryTestCase { assertEquals("Hello world b", read(b)); } } + + @Test + public void testCheckouSingleFile() throws Exception { + try (Git git = new Git(db)) { + File a = writeTrashFile("a", "file a"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("commit file a").call(); + writeTrashFile("a", "b"); + assertEquals("b", read(a)); + assertEquals("[]", Arrays.toString(execute("git checkout -- a"))); + assertEquals("file a", read(a)); + } + } + + @Test + public void testCheckoutLink() throws Exception { + Assume.assumeTrue(FS.DETECTED.supportsSymlinks()); + try (Git git = new Git(db)) { + Path path = writeLink("a", "link_a"); + assertTrue(Files.isSymbolicLink(path)); + git.add().addFilepattern(".").call(); + git.commit().setMessage("commit link a").call(); + deleteTrashFile("a"); + writeTrashFile("a", "Hello world a"); + assertFalse(Files.isSymbolicLink(path)); + assertEquals("[]", Arrays.toString(execute("git checkout -- a"))); + assertEquals("link_a", FileUtils.readSymLink(path.toFile())); + assertTrue(Files.isSymbolicLink(path)); + } + } } diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CommitTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CommitTest.java new file mode 100644 index 000000000..6bccb6d4a --- /dev/null +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CommitTest.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2015, Andrey Loskutov + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.pgm; + +import static org.junit.Assert.assertEquals; + +import org.eclipse.jgit.lib.CLIRepositoryTestCase; +import org.junit.Test; + +public class CommitTest extends CLIRepositoryTestCase { + + @Test + public void testCommitPath() throws Exception { + writeTrashFile("a", "a"); + writeTrashFile("b", "a"); + String result = toString(execute("git add a")); + assertEquals("", result); + + result = toString(execute("git status -- a")); + assertEquals(toString("On branch master", "Changes to be committed:", + "new file: a"), result); + + result = toString(execute("git status -- b")); + assertEquals(toString("On branch master", "Untracked files:", "b"), + result); + + result = toString(execute("git commit a -m 'added a'")); + assertEquals( + "[master 8cb3ef7e5171aaee1792df6302a5a0cd30425f7a] added a", + result); + + result = toString(execute("git status -- a")); + assertEquals("On branch master", result); + + result = toString(execute("git status -- b")); + assertEquals(toString("On branch master", "Untracked files:", "b"), + result); + } + + @Test + public void testCommitAll() throws Exception { + writeTrashFile("a", "a"); + writeTrashFile("b", "a"); + String result = toString(execute("git add a b")); + assertEquals("", result); + + result = toString(execute("git status -- a b")); + assertEquals(toString("On branch master", "Changes to be committed:", + "new file: a", "new file: b"), result); + + result = toString(execute("git commit -m 'added a b'")); + assertEquals( + "[master 3c93fa8e3a28ee26690498be78016edcb3a38c73] added a b", + result); + + result = toString(execute("git status -- a b")); + assertEquals("On branch master", result); + } + +} diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DescribeTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DescribeTest.java index 6352a2652..086e72e9a 100644 --- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DescribeTest.java +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DescribeTest.java @@ -43,9 +43,15 @@ package org.eclipse.jgit.pgm; import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.lib.CLIRepositoryTestCase; +import org.eclipse.jgit.pgm.internal.CLIText; import org.junit.Before; import org.junit.Test; @@ -67,17 +73,15 @@ public class DescribeTest extends CLIRepositoryTestCase { @Test public void testNoHead() throws Exception { - assertArrayEquals( - new String[] { "fatal: No names found, cannot describe anything." }, - execute("git describe")); + assertEquals(CLIText.fatalError(CLIText.get().noNamesFound), + toString(executeUnchecked("git describe"))); } @Test public void testHeadNoTag() throws Exception { git.commit().setMessage("initial commit").call(); - assertArrayEquals( - new String[] { "fatal: No names found, cannot describe anything." }, - execute("git describe")); + assertEquals(CLIText.fatalError(CLIText.get().noNamesFound), + toString(executeUnchecked("git describe"))); } @Test @@ -103,4 +107,22 @@ public class DescribeTest extends CLIRepositoryTestCase { assertArrayEquals(new String[] { "v1.0-0-g6fd41be", "" }, execute("git describe --long HEAD")); } + + @Test + public void testHelpArgumentBeforeUnknown() throws Exception { + String[] output = execute("git describe -h -XYZ"); + String all = Arrays.toString(output); + assertTrue("Unexpected help output: " + all, + all.contains("jgit describe")); + assertFalse("Unexpected help output: " + all, all.contains("fatal")); + } + + @Test + public void testHelpArgumentAfterUnknown() throws Exception { + String[] output = executeUnchecked("git describe -XYZ -h"); + String all = Arrays.toString(output); + assertTrue("Unexpected help output: " + all, + all.contains("jgit describe")); + assertTrue("Unexpected help output: " + all, all.contains("fatal")); + } } diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeTest.java index 975e8c4f7..47199016d 100644 --- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeTest.java +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeTest.java @@ -50,6 +50,7 @@ import java.util.Iterator; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.lib.CLIRepositoryTestCase; import org.eclipse.jgit.merge.MergeStrategy; +import org.eclipse.jgit.pgm.internal.CLIText; import org.eclipse.jgit.revwalk.RevCommit; import org.junit.Before; import org.junit.Test; @@ -194,8 +195,9 @@ public class MergeTest extends CLIRepositoryTestCase { @Test public void testNoFastForwardAndSquash() throws Exception { - assertEquals("fatal: You cannot combine --squash with --no-ff.", - execute("git merge master --no-ff --squash")[0]); + assertEquals( + CLIText.fatalError(CLIText.get().cannotCombineSquashWithNoff), + executeUnchecked("git merge master --no-ff --squash")[0]); } @Test @@ -209,8 +211,8 @@ public class MergeTest extends CLIRepositoryTestCase { git.add().addFilepattern("file").call(); git.commit().setMessage("commit#2").call(); - assertEquals("fatal: Not possible to fast-forward, aborting.", - execute("git merge master --ff-only")[0]); + assertEquals(CLIText.fatalError(CLIText.get().ffNotPossibleAborting), + executeUnchecked("git merge master --ff-only")[0]); } @Test diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/RepoTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/RepoTest.java index 90efae286..0eee771d2 100644 --- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/RepoTest.java +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/RepoTest.java @@ -44,8 +44,11 @@ package org.eclipse.jgit.pgm; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import java.io.File; +import java.util.Arrays; + import org.eclipse.jgit.api.Git; import org.eclipse.jgit.junit.JGitTestUtil; import org.eclipse.jgit.lib.CLIRepositoryTestCase; @@ -97,6 +100,31 @@ public class RepoTest extends CLIRepositoryTestCase { resolveRelativeUris(); } + @Test + public void testMissingPath() throws Exception { + try { + execute("git repo"); + fail("Must die"); + } catch (Die e) { + // expected, requires argument + } + } + + /** + * See bug 484951: "git repo -h" should not print unexpected values + * + * @throws Exception + */ + @Test + public void testZombieHelpArgument() throws Exception { + String[] output = execute("git repo -h"); + String all = Arrays.toString(output); + assertTrue("Unexpected help output: " + all, + all.contains("jgit repo")); + assertFalse("Unexpected help output: " + all, + all.contains("jgit repo VAL")); + } + @Test public void testAddRepoManifest() throws Exception { StringBuilder xmlContent = new StringBuilder(); diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ResetTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ResetTest.java index dae477928..16c5889c4 100644 --- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ResetTest.java +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ResetTest.java @@ -48,6 +48,7 @@ import org.eclipse.jgit.api.Git; import org.eclipse.jgit.lib.CLIRepositoryTestCase; import org.eclipse.jgit.revwalk.RevCommit; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; public class ResetTest extends CLIRepositoryTestCase { @@ -61,6 +62,20 @@ public class ResetTest extends CLIRepositoryTestCase { git = new Git(db); } + @Test + public void testPathOptionHelp() throws Exception { + String[] result = execute("git reset -h"); + assertTrue("Unexpected argument: " + result[1], + result[1].endsWith("[-- path ... ...]")); + } + + @Test + public void testZombieArgument_Bug484951() throws Exception { + String[] result = execute("git reset -h"); + assertFalse("Unexpected argument: " + result[0], + result[0].contains("[VAL ...]")); + } + @Test public void testResetSelf() throws Exception { RevCommit commit = git.commit().setMessage("initial commit").call(); @@ -91,15 +106,28 @@ public class ResetTest extends CLIRepositoryTestCase { @Test public void testResetPathDoubleDash() throws Exception { - resetPath(true); + resetPath(true, true); } @Test public void testResetPathNoDoubleDash() throws Exception { - resetPath(false); + resetPath(false, true); + } + + @Test + public void testResetPathDoubleDashNoRef() throws Exception { + resetPath(true, false); + } + + @Ignore("Currently we cannote recognize if a name is a commit-ish or a path, " + + "so 'git reset a' will not work if 'a' is not a branch name but a file path") + @Test + public void testResetPathNoDoubleDashNoRef() throws Exception { + resetPath(false, false); } - private void resetPath(boolean useDoubleDash) throws Exception { + private void resetPath(boolean useDoubleDash, boolean supplyCommit) + throws Exception { // create files a and b writeTrashFile("a", "Hello world a"); writeTrashFile("b", "Hello world b"); @@ -115,8 +143,9 @@ public class ResetTest extends CLIRepositoryTestCase { git.add().addFilepattern(".").call(); // reset only file a - String cmd = String.format("git reset %s%s a", commit.getId().name(), - (useDoubleDash) ? " --" : ""); + String cmd = String.format("git reset %s%s a", + supplyCommit ? commit.getId().name() : "", + useDoubleDash ? " --" : ""); assertStringArrayEquals("", execute(cmd)); assertEquals(commit.getId(), git.getRepository().exactRef("HEAD").getObjectId()); diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/StatusTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/StatusTest.java index 854c52d88..368047c60 100644 --- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/StatusTest.java +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/StatusTest.java @@ -44,6 +44,7 @@ package org.eclipse.jgit.pgm; import static org.eclipse.jgit.lib.Constants.MASTER; import static org.eclipse.jgit.lib.Constants.R_HEADS; +import static org.junit.Assert.assertTrue; import java.io.IOException; @@ -55,6 +56,13 @@ import org.junit.Test; public class StatusTest extends CLIRepositoryTestCase { + @Test + public void testPathOptionHelp() throws Exception { + String[] result = execute("git status -h"); + assertTrue("Unexpected argument: " + result[1], + result[1].endsWith("[-- path ... ...]")); + } + @Test public void testStatusDefault() throws Exception { executeTest("git status", false, true); diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/TagTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/TagTest.java index ab09db5a5..0fe25f550 100644 --- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/TagTest.java +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/TagTest.java @@ -68,6 +68,6 @@ public class TagTest extends CLIRepositoryTestCase { git.commit().setMessage("commit").call(); assertEquals("fatal: tag 'test' already exists", - execute("git tag test")[0]); + executeUnchecked("git tag test")[0]); } } diff --git a/org.eclipse.jgit.pgm/BUCK b/org.eclipse.jgit.pgm/BUCK new file mode 100644 index 000000000..edcf2fc28 --- /dev/null +++ b/org.eclipse.jgit.pgm/BUCK @@ -0,0 +1,70 @@ +include_defs('//tools/git.defs') + +java_library( + name = 'pgm', + srcs = glob(['src/**']), + resources = glob(['resources/**']), + deps = [ + ':services', + '//org.eclipse.jgit:jgit', + '//org.eclipse.jgit.archive:jgit-archive', + '//org.eclipse.jgit.http.apache:http-apache', + '//org.eclipse.jgit.ui:ui', + '//lib:args4j', + ], + visibility = ['PUBLIC'], +) + +prebuilt_jar( + name = 'services', + binary_jar = ':services__jar', +) + +genrule( + name = 'services__jar', + cmd = 'cd $SRCDIR ; zip -qr $OUT .', + srcs = glob(['META-INF/services/*']), + out = 'services.jar', +) + +genrule( + name = 'jgit', + cmd = ''.join([ + 'mkdir $TMP/META-INF &&', + 'cp $(location :binary_manifest) $TMP/META-INF/MANIFEST.MF &&', + 'cp $(location :jgit_jar) $TMP/jgit.jar &&', + 'cd $TMP && zip $TMP/jgit.jar META-INF/MANIFEST.MF &&', + 'cat $SRCDIR/jgit.sh $TMP/jgit.jar >$OUT &&', + 'chmod a+x $OUT', + ]), + srcs = ['jgit.sh'], + out = 'jgit', + visibility = ['PUBLIC'], +) + +java_binary( + name = 'jgit_jar', + deps = [ + ':pgm', + '//lib:slf4j-simple', + '//lib:tukaani-xz', + ], + blacklist = [ + 'META-INF/DEPENDENCIES', + 'META-INF/maven/.*', + ], +) + +genrule( + name = 'binary_manifest', + cmd = ';'.join(['echo "%s: %s" >>$OUT' % e for e in [ + ('Manifest-Version', '1.0'), + ('Main-Class', 'org.eclipse.jgit.pgm.Main'), + ('Bundle-Version', git_version()), + ('Implementation-Title', 'JGit Command Line Interface'), + ('Implementation-Vendor', 'Eclipse.org - JGit'), + ('Implementation-Vendor-URL', 'http://www.eclipse.org/jgit/'), + ('Implementation-Vendor-Id', 'org.eclipse.jgit'), + ]] + ['echo >>$OUT']), + out = 'MANIFEST.MF', +) diff --git a/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF b/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF index 567fd0575..9dc6aea16 100644 --- a/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF @@ -21,6 +21,7 @@ Import-Package: org.apache.commons.compress.archivers;version="[1.3,2.0)", org.eclipse.jgit.gitrepo;version="[4.2.0,4.3.0)", org.eclipse.jgit.internal.storage.file;version="[4.2.0,4.3.0)", org.eclipse.jgit.internal.storage.pack;version="[4.2.0,4.3.0)", + org.eclipse.jgit.internal.storage.reftree;version="[4.2.0,4.3.0)", org.eclipse.jgit.lib;version="[4.2.0,4.3.0)", org.eclipse.jgit.merge;version="4.2.0", org.eclipse.jgit.nls;version="[4.2.0,4.3.0)", @@ -31,6 +32,7 @@ Import-Package: org.apache.commons.compress.archivers;version="[1.3,2.0)", org.eclipse.jgit.storage.file;version="[4.2.0,4.3.0)", org.eclipse.jgit.storage.pack;version="[4.2.0,4.3.0)", org.eclipse.jgit.transport;version="[4.2.0,4.3.0)", + org.eclipse.jgit.transport.http.apache;version="[4.2.0,4.3.0)", org.eclipse.jgit.transport.resolver;version="[4.2.0,4.3.0)", org.eclipse.jgit.treewalk;version="[4.2.0,4.3.0)", org.eclipse.jgit.treewalk.filter;version="[4.2.0,4.3.0)", diff --git a/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin b/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin index c13f63e80..6aa20041b 100644 --- a/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin +++ b/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin @@ -41,6 +41,7 @@ org.eclipse.jgit.pgm.debug.DiffAlgorithms org.eclipse.jgit.pgm.debug.MakeCacheTree org.eclipse.jgit.pgm.debug.ReadDirCache org.eclipse.jgit.pgm.debug.RebuildCommitGraph +org.eclipse.jgit.pgm.debug.RebuildRefTree org.eclipse.jgit.pgm.debug.ShowCacheTree org.eclipse.jgit.pgm.debug.ShowCommands org.eclipse.jgit.pgm.debug.ShowDirCache diff --git a/org.eclipse.jgit.pgm/pom.xml b/org.eclipse.jgit.pgm/pom.xml index ca2ead292..264249132 100644 --- a/org.eclipse.jgit.pgm/pom.xml +++ b/org.eclipse.jgit.pgm/pom.xml @@ -94,6 +94,17 @@ ${project.version} + + org.eclipse.jgit + org.eclipse.jgit.http.apache + ${project.version} + + + + org.apache.httpcomponents + httpclient + + org.slf4j slf4j-api diff --git a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties index 335336da2..b4b1261b3 100644 --- a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties +++ b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties @@ -20,6 +20,7 @@ branchAlreadyExists=A branch named ''{0}'' already exists. branchCreatedFrom=branch: Created from {0} branchDetachedHEAD=detached HEAD branchIsNotAnAncestorOfYourCurrentHEAD=The branch ''{0}'' is not an ancestor of your current HEAD.\nIf you are sure you want to delete it, run ''jgit branch -D {0}''. +branchNameRequired=branch name required branchNotFound=branch ''{0}'' not found. cacheTreePathInfo="{0}": {1} entries, {2} children cannotBeRenamed={0} cannot be renamed @@ -89,7 +90,9 @@ metaVar_author=AUTHOR metaVar_base=base metaVar_blameL=START,END metaVar_blameReverse=START..END +metaVar_branchAndStartPoint=branch [start-name] metaVar_branchName=branch +metaVar_branchNames=branch ... metaVar_bucket=BUCKET metaVar_command=command metaVar_commandDetail=DETAIL @@ -109,6 +112,7 @@ metaVar_message=message metaVar_n=n metaVar_name=name metaVar_object=object +metaVar_oldNewBranchNames=[oldbranch] newbranch metaVar_op=OP metaVar_pass=PASS metaVar_path=path @@ -125,6 +129,7 @@ metaVar_treeish=tree-ish metaVar_uriish=uri-ish metaVar_url=URL metaVar_user=USER +metaVar_values=value ... metaVar_version=VERSION mostCommonlyUsedCommandsAre=The most commonly used commands are: needApprovalToDestroyCurrentRepository=Need approval to destroy current repository @@ -223,6 +228,7 @@ usage_MergeBase=Find as good common ancestors as possible for a merge usage_MergesTwoDevelopmentHistories=Merges two development histories usage_ReadDirCache= Read the DirCache 100 times usage_RebuildCommitGraph=Recreate a repository from another one's commit graph +usage_RebuildRefTree=Copy references into a RefTree usage_Remote=Manage set of tracked repositories usage_RepositoryToReadFrom=Repository to read from usage_RepositoryToReceiveInto=Repository to receive into @@ -337,6 +343,7 @@ usage_recordChangesToRepository=Record changes to the repository usage_recurseIntoSubtrees=recurse into subtrees usage_renameLimit=limit size of rename matrix usage_reset=Reset current HEAD to the specified state +usage_resetReference=Reset to given reference name usage_resetHard=Resets the index and working tree usage_resetSoft=Resets without touching the index file nor the working tree usage_resetMixed=Resets the index but not the working tree @@ -353,6 +360,7 @@ usage_tags=fetch all tags usage_notags=do not fetch tags usage_tagMessage=tag message usage_untrackedFilesMode=show untracked files +usage_updateRef=reference to update usage_updateRemoteRefsFromAnotherRepository=Update remote refs from another repository usage_useNameInsteadOfOriginToTrackUpstream=use instead of 'origin' to track upstream usage_checkoutBranchAfterClone=checkout named branch instead of remotes's HEAD diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Branch.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Branch.java index 65aa24f35..045f3571e 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Branch.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Branch.java @@ -45,7 +45,6 @@ package org.eclipse.jgit.pgm; import java.io.IOException; import java.text.MessageFormat; -import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; @@ -65,15 +64,18 @@ import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.RefUpdate.Result; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.pgm.internal.CLIText; -import org.eclipse.jgit.pgm.opt.CmdLineParser; +import org.eclipse.jgit.pgm.opt.OptionWithValuesListHandler; import org.eclipse.jgit.revwalk.RevWalk; import org.kohsuke.args4j.Argument; -import org.kohsuke.args4j.ExampleMode; import org.kohsuke.args4j.Option; @Command(common = true, usage = "usage_listCreateOrDeleteBranches") class Branch extends TextBuiltin { + private String otherBranch; + private boolean createForce; + private boolean rename; + @Option(name = "--remote", aliases = { "-r" }, usage = "usage_actOnRemoteTrackingBranches") private boolean remote = false; @@ -83,23 +85,69 @@ class Branch extends TextBuiltin { @Option(name = "--contains", metaVar = "metaVar_commitish", usage = "usage_printOnlyBranchesThatContainTheCommit") private String containsCommitish; - @Option(name = "--delete", aliases = { "-d" }, usage = "usage_deleteFullyMergedBranch") - private boolean delete = false; + private List delete; - @Option(name = "--delete-force", aliases = { "-D" }, usage = "usage_deleteBranchEvenIfNotMerged") - private boolean deleteForce = false; + @Option(name = "--delete", aliases = { + "-d" }, metaVar = "metaVar_branchNames", usage = "usage_deleteFullyMergedBranch", handler = OptionWithValuesListHandler.class) + public void delete(List names) { + if (names.isEmpty()) { + throw die(CLIText.get().branchNameRequired); + } + delete = names; + } - @Option(name = "--create-force", aliases = { "-f" }, usage = "usage_forceCreateBranchEvenExists") - private boolean createForce = false; + private List deleteForce; - @Option(name = "-m", usage = "usage_moveRenameABranch") - private boolean rename = false; + @Option(name = "--delete-force", aliases = { + "-D" }, metaVar = "metaVar_branchNames", usage = "usage_deleteBranchEvenIfNotMerged", handler = OptionWithValuesListHandler.class) + public void deleteForce(List names) { + if (names.isEmpty()) { + throw die(CLIText.get().branchNameRequired); + } + deleteForce = names; + } + + @Option(name = "--create-force", aliases = { + "-f" }, metaVar = "metaVar_branchAndStartPoint", usage = "usage_forceCreateBranchEvenExists", handler = OptionWithValuesListHandler.class) + public void createForce(List branchAndStartPoint) { + createForce = true; + if (branchAndStartPoint.isEmpty()) { + throw die(CLIText.get().branchNameRequired); + } + if (branchAndStartPoint.size() > 2) { + throw die(CLIText.get().tooManyRefsGiven); + } + if (branchAndStartPoint.size() == 1) { + branch = branchAndStartPoint.get(0); + } else { + branch = branchAndStartPoint.get(0); + otherBranch = branchAndStartPoint.get(1); + } + } + + @Option(name = "--move", aliases = { + "-m" }, metaVar = "metaVar_oldNewBranchNames", usage = "usage_moveRenameABranch", handler = OptionWithValuesListHandler.class) + public void moveRename(List currentAndNew) { + rename = true; + if (currentAndNew.isEmpty()) { + throw die(CLIText.get().branchNameRequired); + } + if (currentAndNew.size() > 2) { + throw die(CLIText.get().tooManyRefsGiven); + } + if (currentAndNew.size() == 1) { + branch = currentAndNew.get(0); + } else { + branch = currentAndNew.get(0); + otherBranch = currentAndNew.get(1); + } + } @Option(name = "--verbose", aliases = { "-v" }, usage = "usage_beVerbose") private boolean verbose = false; - @Argument - private List branches = new ArrayList(); + @Argument(metaVar = "metaVar_name") + private String branch; private final Map printRefs = new LinkedHashMap(); @@ -110,30 +158,33 @@ class Branch extends TextBuiltin { @Override protected void run() throws Exception { - if (delete || deleteForce) - delete(deleteForce); - else { - if (branches.size() > 2) - throw die(CLIText.get().tooManyRefsGiven + new CmdLineParser(this).printExample(ExampleMode.ALL)); - + if (delete != null || deleteForce != null) { + if (delete != null) { + delete(delete, false); + } + if (deleteForce != null) { + delete(deleteForce, true); + } + } else { if (rename) { String src, dst; - if (branches.size() == 1) { + if (otherBranch == null) { final Ref head = db.getRef(Constants.HEAD); - if (head != null && head.isSymbolic()) + if (head != null && head.isSymbolic()) { src = head.getLeaf().getName(); - else + } else { throw die(CLIText.get().cannotRenameDetachedHEAD); - dst = branches.get(0); + } + dst = branch; } else { - src = branches.get(0); + src = branch; final Ref old = db.getRef(src); if (old == null) throw die(MessageFormat.format(CLIText.get().doesNotExist, src)); if (!old.getName().startsWith(Constants.R_HEADS)) throw die(MessageFormat.format(CLIText.get().notABranch, src)); src = old.getName(); - dst = branches.get(1); + dst = otherBranch; } if (!dst.startsWith(Constants.R_HEADS)) @@ -145,13 +196,14 @@ class Branch extends TextBuiltin { if (r.rename() != Result.RENAMED) throw die(MessageFormat.format(CLIText.get().cannotBeRenamed, src)); - } else if (branches.size() > 0) { - String newHead = branches.get(0); + } else if (createForce || branch != null) { + String newHead = branch; String startBranch; - if (branches.size() == 2) - startBranch = branches.get(1); - else + if (createForce) { + startBranch = otherBranch; + } else { startBranch = Constants.HEAD; + } Ref startRef = db.getRef(startBranch); ObjectId startAt = db.resolve(startBranch + "^0"); //$NON-NLS-1$ if (startRef != null) { @@ -164,22 +216,27 @@ class Branch extends TextBuiltin { } startBranch = Repository.shortenRefName(startBranch); String newRefName = newHead; - if (!newRefName.startsWith(Constants.R_HEADS)) + if (!newRefName.startsWith(Constants.R_HEADS)) { newRefName = Constants.R_HEADS + newRefName; - if (!Repository.isValidRefName(newRefName)) + } + if (!Repository.isValidRefName(newRefName)) { throw die(MessageFormat.format(CLIText.get().notAValidRefName, newRefName)); - if (!createForce && db.resolve(newRefName) != null) + } + if (!createForce && db.resolve(newRefName) != null) { throw die(MessageFormat.format(CLIText.get().branchAlreadyExists, newHead)); + } RefUpdate updateRef = db.updateRef(newRefName); updateRef.setNewObjectId(startAt); updateRef.setForceUpdate(createForce); updateRef.setRefLogMessage(MessageFormat.format(CLIText.get().branchCreatedFrom, startBranch), false); Result update = updateRef.update(); - if (update == Result.REJECTED) + if (update == Result.REJECTED) { throw die(MessageFormat.format(CLIText.get().couldNotCreateBranch, newHead, update.toString())); + } } else { - if (verbose) + if (verbose) { rw = new RevWalk(db); + } list(); } } @@ -249,7 +306,8 @@ class Branch extends TextBuiltin { outw.println(); } - private void delete(boolean force) throws IOException { + private void delete(List branches, boolean force) + throws IOException { String current = db.getBranch(); ObjectId head = db.resolve(Constants.HEAD); for (String branch : branches) { diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Checkout.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Checkout.java index 45794629e..94517dbf2 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Checkout.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Checkout.java @@ -60,7 +60,7 @@ import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.pgm.internal.CLIText; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.Option; -import org.kohsuke.args4j.spi.StopOptionHandler; +import org.kohsuke.args4j.spi.RestOfArgumentsHandler; @Command(common = true, usage = "usage_checkout") class Checkout extends TextBuiltin { @@ -74,11 +74,10 @@ class Checkout extends TextBuiltin { @Option(name = "--orphan", usage = "usage_orphan") private boolean orphan = false; - @Argument(required = true, index = 0, metaVar = "metaVar_name", usage = "usage_checkout") + @Argument(required = false, index = 0, metaVar = "metaVar_name", usage = "usage_checkout") private String name; - @Argument(index = 1) - @Option(name = "--", metaVar = "metaVar_paths", multiValued = true, handler = StopOptionHandler.class) + @Option(name = "--", metaVar = "metaVar_paths", multiValued = true, handler = RestOfArgumentsHandler.class) private List paths = new ArrayList(); @Override diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Clone.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Clone.java index cd6953cb0..04078287f 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Clone.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Clone.java @@ -50,6 +50,7 @@ import org.eclipse.jgit.api.CloneCommand; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.InvalidRemoteException; import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.TextProgressMonitor; import org.eclipse.jgit.pgm.internal.CLIText; import org.eclipse.jgit.transport.URIish; import org.eclipse.jgit.util.SystemReader; @@ -70,6 +71,9 @@ class Clone extends AbstractFetchCommand { @Option(name = "--bare", usage = "usage_bareClone") private boolean isBare; + @Option(name = "--quiet", usage = "usage_quiet") + private Boolean quiet; + @Argument(index = 0, required = true, metaVar = "metaVar_uriish") private String sourceUri; @@ -109,10 +113,16 @@ class Clone extends AbstractFetchCommand { command.setGitDir(gitdir == null ? null : new File(gitdir)); command.setDirectory(localNameF); - outw.println(MessageFormat.format(CLIText.get().cloningInto, localName)); + boolean msgs = quiet == null || !quiet.booleanValue(); + if (msgs) { + command.setProgressMonitor(new TextProgressMonitor(errw)); + outw.println(MessageFormat.format( + CLIText.get().cloningInto, localName)); + outw.flush(); + } try { db = command.call().getRepository(); - if (db.resolve(Constants.HEAD) == null) + if (msgs && db.resolve(Constants.HEAD) == null) outw.println(CLIText.get().clonedEmptyRepository); } catch (InvalidRemoteException e) { throw die(MessageFormat.format(CLIText.get().doesNotExist, @@ -121,8 +131,9 @@ class Clone extends AbstractFetchCommand { if (db != null) db.close(); } - - outw.println(); - outw.flush(); + if (msgs) { + outw.println(); + outw.flush(); + } } } diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Die.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Die.java index f07df1a4b..a25f1e930 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Die.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Die.java @@ -86,6 +86,21 @@ public class Die extends RuntimeException { * @since 3.4 */ public Die(boolean aborted) { + this(aborted, null); + } + + /** + * Construct a new exception reflecting the fact that the command execution + * has been aborted before running. + * + * @param aborted + * boolean indicating the fact the execution has been aborted + * @param cause + * can be null + * @since 4.2 + */ + public Die(boolean aborted, final Throwable cause) { + super(cause != null ? cause.getMessage() : null, cause); this.aborted = aborted; } diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Main.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Main.java index ceb0d6b2f..d701f22c3 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Main.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Main.java @@ -62,6 +62,8 @@ import org.eclipse.jgit.lib.RepositoryBuilder; import org.eclipse.jgit.pgm.internal.CLIText; import org.eclipse.jgit.pgm.opt.CmdLineParser; import org.eclipse.jgit.pgm.opt.SubcommandHandler; +import org.eclipse.jgit.transport.HttpTransport; +import org.eclipse.jgit.transport.http.apache.HttpClientConnectionFactory; import org.eclipse.jgit.util.CachedAuthenticator; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.CmdLineException; @@ -88,13 +90,23 @@ public class Main { @Argument(index = 1, metaVar = "metaVar_arg") private List arguments = new ArrayList(); + PrintWriter writer; + + /** + * + */ + public Main() { + HttpTransport.setConnectionFactory(new HttpClientConnectionFactory()); + } + /** * Execute the command line. * * @param argv * arguments. + * @throws Exception */ - public static void main(final String[] argv) { + public static void main(final String[] argv) throws Exception { new Main().run(argv); } @@ -113,8 +125,10 @@ public class Main { * * @param argv * arguments. + * @throws Exception */ - protected void run(final String[] argv) { + protected void run(final String[] argv) throws Exception { + writer = createErrorWriter(); try { if (!installConsole()) { AwtAuthenticator.install(); @@ -123,12 +137,14 @@ public class Main { configureHttpProxy(); execute(argv); } catch (Die err) { - if (err.isAborted()) - System.exit(1); - System.err.println(MessageFormat.format(CLIText.get().fatalError, err.getMessage())); - if (showStackTrace) - err.printStackTrace(); - System.exit(128); + if (err.isAborted()) { + exit(1, err); + } + writer.println(CLIText.fatalError(err.getMessage())); + if (showStackTrace) { + err.printStackTrace(writer); + } + exit(128, err); } catch (Exception err) { // Try to detect errno == EPIPE and exit normally if that happens // There may be issues with operating system versions and locale, @@ -136,46 +152,54 @@ public class Main { // under other circumstances. if (err.getClass() == IOException.class) { // Linux, OS X - if (err.getMessage().equals("Broken pipe")) //$NON-NLS-1$ - System.exit(0); + if (err.getMessage().equals("Broken pipe")) { //$NON-NLS-1$ + exit(0, err); + } // Windows - if (err.getMessage().equals("The pipe is being closed")) //$NON-NLS-1$ - System.exit(0); + if (err.getMessage().equals("The pipe is being closed")) { //$NON-NLS-1$ + exit(0, err); + } } if (!showStackTrace && err.getCause() != null - && err instanceof TransportException) - System.err.println(MessageFormat.format(CLIText.get().fatalError, err.getCause().getMessage())); + && err instanceof TransportException) { + writer.println(CLIText.fatalError(err.getCause().getMessage())); + } if (err.getClass().getName().startsWith("org.eclipse.jgit.errors.")) { //$NON-NLS-1$ - System.err.println(MessageFormat.format(CLIText.get().fatalError, err.getMessage())); - if (showStackTrace) + writer.println(CLIText.fatalError(err.getMessage())); + if (showStackTrace) { err.printStackTrace(); - System.exit(128); + } + exit(128, err); } err.printStackTrace(); - System.exit(1); + exit(1, err); } if (System.out.checkError()) { - System.err.println(CLIText.get().unknownIoErrorStdout); - System.exit(1); + writer.println(CLIText.get().unknownIoErrorStdout); + exit(1, null); } - if (System.err.checkError()) { + if (writer.checkError()) { // No idea how to present an error here, most likely disk full or // broken pipe - System.exit(1); + exit(1, null); } } + PrintWriter createErrorWriter() { + return new PrintWriter(System.err); + } + private void execute(final String[] argv) throws Exception { - final CmdLineParser clp = new CmdLineParser(this); - PrintWriter writer = new PrintWriter(System.err); + final CmdLineParser clp = new SubcommandLineParser(this); + try { clp.parseArgument(argv); } catch (CmdLineException err) { if (argv.length > 0 && !help && !version) { - writer.println(MessageFormat.format(CLIText.get().fatalError, err.getMessage())); + writer.println(CLIText.fatalError(err.getMessage())); writer.flush(); - System.exit(1); + exit(1, err); } } @@ -191,22 +215,24 @@ public class Main { writer.println(CLIText.get().mostCommonlyUsedCommandsAre); final CommandRef[] common = CommandCatalog.common(); int width = 0; - for (final CommandRef c : common) + for (final CommandRef c : common) { width = Math.max(width, c.getName().length()); + } width += 2; for (final CommandRef c : common) { writer.print(' '); writer.print(c.getName()); - for (int i = c.getName().length(); i < width; i++) + for (int i = c.getName().length(); i < width; i++) { writer.print(' '); + } writer.print(CLIText.get().resourceBundle().getString(c.getUsage())); writer.println(); } writer.println(); } writer.flush(); - System.exit(1); + exit(1, null); } if (version) { @@ -215,20 +241,38 @@ public class Main { } final TextBuiltin cmd = subcommand; - if (cmd.requiresRepository()) - cmd.init(openGitDir(gitdir), null); - else - cmd.init(null, gitdir); + init(cmd); try { cmd.execute(arguments.toArray(new String[arguments.size()])); } finally { - if (cmd.outw != null) + if (cmd.outw != null) { cmd.outw.flush(); - if (cmd.errw != null) + } + if (cmd.errw != null) { cmd.errw.flush(); + } + } + } + + void init(final TextBuiltin cmd) throws IOException { + if (cmd.requiresRepository()) { + cmd.init(openGitDir(gitdir), null); + } else { + cmd.init(null, gitdir); } } + /** + * @param status + * @param t + * can be {@code null} + * @throws Exception + */ + void exit(int status, Exception t) throws Exception { + writer.flush(); + System.exit(status); + } + /** * Evaluate the {@code --git-dir} option and open the repository. * @@ -278,7 +322,7 @@ public class Main { throws IllegalAccessException, InvocationTargetException, NoSuchMethodException, ClassNotFoundException { try { - Class.forName(name).getMethod("install").invoke(null); //$NON-NLS-1$ + Class.forName(name).getMethod("install").invoke(null); //$NON-NLS-1$ } catch (InvocationTargetException e) { if (e.getCause() instanceof RuntimeException) throw (RuntimeException) e.getCause(); @@ -332,4 +376,19 @@ public class Main { } } } + + /** + * Parser for subcommands which doesn't stop parsing on help options and so + * proceeds all specified options + */ + static class SubcommandLineParser extends CmdLineParser { + public SubcommandLineParser(Object bean) { + super(bean); + } + + @Override + protected boolean containsHelp(String... args) { + return false; + } + } } diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Merge.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Merge.java index cd65af954..e739b58ae 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Merge.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Merge.java @@ -148,9 +148,12 @@ class Merge extends TextBuiltin { break; case FAST_FORWARD: ObjectId oldHeadId = oldHead.getObjectId(); - outw.println(MessageFormat.format(CLIText.get().updating, oldHeadId - .abbreviate(7).name(), result.getNewHead().abbreviate(7) - .name())); + if (oldHeadId != null) { + String oldId = oldHeadId.abbreviate(7).name(); + String newId = result.getNewHead().abbreviate(7).name(); + outw.println(MessageFormat.format(CLIText.get().updating, oldId, + newId)); + } outw.println(result.getMergeStatus().toString()); break; case CHECKOUT_CONFLICT: diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Remote.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Remote.java index 70868e920..24916bd1c 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Remote.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Remote.java @@ -144,7 +144,7 @@ class Remote extends TextBuiltin { } @Override - public void printUsageAndExit(final String message, final CmdLineParser clp) + public void printUsage(final String message, final CmdLineParser clp) throws IOException { errw.println(message); errw.println("jgit remote [--verbose (-v)] [--help (-h)]"); //$NON-NLS-1$ @@ -160,7 +160,6 @@ class Remote extends TextBuiltin { errw.println(); errw.flush(); - throw die(true); } private void print(List remotes) throws IOException { diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Repo.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Repo.java index db88008e1..ea59527fe 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Repo.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Repo.java @@ -55,7 +55,7 @@ class Repo extends TextBuiltin { @Option(name = "--groups", aliases = { "-g" }, usage = "usage_groups") private String groups = "default"; //$NON-NLS-1$ - @Argument(required = true, usage = "usage_pathToXml") + @Argument(required = true, metaVar = "metaVar_path", usage = "usage_pathToXml") private String path; @Option(name = "--record-remote-branch", usage = "usage_branches") diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Reset.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Reset.java index 4d3af4b56..9cee37b79 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Reset.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Reset.java @@ -51,7 +51,7 @@ import org.eclipse.jgit.api.ResetCommand; import org.eclipse.jgit.api.ResetCommand.ResetType; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.Option; -import org.kohsuke.args4j.spi.StopOptionHandler; +import org.kohsuke.args4j.spi.RestOfArgumentsHandler; @Command(common = true, usage = "usage_reset") class Reset extends TextBuiltin { @@ -65,12 +65,12 @@ class Reset extends TextBuiltin { @Option(name = "--hard", usage = "usage_resetHard") private boolean hard = false; - @Argument(required = true, index = 0, metaVar = "metaVar_name", usage = "usage_reset") + @Argument(required = false, index = 0, metaVar = "metaVar_commitish", usage = "usage_resetReference") private String commit; - @Argument(index = 1) - @Option(name = "--", metaVar = "metaVar_paths", multiValued = true, handler = StopOptionHandler.class) - private List paths = new ArrayList(); + @Argument(required = false, index = 1, metaVar = "metaVar_paths") + @Option(name = "--", metaVar = "metaVar_paths", multiValued = true, handler = RestOfArgumentsHandler.class) + private List paths = new ArrayList<>(); @Override protected void run() throws Exception { diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/RevParse.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/RevParse.java index e32fc9cab..c5ecb8496 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/RevParse.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/RevParse.java @@ -75,7 +75,13 @@ class RevParse extends TextBuiltin { if (all) { Map allRefs = db.getRefDatabase().getRefs(ALL); for (final Ref r : allRefs.values()) { - outw.println(r.getObjectId().name()); + ObjectId objectId = r.getObjectId(); + // getRefs skips dangling symrefs, so objectId should never be + // null. + if (objectId == null) { + throw new NullPointerException(); + } + outw.println(objectId.name()); } } else { if (verify && commits.size() > 1) { diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Status.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Status.java index be82d070f..6a6322131 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Status.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Status.java @@ -59,8 +59,9 @@ import org.eclipse.jgit.lib.IndexDiff.StageState; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.pgm.internal.CLIText; +import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.Option; - +import org.kohsuke.args4j.spi.RestOfArgumentsHandler; import org.eclipse.jgit.pgm.opt.UntrackedFilesHandler; /** @@ -83,7 +84,8 @@ class Status extends TextBuiltin { @Option(name = "--untracked-files", aliases = { "-u", "-uno", "-uall" }, usage = "usage_untrackedFilesMode", handler = UntrackedFilesHandler.class) protected String untrackedFilesMode = "all"; // default value //$NON-NLS-1$ - @Option(name = "--", metaVar = "metaVar_path", multiValued = true) + @Argument(required = false, index = 0, metaVar = "metaVar_paths") + @Option(name = "--", metaVar = "metaVar_paths", multiValued = true, handler = RestOfArgumentsHandler.class) protected List filterPaths; @Override diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/TextBuiltin.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/TextBuiltin.java index 56cfc7e8e..0dc549c7d 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/TextBuiltin.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/TextBuiltin.java @@ -212,17 +212,20 @@ public abstract class TextBuiltin { */ protected void parseArguments(final String[] args) throws IOException { final CmdLineParser clp = new CmdLineParser(this); + help = containsHelp(args); try { clp.parseArgument(args); } catch (CmdLineException err) { - if (!help) { - this.errw.println(MessageFormat.format(CLIText.get().fatalError, err.getMessage())); - throw die(true); + this.errw.println(CLIText.fatalError(err.getMessage())); + if (help) { + printUsage("", clp); //$NON-NLS-1$ } + throw die(true, err); } if (help) { - printUsageAndExit(clp); + printUsage("", clp); //$NON-NLS-1$ + throw new TerminatedByHelpException(); } argWalk = clp.getRevWalkGently(); @@ -246,6 +249,20 @@ public abstract class TextBuiltin { * @throws IOException */ public void printUsageAndExit(final String message, final CmdLineParser clp) throws IOException { + printUsage(message, clp); + throw die(true); + } + + /** + * @param message + * non null + * @param clp + * parser used to print options + * @throws IOException + * @since 4.2 + */ + protected void printUsage(final String message, final CmdLineParser clp) + throws IOException { errw.println(message); errw.print("jgit "); //$NON-NLS-1$ errw.print(commandName); @@ -257,12 +274,19 @@ public abstract class TextBuiltin { errw.println(); errw.flush(); - throw die(true); } /** - * @return the resource bundle that will be passed to args4j for purpose - * of string localization + * @return error writer, typically this is standard error. + * @since 4.2 + */ + public ThrowingPrintWriter getErrorWriter() { + return errw; + } + + /** + * @return the resource bundle that will be passed to args4j for purpose of + * string localization */ protected ResourceBundle getResourceBundle() { return CLIText.get().resourceBundle(); @@ -324,6 +348,19 @@ public abstract class TextBuiltin { return new Die(aborted); } + /** + * @param aborted + * boolean indicating that the execution has been aborted before + * running + * @param cause + * why the command has failed. + * @return a runtime exception the caller is expected to throw + * @since 4.2 + */ + protected static Die die(boolean aborted, final Throwable cause) { + return new Die(aborted, cause); + } + String abbreviateRef(String dst, boolean abbreviateRemote) { if (dst.startsWith(R_HEADS)) dst = dst.substring(R_HEADS.length()); @@ -333,4 +370,36 @@ public abstract class TextBuiltin { dst = dst.substring(R_REMOTES.length()); return dst; } + + /** + * @param args + * non null + * @return true if the given array contains help option + * @since 4.2 + */ + public static boolean containsHelp(String[] args) { + for (String str : args) { + if (str.equals("-h") || str.equals("--help")) { //$NON-NLS-1$ //$NON-NLS-2$ + return true; + } + } + return false; + } + + /** + * Exception thrown by {@link TextBuiltin} if it proceeds 'help' option + * + * @since 4.2 + */ + public static class TerminatedByHelpException extends Die { + private static final long serialVersionUID = 1L; + + /** + * Default constructor + */ + public TerminatedByHelpException() { + super(true); + } + + } } diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/RebuildRefTree.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/RebuildRefTree.java new file mode 100644 index 000000000..78ca1a712 --- /dev/null +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/RebuildRefTree.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2015, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.pgm.debug; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.eclipse.jgit.internal.storage.reftree.RefTree; +import org.eclipse.jgit.internal.storage.reftree.RefTreeDatabase; +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefDatabase; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.pgm.Command; +import org.eclipse.jgit.pgm.TextBuiltin; +import org.eclipse.jgit.revwalk.RevWalk; + +@Command(usage = "usage_RebuildRefTree") +class RebuildRefTree extends TextBuiltin { + private String txnNamespace; + private String txnCommitted; + + @Override + protected void run() throws Exception { + try (ObjectReader reader = db.newObjectReader(); + RevWalk rw = new RevWalk(reader); + ObjectInserter inserter = db.newObjectInserter()) { + RefDatabase refDb = db.getRefDatabase(); + if (refDb instanceof RefTreeDatabase) { + RefTreeDatabase d = (RefTreeDatabase) refDb; + refDb = d.getBootstrap(); + txnNamespace = d.getTxnNamespace(); + txnCommitted = d.getTxnCommitted(); + } else { + RefTreeDatabase d = new RefTreeDatabase(db, refDb); + txnNamespace = d.getTxnNamespace(); + txnCommitted = d.getTxnCommitted(); + } + + errw.format("Rebuilding %s from %s", //$NON-NLS-1$ + txnCommitted, refDb.getClass().getSimpleName()); + errw.println(); + errw.flush(); + + CommitBuilder b = new CommitBuilder(); + Ref ref = refDb.exactRef(txnCommitted); + RefUpdate update = refDb.newUpdate(txnCommitted, true); + ObjectId oldTreeId; + + if (ref != null && ref.getObjectId() != null) { + ObjectId oldId = ref.getObjectId(); + update.setExpectedOldObjectId(oldId); + b.setParentId(oldId); + oldTreeId = rw.parseCommit(oldId).getTree(); + } else { + update.setExpectedOldObjectId(ObjectId.zeroId()); + oldTreeId = ObjectId.zeroId(); + } + + RefTree tree = rebuild(refDb.getRefs(RefDatabase.ALL)); + b.setTreeId(tree.writeTree(inserter)); + b.setAuthor(new PersonIdent(db)); + b.setCommitter(b.getAuthor()); + if (b.getTreeId().equals(oldTreeId)) { + return; + } + + update.setNewObjectId(inserter.insert(b)); + inserter.flush(); + + RefUpdate.Result result = update.update(rw); + switch (result) { + case NEW: + case FAST_FORWARD: + break; + default: + throw die(String.format("%s: %s", update.getName(), result)); //$NON-NLS-1$ + } + } + } + + private RefTree rebuild(Map refMap) { + RefTree tree = RefTree.newEmptyTree(); + List cmds + = new ArrayList<>(); + + for (Ref r : refMap.values()) { + if (r.getName().equals(txnCommitted) + || r.getName().startsWith(txnNamespace)) { + continue; + } + cmds.add(new org.eclipse.jgit.internal.storage.reftree.Command( + null, + db.peel(r))); + } + tree.apply(cmds); + return tree; + } +} diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java index f5d581ad0..281213726 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java @@ -74,6 +74,19 @@ public class CLIText extends TranslationBundle { return MessageFormat.format(get().lineFormat, line); } + /** + * Format the given argument as fatal error using the format defined by + * {@link #fatalError} ("fatal: " by default). + * + * @param message + * the message to format + * @return the formatted line + * @since 4.2 + */ + public static String fatalError(String message) { + return MessageFormat.format(get().fatalError, message); + } + // @formatter:off /***/ public String alreadyOnBranch; /***/ public String alreadyUpToDate; @@ -85,6 +98,7 @@ public class CLIText extends TranslationBundle { /***/ public String branchCreatedFrom; /***/ public String branchDetachedHEAD; /***/ public String branchIsNotAnAncestorOfYourCurrentHEAD; + /***/ public String branchNameRequired; /***/ public String branchNotFound; /***/ public String cacheTreePathInfo; /***/ public String configFileNotFound; @@ -184,6 +198,7 @@ public class CLIText extends TranslationBundle { /***/ public String metaVar_uriish; /***/ public String metaVar_url; /***/ public String metaVar_user; + /***/ public String metaVar_values; /***/ public String metaVar_version; /***/ public String mostCommonlyUsedCommandsAre; /***/ public String needApprovalToDestroyCurrentRepository; diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/opt/CmdLineParser.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/opt/CmdLineParser.java index 3f77aa668..b531ba65a 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/opt/CmdLineParser.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/opt/CmdLineParser.java @@ -43,19 +43,18 @@ package org.eclipse.jgit.pgm.opt; +import java.io.IOException; +import java.io.Writer; import java.lang.reflect.Field; import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.ResourceBundle; -import org.kohsuke.args4j.Argument; -import org.kohsuke.args4j.CmdLineException; -import org.kohsuke.args4j.IllegalAnnotationError; -import org.kohsuke.args4j.NamedOptionDef; -import org.kohsuke.args4j.Option; -import org.kohsuke.args4j.OptionDef; -import org.kohsuke.args4j.spi.OptionHandler; -import org.kohsuke.args4j.spi.Setter; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.pgm.Die; import org.eclipse.jgit.pgm.TextBuiltin; import org.eclipse.jgit.pgm.internal.CLIText; import org.eclipse.jgit.revwalk.RevCommit; @@ -63,6 +62,15 @@ import org.eclipse.jgit.revwalk.RevTree; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.transport.RefSpec; import org.eclipse.jgit.treewalk.AbstractTreeIterator; +import org.kohsuke.args4j.Argument; +import org.kohsuke.args4j.CmdLineException; +import org.kohsuke.args4j.IllegalAnnotationError; +import org.kohsuke.args4j.NamedOptionDef; +import org.kohsuke.args4j.Option; +import org.kohsuke.args4j.OptionDef; +import org.kohsuke.args4j.spi.OptionHandler; +import org.kohsuke.args4j.spi.RestOfArgumentsHandler; +import org.kohsuke.args4j.spi.Setter; /** * Extended command line parser which handles --foo=value arguments. @@ -80,12 +88,17 @@ public class CmdLineParser extends org.kohsuke.args4j.CmdLineParser { registerHandler(RefSpec.class, RefSpecHandler.class); registerHandler(RevCommit.class, RevCommitHandler.class); registerHandler(RevTree.class, RevTreeHandler.class); + registerHandler(List.class, OptionWithValuesListHandler.class); } private final Repository db; private RevWalk walk; + private boolean seenHelp; + + private TextBuiltin cmd; + /** * Creates a new command line owner that parses arguments/options and set * them into the given object. @@ -117,8 +130,12 @@ public class CmdLineParser extends org.kohsuke.args4j.CmdLineParser { */ public CmdLineParser(final Object bean, Repository repo) { super(bean); - if (repo == null && bean instanceof TextBuiltin) - repo = ((TextBuiltin) bean).getRepository(); + if (bean instanceof TextBuiltin) { + cmd = (TextBuiltin) bean; + } + if (repo == null && cmd != null) { + repo = cmd.getRepository(); + } this.db = repo; } @@ -143,9 +160,75 @@ public class CmdLineParser extends org.kohsuke.args4j.CmdLineParser { } tmp.add(str); + + if (containsHelp(args)) { + // suppress exceptions on required parameters if help is present + seenHelp = true; + // stop argument parsing here + break; + } + } + List backup = null; + if (seenHelp) { + backup = unsetRequiredOptions(); } - super.parseArgument(tmp.toArray(new String[tmp.size()])); + try { + super.parseArgument(tmp.toArray(new String[tmp.size()])); + } catch (Die e) { + if (!seenHelp) { + throw e; + } + printToErrorWriter(CLIText.fatalError(e.getMessage())); + } finally { + // reset "required" options to defaults for correct command printout + if (backup != null && !backup.isEmpty()) { + restoreRequiredOptions(backup); + } + seenHelp = false; + } + } + + private void printToErrorWriter(String error) { + if (cmd == null) { + System.err.println(error); + } else { + try { + cmd.getErrorWriter().println(error); + } catch (IOException e1) { + System.err.println(error); + } + } + } + + private List unsetRequiredOptions() { + List options = getOptions(); + List backup = new ArrayList<>(options); + for (Iterator iterator = options.iterator(); iterator + .hasNext();) { + OptionHandler handler = iterator.next(); + if (handler.option instanceof NamedOptionDef + && handler.option.required()) { + iterator.remove(); + } + } + return backup; + } + + private void restoreRequiredOptions(List backup) { + List options = getOptions(); + options.clear(); + options.addAll(backup); + } + + /** + * @param args + * non null + * @return true if the given array contains help option + * @since 4.2 + */ + protected boolean containsHelp(final String... args) { + return TextBuiltin.containsHelp(args); } /** @@ -181,7 +264,7 @@ public class CmdLineParser extends org.kohsuke.args4j.CmdLineParser { return walk; } - static class MyOptionDef extends OptionDef { + class MyOptionDef extends OptionDef { public MyOptionDef(OptionDef o) { super(o.usage(), o.metaVar(), o.required(), o.handler(), o @@ -201,6 +284,11 @@ public class CmdLineParser extends org.kohsuke.args4j.CmdLineParser { return metaVar(); } } + + @Override + public boolean required() { + return seenHelp ? false : super.required(); + } } @Override @@ -211,4 +299,55 @@ public class CmdLineParser extends org.kohsuke.args4j.CmdLineParser { return super.createOptionHandler(new MyOptionDef(o), setter); } + + @SuppressWarnings("unchecked") + private List getOptions() { + List options = null; + try { + Field field = org.kohsuke.args4j.CmdLineParser.class + .getDeclaredField("options"); //$NON-NLS-1$ + field.setAccessible(true); + options = (List) field.get(this); + } catch (NoSuchFieldException | SecurityException + | IllegalArgumentException | IllegalAccessException e) { + // ignore + } + if (options == null) { + return Collections.emptyList(); + } + return options; + } + + @Override + public void printSingleLineUsage(Writer w, ResourceBundle rb) { + List options = getOptions(); + if (options.isEmpty()) { + super.printSingleLineUsage(w, rb); + return; + } + List backup = new ArrayList<>(options); + boolean changed = sortRestOfArgumentsHandlerToTheEnd(options); + try { + super.printSingleLineUsage(w, rb); + } finally { + if (changed) { + options.clear(); + options.addAll(backup); + } + } + } + + private boolean sortRestOfArgumentsHandlerToTheEnd( + List options) { + for (int i = 0; i < options.size(); i++) { + OptionHandler handler = options.get(i); + if (handler instanceof RestOfArgumentsHandler + || handler instanceof PathTreeFilterHandler) { + options.remove(i); + options.add(handler); + return true; + } + } + return false; + } } diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/opt/OptionWithValuesListHandler.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/opt/OptionWithValuesListHandler.java new file mode 100644 index 000000000..3de7a8109 --- /dev/null +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/opt/OptionWithValuesListHandler.java @@ -0,0 +1,52 @@ +package org.eclipse.jgit.pgm.opt; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jgit.pgm.internal.CLIText; +import org.kohsuke.args4j.CmdLineException; +import org.kohsuke.args4j.CmdLineParser; +import org.kohsuke.args4j.OptionDef; +import org.kohsuke.args4j.spi.OptionHandler; +import org.kohsuke.args4j.spi.Parameters; +import org.kohsuke.args4j.spi.Setter; + +/** + * Handler which allows to parse option with few values + * + * @since 4.2 + */ +public class OptionWithValuesListHandler extends OptionHandler> { + + /** + * @param parser + * @param option + * @param setter + */ + public OptionWithValuesListHandler(CmdLineParser parser, + OptionDef option, Setter> setter) { + super(parser, option, setter); + } + + @Override + public int parseArguments(Parameters params) throws CmdLineException { + final List list = new ArrayList<>(); + for (int idx = 0; idx < params.size(); idx++) { + final String p; + try { + p = params.getParameter(idx); + } catch (CmdLineException cle) { + break; + } + list.add(p); + } + setter.addValue(list); + return list.size(); + } + + @Override + public String getDefaultMetaVariable() { + return CLIText.get().metaVar_values; + } + +} diff --git a/org.eclipse.jgit.test/BUCK b/org.eclipse.jgit.test/BUCK new file mode 100644 index 000000000..3df3336b4 --- /dev/null +++ b/org.eclipse.jgit.test/BUCK @@ -0,0 +1,95 @@ +PKG = 'tst/org/eclipse/jgit/' +HELPERS = glob(['src/**/*.java']) + [PKG + c for c in [ + 'api/AbstractRemoteCommandTest.java', + 'diff/AbstractDiffTestCase.java', + 'internal/storage/file/GcTestCase.java', + 'internal/storage/file/PackIndexTestCase.java', + 'internal/storage/file/XInputStream.java', + 'nls/GermanTranslatedBundle.java', + 'nls/MissingPropertyBundle.java', + 'nls/NoPropertiesBundle.java', + 'nls/NonTranslatedBundle.java', + 'revwalk/RevQueueTestCase.java', + 'revwalk/RevWalkTestCase.java', + 'transport/SpiTransport.java', + 'treewalk/FileTreeIteratorWithTimeControl.java', + 'treewalk/filter/AlwaysCloneTreeFilter.java', + 'test/resources/SampleDataRepositoryTestCase.java', + 'util/CPUTimeStopWatch.java', + 'util/io/Strings.java', +]] + +DATA = [ + PKG + 'lib/empty.gitindex.dat', + PKG + 'lib/sorttest.gitindex.dat', +] + +TESTS = glob( + ['tst/**/*.java'], + excludes = HELPERS + DATA, +) + +DEPS = { + PKG + 'nls/RootLocaleTest.java': [ + '//org.eclipse.jgit.pgm:pgm', + '//org.eclipse.jgit.ui:ui', + ], +} + +for src in TESTS: + name = src[len('tst/'):len(src)-len('.java')].replace('/', '.') + labels = [] + if name.startswith('org.eclipse.jgit.'): + l = name[len('org.eclipse.jgit.'):] + if l.startswith('internal.storage.'): + l = l[len('internal.storage.'):] + i = l.find('.') + if i > 0: + labels.append(l[:i]) + else: + labels.append(i) + if 'lib' not in labels: + labels.append('lib') + + java_test( + name = name, + labels = labels, + srcs = [src], + deps = [ + ':helpers', + ':tst_rsrc', + '//org.eclipse.jgit:jgit', + '//org.eclipse.jgit.junit:junit', + '//lib:hamcrest-core', + '//lib:hamcrest-library', + '//lib:javaewah', + '//lib:junit', + '//lib:slf4j-api', + '//lib:slf4j-simple', + ] + DEPS.get(src, []), + source_under_test = ['//org.eclipse.jgit:jgit'], + vm_args = ['-Xmx256m', '-Dfile.encoding=UTF-8'], + ) + +java_library( + name = 'helpers', + srcs = HELPERS, + resources = DATA, + deps = [ + '//org.eclipse.jgit:jgit', + '//org.eclipse.jgit.junit:junit', + '//lib:junit', + ], +) + +prebuilt_jar( + name = 'tst_rsrc', + binary_jar = ':tst_rsrc_jar', +) + +genrule( + name = 'tst_rsrc_jar', + cmd = 'cd $SRCDIR/tst-rsrc ; zip -qr $OUT .', + srcs = glob(['tst-rsrc/**']), + out = 'tst_rsrc.jar', +) diff --git a/org.eclipse.jgit.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.test/META-INF/MANIFEST.MF index 37fd36717..f78fe5b24 100644 --- a/org.eclipse.jgit.test/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.test/META-INF/MANIFEST.MF @@ -26,6 +26,7 @@ Import-Package: com.googlecode.javaewah;version="[0.7.9,0.8.0)", org.eclipse.jgit.internal.storage.dfs;version="[4.2.0,4.3.0)", org.eclipse.jgit.internal.storage.file;version="[4.2.0,4.3.0)", org.eclipse.jgit.internal.storage.pack;version="[4.2.0,4.3.0)", + org.eclipse.jgit.internal.storage.reftree;version="[4.2.0,4.3.0)", org.eclipse.jgit.junit;version="[4.2.0,4.3.0)", org.eclipse.jgit.lib;version="[4.2.0,4.3.0)", org.eclipse.jgit.merge;version="[4.2.0,4.3.0)", diff --git a/org.eclipse.jgit.test/org.eclipse.jgit.core--All-Tests (Java 8) (de).launch b/org.eclipse.jgit.test/org.eclipse.jgit.core--All-Tests (Java 8) (de).launch new file mode 100644 index 000000000..f12a529e1 --- /dev/null +++ b/org.eclipse.jgit.test/org.eclipse.jgit.core--All-Tests (Java 8) (de).launch @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java index d4bd68e68..4fefdfdda 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java @@ -43,6 +43,7 @@ */ package org.eclipse.jgit.api; +import static org.eclipse.jgit.util.FileUtils.RECURSIVE; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @@ -797,13 +798,108 @@ public class AddCommandTest extends RepositoryTestCase { assertEquals("[a.txt, mode:100644, content:more content," + " assume-unchanged:false][b.txt, mode:100644," - + "" + "" + " content:content, assume-unchanged:true]", indexState(CONTENT | ASSUME_UNCHANGED)); } } + @Test + public void testReplaceFileWithDirectory() + throws IOException, NoFilepatternException, GitAPIException { + try (Git git = new Git(db)) { + writeTrashFile("df", "before replacement"); + git.add().addFilepattern("df").call(); + assertEquals("[df, mode:100644, content:before replacement]", + indexState(CONTENT)); + FileUtils.delete(new File(db.getWorkTree(), "df")); + writeTrashFile("df/f", "after replacement"); + git.add().addFilepattern("df").call(); + assertEquals("[df/f, mode:100644, content:after replacement]", + indexState(CONTENT)); + } + } + + @Test + public void testReplaceDirectoryWithFile() + throws IOException, NoFilepatternException, GitAPIException { + try (Git git = new Git(db)) { + writeTrashFile("df/f", "before replacement"); + git.add().addFilepattern("df").call(); + assertEquals("[df/f, mode:100644, content:before replacement]", + indexState(CONTENT)); + FileUtils.delete(new File(db.getWorkTree(), "df"), RECURSIVE); + writeTrashFile("df", "after replacement"); + git.add().addFilepattern("df").call(); + assertEquals("[df, mode:100644, content:after replacement]", + indexState(CONTENT)); + } + } + + @Test + public void testReplaceFileByPartOfDirectory() + throws IOException, NoFilepatternException, GitAPIException { + try (Git git = new Git(db)) { + writeTrashFile("src/main", "df", "before replacement"); + writeTrashFile("src/main", "z", "z"); + writeTrashFile("z", "z2"); + git.add().addFilepattern("src/main/df") + .addFilepattern("src/main/z") + .addFilepattern("z") + .call(); + assertEquals( + "[src/main/df, mode:100644, content:before replacement]" + + "[src/main/z, mode:100644, content:z]" + + "[z, mode:100644, content:z2]", + indexState(CONTENT)); + FileUtils.delete(new File(db.getWorkTree(), "src/main/df")); + writeTrashFile("src/main/df", "a", "after replacement"); + writeTrashFile("src/main/df", "b", "unrelated file"); + git.add().addFilepattern("src/main/df/a").call(); + assertEquals( + "[src/main/df/a, mode:100644, content:after replacement]" + + "[src/main/z, mode:100644, content:z]" + + "[z, mode:100644, content:z2]", + indexState(CONTENT)); + } + } + + @Test + public void testReplaceDirectoryConflictsWithFile() + throws IOException, NoFilepatternException, GitAPIException { + DirCache dc = db.lockDirCache(); + try (ObjectInserter oi = db.newObjectInserter()) { + DirCacheBuilder builder = dc.builder(); + File f = writeTrashFile("a", "df", "content"); + addEntryToBuilder("a", f, oi, builder, 1); + + f = writeTrashFile("a", "df", "other content"); + addEntryToBuilder("a/df", f, oi, builder, 3); + + f = writeTrashFile("a", "df", "our content"); + addEntryToBuilder("a/df", f, oi, builder, 2); + + f = writeTrashFile("z", "z"); + addEntryToBuilder("z", f, oi, builder, 0); + builder.commit(); + } + assertEquals( + "[a, mode:100644, stage:1, content:content]" + + "[a/df, mode:100644, stage:2, content:our content]" + + "[a/df, mode:100644, stage:3, content:other content]" + + "[z, mode:100644, content:z]", + indexState(CONTENT)); + + try (Git git = new Git(db)) { + FileUtils.delete(new File(db.getWorkTree(), "a"), RECURSIVE); + writeTrashFile("a", "merged"); + git.add().addFilepattern("a").call(); + assertEquals("[a, mode:100644, content:merged]" + + "[z, mode:100644, content:z]", + indexState(CONTENT)); + } + } + @Test public void testExecutableRetention() throws Exception { StoredConfig config = db.getConfig(); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java index 0d03047d5..b39a68a22 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java @@ -46,12 +46,15 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.fail; import java.io.File; import java.util.Date; import java.util.List; import java.util.TimeZone; +import org.eclipse.jgit.api.errors.EmtpyCommitException; import org.eclipse.jgit.api.errors.WrongRepositoryStateException; import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.dircache.DirCache; @@ -476,6 +479,34 @@ public class CommitCommandTest extends RepositoryTestCase { assertEquals("newauthor@example.org", amendedAuthor.getEmailAddress()); } + @Test + public void commitEmptyCommits() throws Exception { + try (Git git = new Git(db)) { + + writeTrashFile("file1", "file1"); + git.add().addFilepattern("file1").call(); + RevCommit initial = git.commit().setMessage("initial commit") + .call(); + + RevCommit emptyFollowUp = git.commit() + .setAuthor("New Author", "newauthor@example.org") + .setMessage("no change").call(); + + assertNotEquals(initial.getId(), emptyFollowUp.getId()); + assertEquals(initial.getTree().getId(), + emptyFollowUp.getTree().getId()); + + try { + git.commit().setAuthor("New Author", "newauthor@example.org") + .setMessage("again no change").setAllowEmpty(false) + .call(); + fail("Didn't get the expected EmtpyCommitException"); + } catch (EmtpyCommitException e) { + // expect this exception + } + } + } + @Test public void commitOnlyShouldCommitUnmergedPathAndNotAffectOthers() throws Exception { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PathCheckoutCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PathCheckoutCommandTest.java index db811cdf5..3343af06d 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PathCheckoutCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PathCheckoutCommandTest.java @@ -43,10 +43,12 @@ package org.eclipse.jgit.api; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import java.io.File; import java.io.IOException; +import java.nio.file.Path; import org.eclipse.jgit.api.CheckoutCommand.Stage; import org.eclipse.jgit.api.errors.JGitInternalException; @@ -59,6 +61,9 @@ import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.RepositoryState; import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.FileUtils; +import org.junit.Assume; import org.junit.Before; import org.junit.Test; @@ -73,6 +78,8 @@ public class PathCheckoutCommandTest extends RepositoryTestCase { private static final String FILE3 = "Test3.txt"; + private static final String LINK = "link"; + Git git; RevCommit initialCommit; @@ -98,6 +105,64 @@ public class PathCheckoutCommandTest extends RepositoryTestCase { git.commit().setMessage("Third commit").call(); } + @Test + public void testUpdateSymLink() throws Exception { + Assume.assumeTrue(FS.DETECTED.supportsSymlinks()); + + Path path = writeLink(LINK, FILE1); + git.add().addFilepattern(LINK).call(); + git.commit().setMessage("Added link").call(); + assertEquals("3", read(path.toFile())); + + writeLink(LINK, FILE2); + assertEquals("c", read(path.toFile())); + + CheckoutCommand co = git.checkout(); + co.addPath(LINK).call(); + + assertEquals("3", read(path.toFile())); + } + + @Test + public void testUpdateBrokenSymLinkToDirectory() throws Exception { + Assume.assumeTrue(FS.DETECTED.supportsSymlinks()); + + Path path = writeLink(LINK, "f"); + git.add().addFilepattern(LINK).call(); + git.commit().setMessage("Added link").call(); + assertEquals("f", FileUtils.readSymLink(path.toFile())); + assertTrue(path.toFile().exists()); + + writeLink(LINK, "link_to_nowhere"); + assertFalse(path.toFile().exists()); + assertEquals("link_to_nowhere", FileUtils.readSymLink(path.toFile())); + + CheckoutCommand co = git.checkout(); + co.addPath(LINK).call(); + + assertEquals("f", FileUtils.readSymLink(path.toFile())); + } + + @Test + public void testUpdateBrokenSymLink() throws Exception { + Assume.assumeTrue(FS.DETECTED.supportsSymlinks()); + + Path path = writeLink(LINK, FILE1); + git.add().addFilepattern(LINK).call(); + git.commit().setMessage("Added link").call(); + assertEquals("3", read(path.toFile())); + assertEquals(FILE1, FileUtils.readSymLink(path.toFile())); + + writeLink(LINK, "link_to_nowhere"); + assertFalse(path.toFile().exists()); + assertEquals("link_to_nowhere", FileUtils.readSymLink(path.toFile())); + + CheckoutCommand co = git.checkout(); + co.addPath(LINK).call(); + + assertEquals("3", read(path.toFile())); + } + @Test public void testUpdateWorkingDirectory() throws Exception { CheckoutCommand co = git.checkout(); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java index a67f2b912..66f25e8e5 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java @@ -65,6 +65,7 @@ import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.TreeWalk; @@ -139,8 +140,8 @@ public class ResetCommandTest extends RepositoryTestCase { AmbiguousObjectException, IOException, GitAPIException { setupRepository(); ObjectId prevHead = db.resolve(Constants.HEAD); - git.reset().setMode(ResetType.HARD).setRef(initialCommit.getName()) - .call(); + assertSameAsHead(git.reset().setMode(ResetType.HARD) + .setRef(initialCommit.getName()).call()); // check if HEAD points to initial commit now ObjectId head = db.resolve(Constants.HEAD); assertEquals(initialCommit, head); @@ -176,8 +177,8 @@ public class ResetCommandTest extends RepositoryTestCase { AmbiguousObjectException, IOException, GitAPIException { setupRepository(); ObjectId prevHead = db.resolve(Constants.HEAD); - git.reset().setMode(ResetType.SOFT).setRef(initialCommit.getName()) - .call(); + assertSameAsHead(git.reset().setMode(ResetType.SOFT) + .setRef(initialCommit.getName()).call()); // check if HEAD points to initial commit now ObjectId head = db.resolve(Constants.HEAD); assertEquals(initialCommit, head); @@ -197,8 +198,8 @@ public class ResetCommandTest extends RepositoryTestCase { AmbiguousObjectException, IOException, GitAPIException { setupRepository(); ObjectId prevHead = db.resolve(Constants.HEAD); - git.reset().setMode(ResetType.MIXED).setRef(initialCommit.getName()) - .call(); + assertSameAsHead(git.reset().setMode(ResetType.MIXED) + .setRef(initialCommit.getName()).call()); // check if HEAD points to initial commit now ObjectId head = db.resolve(Constants.HEAD); assertEquals(initialCommit, head); @@ -241,7 +242,8 @@ public class ResetCommandTest extends RepositoryTestCase { assertTrue(bEntry.getLength() > 0); assertTrue(bEntry.getLastModified() > 0); - git.reset().setMode(ResetType.MIXED).setRef(commit2.getName()).call(); + assertSameAsHead(git.reset().setMode(ResetType.MIXED) + .setRef(commit2.getName()).call()); cache = db.readDirCache(); @@ -280,7 +282,7 @@ public class ResetCommandTest extends RepositoryTestCase { + "[a.txt, mode:100644, stage:3]", indexState(0)); - git.reset().setMode(ResetType.MIXED).call(); + assertSameAsHead(git.reset().setMode(ResetType.MIXED).call()); assertEquals("[a.txt, mode:100644]" + "[b.txt, mode:100644]", indexState(0)); @@ -298,8 +300,8 @@ public class ResetCommandTest extends RepositoryTestCase { // 'a.txt' has already been modified in setupRepository // 'notAddedToIndex.txt' has been added to repository - git.reset().addPath(indexFile.getName()) - .addPath(untrackedFile.getName()).call(); + assertSameAsHead(git.reset().addPath(indexFile.getName()) + .addPath(untrackedFile.getName()).call()); DirCacheEntry postReset = DirCache.read(db.getIndexFile(), db.getFS()) .getEntry(indexFile.getName()); @@ -329,7 +331,7 @@ public class ResetCommandTest extends RepositoryTestCase { git.add().addFilepattern(untrackedFile.getName()).call(); // 'dir/b.txt' has already been modified in setupRepository - git.reset().addPath("dir").call(); + assertSameAsHead(git.reset().addPath("dir").call()); DirCacheEntry postReset = DirCache.read(db.getIndexFile(), db.getFS()) .getEntry("dir/b.txt"); @@ -358,9 +360,9 @@ public class ResetCommandTest extends RepositoryTestCase { // 'a.txt' has already been modified in setupRepository // 'notAddedToIndex.txt' has been added to repository // reset to the inital commit - git.reset().setRef(initialCommit.getName()) - .addPath(indexFile.getName()) - .addPath(untrackedFile.getName()).call(); + assertSameAsHead(git.reset().setRef(initialCommit.getName()) + .addPath(indexFile.getName()).addPath(untrackedFile.getName()) + .call()); // check that HEAD hasn't moved ObjectId head = db.resolve(Constants.HEAD); @@ -397,7 +399,7 @@ public class ResetCommandTest extends RepositoryTestCase { + "[b.txt, mode:100644]", indexState(0)); - git.reset().addPath(file).call(); + assertSameAsHead(git.reset().addPath(file).call()); assertEquals("[a.txt, mode:100644]" + "[b.txt, mode:100644]", indexState(0)); @@ -409,7 +411,7 @@ public class ResetCommandTest extends RepositoryTestCase { writeTrashFile("a.txt", "content"); git.add().addFilepattern("a.txt").call(); // Should assume an empty tree, like in C Git 1.8.2 - git.reset().addPath("a.txt").call(); + assertSameAsHead(git.reset().addPath("a.txt").call()); DirCache cache = db.readDirCache(); DirCacheEntry aEntry = cache.getEntry("a.txt"); @@ -421,7 +423,8 @@ public class ResetCommandTest extends RepositoryTestCase { git = new Git(db); writeTrashFile("a.txt", "content"); git.add().addFilepattern("a.txt").call(); - git.reset().setRef("doesnotexist").addPath("a.txt").call(); + assertSameAsHead( + git.reset().setRef("doesnotexist").addPath("a.txt").call()); } @Test @@ -431,7 +434,7 @@ public class ResetCommandTest extends RepositoryTestCase { git.add().addFilepattern("a.txt").call(); writeTrashFile("a.txt", "modified"); // should use default mode MIXED - git.reset().call(); + assertSameAsHead(git.reset().call()); DirCache cache = db.readDirCache(); DirCacheEntry aEntry = cache.getEntry("a.txt"); @@ -452,7 +455,7 @@ public class ResetCommandTest extends RepositoryTestCase { git.add().addFilepattern(untrackedFile.getName()).call(); - git.reset().setRef(tagName).setMode(HARD).call(); + assertSameAsHead(git.reset().setRef(tagName).setMode(HARD).call()); ObjectId head = db.resolve(Constants.HEAD); assertEquals(secondCommit, head); @@ -486,7 +489,8 @@ public class ResetCommandTest extends RepositoryTestCase { result.getMergeStatus()); assertNotNull(db.readSquashCommitMsg()); - g.reset().setMode(ResetType.HARD).setRef(first.getName()).call(); + assertSameAsHead(g.reset().setMode(ResetType.HARD) + .setRef(first.getName()).call()); assertNull(db.readSquashCommitMsg()); } @@ -497,7 +501,7 @@ public class ResetCommandTest extends RepositoryTestCase { File fileA = writeTrashFile("a.txt", "content"); git.add().addFilepattern("a.txt").call(); // Should assume an empty tree, like in C Git 1.8.2 - git.reset().setMode(ResetType.HARD).call(); + assertSameAsHead(git.reset().setMode(ResetType.HARD).call()); DirCache cache = db.readDirCache(); DirCacheEntry aEntry = cache.getEntry("a.txt"); @@ -558,4 +562,14 @@ public class ResetCommandTest extends RepositoryTestCase { return dc.getEntry(path) != null; } + /** + * Asserts that a certain ref is similar to repos HEAD. + * @param ref + * @throws IOException + */ + private void assertSameAsHead(Ref ref) throws IOException { + Ref headRef = db.getRef(Constants.HEAD); + assertEquals(headRef.getName(), ref.getName()); + assertEquals(headRef.getObjectId(), ref.getObjectId()); + } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCachePathEditTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCachePathEditTest.java index 63ec85861..c85e15635 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCachePathEditTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCachePathEditTest.java @@ -43,11 +43,13 @@ package org.eclipse.jgit.dircache; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; import java.util.ArrayList; import java.util.List; import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit; +import org.eclipse.jgit.errors.DirCacheNameConflictException; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; import org.junit.Test; @@ -154,6 +156,125 @@ public class DirCachePathEditTest { assertEquals(DirCacheEntry.STAGE_3, entries.get(2).getStage()); } + @Test + public void testFileReplacesTree() throws Exception { + DirCache dc = DirCache.newInCore(); + DirCacheEditor editor = dc.editor(); + editor.add(new AddEdit("a")); + editor.add(new AddEdit("b/c")); + editor.add(new AddEdit("b/d")); + editor.add(new AddEdit("e")); + editor.finish(); + + editor = dc.editor(); + editor.add(new AddEdit("b")); + editor.finish(); + + assertEquals(3, dc.getEntryCount()); + assertEquals("a", dc.getEntry(0).getPathString()); + assertEquals("b", dc.getEntry(1).getPathString()); + assertEquals("e", dc.getEntry(2).getPathString()); + + dc.clear(); + editor = dc.editor(); + editor.add(new AddEdit("A.c")); + editor.add(new AddEdit("A/c")); + editor.add(new AddEdit("A0c")); + editor.finish(); + + editor = dc.editor(); + editor.add(new AddEdit("A")); + editor.finish(); + assertEquals(3, dc.getEntryCount()); + assertEquals("A", dc.getEntry(0).getPathString()); + assertEquals("A.c", dc.getEntry(1).getPathString()); + assertEquals("A0c", dc.getEntry(2).getPathString()); + } + + @Test + public void testTreeReplacesFile() throws Exception { + DirCache dc = DirCache.newInCore(); + DirCacheEditor editor = dc.editor(); + editor.add(new AddEdit("a")); + editor.add(new AddEdit("ab")); + editor.add(new AddEdit("b")); + editor.add(new AddEdit("e")); + editor.finish(); + + editor = dc.editor(); + editor.add(new AddEdit("b/c/d/f")); + editor.add(new AddEdit("b/g/h/i")); + editor.finish(); + + assertEquals(5, dc.getEntryCount()); + assertEquals("a", dc.getEntry(0).getPathString()); + assertEquals("ab", dc.getEntry(1).getPathString()); + assertEquals("b/c/d/f", dc.getEntry(2).getPathString()); + assertEquals("b/g/h/i", dc.getEntry(3).getPathString()); + assertEquals("e", dc.getEntry(4).getPathString()); + } + + @Test + public void testDuplicateFiles() throws Exception { + DirCache dc = DirCache.newInCore(); + DirCacheEditor editor = dc.editor(); + editor.add(new AddEdit("a")); + editor.add(new AddEdit("a")); + + try { + editor.finish(); + fail("Expected DirCacheNameConflictException to be thrown"); + } catch (DirCacheNameConflictException e) { + assertEquals("a a", e.getMessage()); + assertEquals("a", e.getPath1()); + assertEquals("a", e.getPath2()); + } + } + + @Test + public void testFileOverlapsTree() throws Exception { + DirCache dc = DirCache.newInCore(); + DirCacheEditor editor = dc.editor(); + editor.add(new AddEdit("a")); + editor.add(new AddEdit("a/b").setReplace(false)); + try { + editor.finish(); + fail("Expected DirCacheNameConflictException to be thrown"); + } catch (DirCacheNameConflictException e) { + assertEquals("a a/b", e.getMessage()); + assertEquals("a", e.getPath1()); + assertEquals("a/b", e.getPath2()); + } + + editor = dc.editor(); + editor.add(new AddEdit("A.c")); + editor.add(new AddEdit("A/c").setReplace(false)); + editor.add(new AddEdit("A0c")); + editor.add(new AddEdit("A")); + try { + editor.finish(); + fail("Expected DirCacheNameConflictException to be thrown"); + } catch (DirCacheNameConflictException e) { + assertEquals("A A/c", e.getMessage()); + assertEquals("A", e.getPath1()); + assertEquals("A/c", e.getPath2()); + } + + editor = dc.editor(); + editor.add(new AddEdit("A.c")); + editor.add(new AddEdit("A/b/c/d").setReplace(false)); + editor.add(new AddEdit("A/b/c")); + editor.add(new AddEdit("A0c")); + try { + editor.finish(); + fail("Expected DirCacheNameConflictException to be thrown"); + } catch (DirCacheNameConflictException e) { + assertEquals("A/b/c A/b/c/d", e.getMessage()); + assertEquals("A/b/c", e.getPath1()); + assertEquals("A/b/c/d", e.getPath2()); + } + } + private static DirCacheEntry createEntry(String path, int stage) { DirCacheEntry entry = new DirCacheEntry(path, stage); entry.setFileMode(FileMode.REGULAR_FILE); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java index b6649b3f0..524d0b8e7 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java @@ -409,6 +409,7 @@ public class RepoCommandTest extends RepositoryTestCase { .append("") .append("") + .append("") .append("").append(""); JGitTestUtil.writeTrashFile(tempDb, "manifest.xml", xmlContent.toString()); @@ -423,8 +424,12 @@ public class RepoCommandTest extends RepositoryTestCase { .getRepository(); // The Hello file should exist File hello = new File(localDb.getWorkTree(), "Hello"); - localDb.close(); assertTrue("The Hello file should exist", hello.exists()); + // The foo/Hello file should be skipped. + File foohello = new File(localDb.getWorkTree(), "foo/Hello"); + assertFalse( + "The foo/Hello file should be skipped", foohello.exists()); + localDb.close(); // The content of Hello file should be expected BufferedReader reader = new BufferedReader(new FileReader(hello)); String content = reader.readLine(); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileRepositoryBuilderTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileRepositoryBuilderTest.java index 2d72d2373..dca356434 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileRepositoryBuilderTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileRepositoryBuilderTest.java @@ -107,7 +107,7 @@ public class FileRepositoryBuilderTest extends LocalDiskRepositoryTestCase { Repository r = createWorkRepository(); StoredConfig config = r.getConfig(); config.setLong(ConfigConstants.CONFIG_CORE_SECTION, null, - ConfigConstants.CONFIG_KEY_REPO_FORMAT_VERSION, 1); + ConfigConstants.CONFIG_KEY_REPO_FORMAT_VERSION, 999999); config.save(); try { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java index bc880a13e..01d6ee68e 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java @@ -43,11 +43,13 @@ package org.eclipse.jgit.internal.storage.file; +import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.eclipse.jgit.internal.storage.pack.PackWriter.NONE; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -66,19 +68,19 @@ import java.util.Set; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.internal.storage.file.PackIndex.MutableEntry; import org.eclipse.jgit.internal.storage.pack.PackWriter; -import org.eclipse.jgit.internal.storage.pack.PackWriter.ObjectIdSet; import org.eclipse.jgit.junit.JGitTestUtil; import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.junit.TestRepository.BranchBuilder; -import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdSet; import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.revwalk.RevBlob; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevObject; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.storage.pack.PackConfig; +import org.eclipse.jgit.storage.pack.PackStatistics; import org.eclipse.jgit.test.resources.SampleDataRepositoryTestCase; import org.eclipse.jgit.transport.PackParser; import org.junit.After; @@ -87,9 +89,6 @@ import org.junit.Test; public class PackWriterTest extends SampleDataRepositoryTestCase { - private static final Set EMPTY_SET_OBJECT = Collections - . emptySet(); - private static final List EMPTY_LIST_REVS = Collections . emptyList(); @@ -170,7 +169,7 @@ public class PackWriterTest extends SampleDataRepositoryTestCase { */ @Test public void testWriteEmptyPack1() throws IOException { - createVerifyOpenPack(EMPTY_SET_OBJECT, EMPTY_SET_OBJECT, false, false); + createVerifyOpenPack(NONE, NONE, false, false); assertEquals(0, writer.getObjectCount()); assertEquals(0, pack.getObjectCount()); @@ -203,8 +202,8 @@ public class PackWriterTest extends SampleDataRepositoryTestCase { final ObjectId nonExisting = ObjectId .fromString("0000000000000000000000000000000000000001"); try { - createVerifyOpenPack(EMPTY_SET_OBJECT, Collections.singleton( - nonExisting), false, false); + createVerifyOpenPack(NONE, Collections.singleton(nonExisting), + false, false); fail("Should have thrown MissingObjectException"); } catch (MissingObjectException x) { // expected @@ -220,8 +219,8 @@ public class PackWriterTest extends SampleDataRepositoryTestCase { public void testIgnoreNonExistingObjects() throws IOException { final ObjectId nonExisting = ObjectId .fromString("0000000000000000000000000000000000000001"); - createVerifyOpenPack(EMPTY_SET_OBJECT, Collections.singleton( - nonExisting), false, true); + createVerifyOpenPack(NONE, Collections.singleton(nonExisting), + false, true); // shouldn't throw anything } @@ -239,8 +238,8 @@ public class PackWriterTest extends SampleDataRepositoryTestCase { final ObjectId nonExisting = ObjectId .fromString("0000000000000000000000000000000000000001"); new GC(db).gc(); - createVerifyOpenPack(EMPTY_SET_OBJECT, - Collections.singleton(nonExisting), false, true, true); + createVerifyOpenPack(NONE, Collections.singleton(nonExisting), false, + true, true); // shouldn't throw anything } @@ -437,6 +436,38 @@ public class PackWriterTest extends SampleDataRepositoryTestCase { assertTrue(sizePack4 > sizePack4Thin); } + @Test + public void testDeltaStatistics() throws Exception { + config.setDeltaCompress(true); + FileRepository repo = createBareRepository(); + TestRepository testRepo = new TestRepository(repo); + ArrayList blobs = new ArrayList<>(); + blobs.add(testRepo.blob(genDeltableData(1000))); + blobs.add(testRepo.blob(genDeltableData(1005))); + + try (PackWriter pw = new PackWriter(repo)) { + NullProgressMonitor m = NullProgressMonitor.INSTANCE; + pw.preparePack(blobs.iterator()); + pw.writePack(m, m, os); + PackStatistics stats = pw.getStatistics(); + assertEquals(1, stats.getTotalDeltas()); + assertTrue("Delta bytes not set.", + stats.byObjectType(OBJ_BLOB).getDeltaBytes() > 0); + } + } + + // Generate consistent junk data for building files that delta well + private String genDeltableData(int length) { + assertTrue("Generated data must have a length > 0", length > 0); + char[] data = {'a', 'b', 'c', '\n'}; + StringBuilder builder = new StringBuilder(length); + for (int i = 0; i < length; i++) { + builder.append(data[i % 4]); + } + return builder.toString(); + } + + @Test public void testWriteIndex() throws Exception { config.setIndexVersion(2); @@ -494,7 +525,7 @@ public class PackWriterTest extends SampleDataRepositoryTestCase { RevCommit c2 = bb.commit().add("f", contentB).create(); testRepo.getRevWalk().parseHeaders(c2); PackIndex pf2 = writePack(repo, Collections.singleton(c2), - Collections.singleton(objectIdSet(pf1))); + Collections. singleton(pf1)); assertContent( pf2, Arrays.asList(c2.getId(), c2.getTree().getId(), @@ -519,8 +550,7 @@ public class PackWriterTest extends SampleDataRepositoryTestCase { pw.setReuseDeltaCommits(false); for (ObjectIdSet idx : excludeObjects) pw.excludeObjects(idx); - pw.preparePack(NullProgressMonitor.INSTANCE, want, - Collections. emptySet()); + pw.preparePack(NullProgressMonitor.INSTANCE, want, NONE); String id = pw.computeName().getName(); File packdir = new File(repo.getObjectsDirectory(), "pack"); File packFile = new File(packdir, "pack-" + id + ".pack"); @@ -543,7 +573,7 @@ public class PackWriterTest extends SampleDataRepositoryTestCase { final HashSet interestings = new HashSet(); interestings.add(ObjectId .fromString("82c6b885ff600be425b4ea96dee75dca255b69e7")); - createVerifyOpenPack(interestings, EMPTY_SET_OBJECT, false, false); + createVerifyOpenPack(interestings, NONE, false, false); final ObjectId expectedOrder[] = new ObjectId[] { ObjectId.fromString("82c6b885ff600be425b4ea96dee75dca255b69e7"), @@ -699,12 +729,4 @@ public class PackWriterTest extends SampleDataRepositoryTestCase { assertEquals(objectsOrder[i++].toObjectId(), me.toObjectId()); } } - - private static ObjectIdSet objectIdSet(final PackIndex idx) { - return new ObjectIdSet() { - public boolean contains(AnyObjectId objectId) { - return idx.hasObject(objectId); - } - }; - } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java index a92ff8d04..f4d655f86 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java @@ -67,7 +67,6 @@ import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.CommitBuilder; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileMode; -import org.eclipse.jgit.lib.FileTreeEntry; import org.eclipse.jgit.lib.ObjectDatabase; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; @@ -75,7 +74,6 @@ import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.TagBuilder; -import org.eclipse.jgit.lib.Tree; import org.eclipse.jgit.lib.TreeFormatter; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevTag; @@ -419,29 +417,6 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase { assertEquals(c.getCommitter(), c2.getCommitterIdent()); } - @Test - public void test012_SubtreeExternalSorting() throws IOException { - final ObjectId emptyBlob = insertEmptyBlob(); - final Tree t = new Tree(db); - final FileTreeEntry e0 = t.addFile("a-"); - final FileTreeEntry e1 = t.addFile("a-b"); - final FileTreeEntry e2 = t.addFile("a/b"); - final FileTreeEntry e3 = t.addFile("a="); - final FileTreeEntry e4 = t.addFile("a=b"); - - e0.setId(emptyBlob); - e1.setId(emptyBlob); - e2.setId(emptyBlob); - e3.setId(emptyBlob); - e4.setId(emptyBlob); - - final Tree a = (Tree) t.findTreeMember("a"); - a.setId(insertTree(a)); - assertEquals(ObjectId - .fromString("b47a8f0a4190f7572e11212769090523e23eb1ea"), - insertTree(t)); - } - @Test public void test020_createBlobTag() throws IOException { final ObjectId emptyId = insertEmptyBlob(); @@ -465,9 +440,8 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase { @Test public void test021_createTreeTag() throws IOException { final ObjectId emptyId = insertEmptyBlob(); - final Tree almostEmptyTree = new Tree(db); - almostEmptyTree.addEntry(new FileTreeEntry(almostEmptyTree, emptyId, - "empty".getBytes(), false)); + TreeFormatter almostEmptyTree = new TreeFormatter(); + almostEmptyTree.append("empty", FileMode.REGULAR_FILE, emptyId); final ObjectId almostEmptyTreeId = insertTree(almostEmptyTree); final TagBuilder t = new TagBuilder(); t.setObjectId(almostEmptyTreeId, Constants.OBJ_TREE); @@ -489,9 +463,8 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase { @Test public void test022_createCommitTag() throws IOException { final ObjectId emptyId = insertEmptyBlob(); - final Tree almostEmptyTree = new Tree(db); - almostEmptyTree.addEntry(new FileTreeEntry(almostEmptyTree, emptyId, - "empty".getBytes(), false)); + TreeFormatter almostEmptyTree = new TreeFormatter(); + almostEmptyTree.append("empty", FileMode.REGULAR_FILE, emptyId); final ObjectId almostEmptyTreeId = insertTree(almostEmptyTree); final CommitBuilder almostEmptyCommit = new CommitBuilder(); almostEmptyCommit.setAuthor(new PersonIdent(author, 1154236443000L, @@ -521,9 +494,8 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase { @Test public void test023_createCommitNonAnullii() throws IOException { final ObjectId emptyId = insertEmptyBlob(); - final Tree almostEmptyTree = new Tree(db); - almostEmptyTree.addEntry(new FileTreeEntry(almostEmptyTree, emptyId, - "empty".getBytes(), false)); + TreeFormatter almostEmptyTree = new TreeFormatter(); + almostEmptyTree.append("empty", FileMode.REGULAR_FILE, emptyId); final ObjectId almostEmptyTreeId = insertTree(almostEmptyTree); CommitBuilder commit = new CommitBuilder(); commit.setTreeId(almostEmptyTreeId); @@ -543,9 +515,8 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase { @Test public void test024_createCommitNonAscii() throws IOException { final ObjectId emptyId = insertEmptyBlob(); - final Tree almostEmptyTree = new Tree(db); - almostEmptyTree.addEntry(new FileTreeEntry(almostEmptyTree, emptyId, - "empty".getBytes(), false)); + TreeFormatter almostEmptyTree = new TreeFormatter(); + almostEmptyTree.append("empty", FileMode.REGULAR_FILE, emptyId); final ObjectId almostEmptyTreeId = insertTree(almostEmptyTree); CommitBuilder commit = new CommitBuilder(); commit.setTreeId(almostEmptyTreeId); @@ -747,14 +718,6 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase { return emptyId; } - private ObjectId insertTree(Tree tree) throws IOException { - try (ObjectInserter oi = db.newObjectInserter()) { - ObjectId id = oi.insert(Constants.OBJ_TREE, tree.format()); - oi.flush(); - return id; - } - } - private ObjectId insertTree(TreeFormatter tree) throws IOException { try (ObjectInserter oi = db.newObjectInserter()) { ObjectId id = oi.insert(tree); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/RefTreeDatabaseTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/RefTreeDatabaseTest.java new file mode 100644 index 000000000..020d1b1b5 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/RefTreeDatabaseTest.java @@ -0,0 +1,685 @@ +/* + * Copyright (C) 2010, 2013, 2016 Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.internal.storage.reftree; + +import static org.eclipse.jgit.lib.Constants.HEAD; +import static org.eclipse.jgit.lib.Constants.R_HEADS; +import static org.eclipse.jgit.lib.Constants.R_TAGS; +import static org.eclipse.jgit.lib.Ref.Storage.LOOSE; +import static org.eclipse.jgit.lib.Ref.Storage.PACKED; +import static org.eclipse.jgit.lib.RefDatabase.ALL; +import static org.eclipse.jgit.transport.ReceiveCommand.Result.LOCK_FAILURE; +import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK; +import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_NONFASTFORWARD; +import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.IOException; +import java.text.MessageFormat; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +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.lib.AnyObjectId; +import org.eclipse.jgit.lib.BatchRefUpdate; +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.NullProgressMonitor; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdRef; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefDatabase; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.SymbolicRef; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevTag; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.ReceiveCommand; +import org.junit.Before; +import org.junit.Test; + +public class RefTreeDatabaseTest { + private InMemRefTreeRepo repo; + private RefTreeDatabase refdb; + private RefDatabase bootstrap; + + private TestRepository testRepo; + private RevCommit A; + private RevCommit B; + private RevTag v1_0; + + @Before + public void setUp() throws Exception { + repo = new InMemRefTreeRepo(new DfsRepositoryDescription("test")); + bootstrap = refdb.getBootstrap(); + + testRepo = new TestRepository<>(repo); + A = testRepo.commit().create(); + B = testRepo.commit(testRepo.getRevWalk().parseCommit(A)); + v1_0 = testRepo.tag("v1_0", B); + testRepo.getRevWalk().parseBody(v1_0); + } + + @Test + public void testSupportsAtomic() { + assertTrue(refdb.performsAtomicTransactions()); + } + + @Test + public void testGetRefs_EmptyDatabase() throws IOException { + assertTrue("no references", refdb.getRefs(ALL).isEmpty()); + assertTrue("no references", refdb.getRefs(R_HEADS).isEmpty()); + assertTrue("no references", refdb.getRefs(R_TAGS).isEmpty()); + } + + @Test + public void testGetRefs_HeadOnOneBranch() throws IOException { + symref(HEAD, "refs/heads/master"); + update("refs/heads/master", A); + + Map all = refdb.getRefs(ALL); + assertEquals(2, all.size()); + assertTrue("has HEAD", all.containsKey(HEAD)); + assertTrue("has master", all.containsKey("refs/heads/master")); + + Ref head = all.get(HEAD); + Ref master = all.get("refs/heads/master"); + + assertEquals(HEAD, head.getName()); + assertTrue(head.isSymbolic()); + assertSame(LOOSE, head.getStorage()); + assertSame("uses same ref as target", master, head.getTarget()); + + assertEquals("refs/heads/master", master.getName()); + assertFalse(master.isSymbolic()); + assertSame(PACKED, master.getStorage()); + assertEquals(A, master.getObjectId()); + } + + @Test + public void testGetRefs_DetachedHead() throws IOException { + update(HEAD, A); + + Map all = refdb.getRefs(ALL); + assertEquals(1, all.size()); + assertTrue("has HEAD", all.containsKey(HEAD)); + + Ref head = all.get(HEAD); + assertEquals(HEAD, head.getName()); + assertFalse(head.isSymbolic()); + assertSame(PACKED, head.getStorage()); + assertEquals(A, head.getObjectId()); + } + + @Test + public void testGetRefs_DeeplyNestedBranch() throws IOException { + String name = "refs/heads/a/b/c/d/e/f/g/h/i/j/k"; + update(name, A); + + Map all = refdb.getRefs(ALL); + assertEquals(1, all.size()); + + Ref r = all.get(name); + assertEquals(name, r.getName()); + assertFalse(r.isSymbolic()); + assertSame(PACKED, r.getStorage()); + assertEquals(A, r.getObjectId()); + } + + @Test + public void testGetRefs_HeadBranchNotBorn() throws IOException { + update("refs/heads/A", A); + update("refs/heads/B", B); + + Map all = refdb.getRefs(ALL); + assertEquals(2, all.size()); + assertFalse("no HEAD", all.containsKey(HEAD)); + + Ref a = all.get("refs/heads/A"); + Ref b = all.get("refs/heads/B"); + + assertEquals(A, a.getObjectId()); + assertEquals(B, b.getObjectId()); + + assertEquals("refs/heads/A", a.getName()); + assertEquals("refs/heads/B", b.getName()); + } + + @Test + public void testGetRefs_HeadsOnly() throws IOException { + update("refs/heads/A", A); + update("refs/heads/B", B); + update("refs/tags/v1.0", v1_0); + + Map heads = refdb.getRefs(R_HEADS); + assertEquals(2, heads.size()); + + Ref a = heads.get("A"); + Ref b = heads.get("B"); + + assertEquals("refs/heads/A", a.getName()); + assertEquals("refs/heads/B", b.getName()); + + assertEquals(A, a.getObjectId()); + assertEquals(B, b.getObjectId()); + } + + @Test + public void testGetRefs_TagsOnly() throws IOException { + update("refs/heads/A", A); + update("refs/heads/B", B); + update("refs/tags/v1.0", v1_0); + + Map tags = refdb.getRefs(R_TAGS); + assertEquals(1, tags.size()); + + Ref a = tags.get("v1.0"); + assertEquals("refs/tags/v1.0", a.getName()); + assertEquals(v1_0, a.getObjectId()); + assertTrue(a.isPeeled()); + assertEquals(v1_0.getObject(), a.getPeeledObjectId()); + } + + @Test + public void testGetRefs_HeadsSymref() throws IOException { + symref("refs/heads/other", "refs/heads/master"); + update("refs/heads/master", A); + + Map heads = refdb.getRefs(R_HEADS); + assertEquals(2, heads.size()); + + Ref master = heads.get("master"); + Ref other = heads.get("other"); + + assertEquals("refs/heads/master", master.getName()); + assertEquals(A, master.getObjectId()); + + assertEquals("refs/heads/other", other.getName()); + assertEquals(A, other.getObjectId()); + assertSame(master, other.getTarget()); + } + + @Test + public void testGetRefs_InvalidPrefixes() throws IOException { + update("refs/heads/A", A); + + assertTrue("empty refs/heads", refdb.getRefs("refs/heads").isEmpty()); + assertTrue("empty objects", refdb.getRefs("objects").isEmpty()); + assertTrue("empty objects/", refdb.getRefs("objects/").isEmpty()); + } + + @Test + public void testGetRefs_DiscoversNew() throws IOException { + update("refs/heads/master", A); + Map orig = refdb.getRefs(ALL); + + update("refs/heads/next", B); + Map next = refdb.getRefs(ALL); + + assertEquals(1, orig.size()); + assertEquals(2, next.size()); + + assertFalse(orig.containsKey("refs/heads/next")); + assertTrue(next.containsKey("refs/heads/next")); + + assertEquals(A, next.get("refs/heads/master").getObjectId()); + assertEquals(B, next.get("refs/heads/next").getObjectId()); + } + + @Test + public void testGetRefs_DiscoversModified() throws IOException { + symref(HEAD, "refs/heads/master"); + update("refs/heads/master", A); + + Map all = refdb.getRefs(ALL); + assertEquals(A, all.get(HEAD).getObjectId()); + + update("refs/heads/master", B); + all = refdb.getRefs(ALL); + assertEquals(B, all.get(HEAD).getObjectId()); + assertEquals(B, refdb.exactRef(HEAD).getObjectId()); + } + + @Test + public void testGetRefs_CycleInSymbolicRef() throws IOException { + symref("refs/1", "refs/2"); + symref("refs/2", "refs/3"); + symref("refs/3", "refs/4"); + symref("refs/4", "refs/5"); + symref("refs/5", "refs/end"); + update("refs/end", A); + + Map all = refdb.getRefs(ALL); + Ref r = all.get("refs/1"); + assertNotNull("has 1", r); + + assertEquals("refs/1", r.getName()); + assertEquals(A, r.getObjectId()); + assertTrue(r.isSymbolic()); + + r = r.getTarget(); + assertEquals("refs/2", r.getName()); + assertEquals(A, r.getObjectId()); + assertTrue(r.isSymbolic()); + + r = r.getTarget(); + assertEquals("refs/3", r.getName()); + assertEquals(A, r.getObjectId()); + assertTrue(r.isSymbolic()); + + r = r.getTarget(); + assertEquals("refs/4", r.getName()); + assertEquals(A, r.getObjectId()); + assertTrue(r.isSymbolic()); + + r = r.getTarget(); + assertEquals("refs/5", r.getName()); + assertEquals(A, r.getObjectId()); + assertTrue(r.isSymbolic()); + + r = r.getTarget(); + assertEquals("refs/end", r.getName()); + assertEquals(A, r.getObjectId()); + assertFalse(r.isSymbolic()); + + symref("refs/5", "refs/6"); + symref("refs/6", "refs/end"); + all = refdb.getRefs(ALL); + assertNull("mising 1 due to cycle", all.get("refs/1")); + assertEquals(A, all.get("refs/2").getObjectId()); + assertEquals(A, all.get("refs/3").getObjectId()); + assertEquals(A, all.get("refs/4").getObjectId()); + assertEquals(A, all.get("refs/5").getObjectId()); + assertEquals(A, all.get("refs/6").getObjectId()); + assertEquals(A, all.get("refs/end").getObjectId()); + } + + @Test + public void testGetRef_NonExistingBranchConfig() throws IOException { + assertNull("find branch config", refdb.getRef("config")); + assertNull("find branch config", refdb.getRef("refs/heads/config")); + } + + @Test + public void testGetRef_FindBranchConfig() throws IOException { + update("refs/heads/config", A); + + for (String t : new String[] { "config", "refs/heads/config" }) { + Ref r = refdb.getRef(t); + assertNotNull("find branch config (" + t + ")", r); + assertEquals("for " + t, "refs/heads/config", r.getName()); + assertEquals("for " + t, A, r.getObjectId()); + } + } + + @Test + public void testFirstExactRef() throws IOException { + update("refs/heads/A", A); + update("refs/tags/v1.0", v1_0); + + Ref a = refdb.firstExactRef("refs/heads/A", "refs/tags/v1.0"); + Ref one = refdb.firstExactRef("refs/tags/v1.0", "refs/heads/A"); + + assertEquals("refs/heads/A", a.getName()); + assertEquals("refs/tags/v1.0", one.getName()); + + assertEquals(A, a.getObjectId()); + assertEquals(v1_0, one.getObjectId()); + } + + @Test + public void testExactRef_DiscoversModified() throws IOException { + symref(HEAD, "refs/heads/master"); + update("refs/heads/master", A); + assertEquals(A, refdb.exactRef(HEAD).getObjectId()); + + update("refs/heads/master", B); + assertEquals(B, refdb.exactRef(HEAD).getObjectId()); + } + + @Test + public void testIsNameConflicting() throws IOException { + update("refs/heads/a/b", A); + update("refs/heads/q", B); + + // new references cannot replace an existing container + assertTrue(refdb.isNameConflicting("refs")); + assertTrue(refdb.isNameConflicting("refs/heads")); + assertTrue(refdb.isNameConflicting("refs/heads/a")); + + // existing reference is not conflicting + assertFalse(refdb.isNameConflicting("refs/heads/a/b")); + + // new references are not conflicting + assertFalse(refdb.isNameConflicting("refs/heads/a/d")); + assertFalse(refdb.isNameConflicting("refs/heads/master")); + + // existing reference must not be used as a container + assertTrue(refdb.isNameConflicting("refs/heads/a/b/c")); + assertTrue(refdb.isNameConflicting("refs/heads/q/master")); + + // refs/txn/ names always conflict. + assertTrue(refdb.isNameConflicting(refdb.getTxnCommitted())); + assertTrue(refdb.isNameConflicting("refs/txn/foo")); + } + + @Test + public void testUpdate_RefusesRefsTxnNamespace() throws IOException { + ObjectId txnId = getTxnCommitted(); + + RefUpdate u = refdb.newUpdate("refs/txn/tmp", false); + u.setNewObjectId(B); + assertEquals(RefUpdate.Result.LOCK_FAILURE, u.update()); + assertEquals(txnId, getTxnCommitted()); + + ReceiveCommand cmd = command(null, B, "refs/txn/tmp"); + BatchRefUpdate batch = refdb.newBatchUpdate(); + batch.addCommand(cmd); + batch.execute(new RevWalk(repo), NullProgressMonitor.INSTANCE); + + assertEquals(REJECTED_OTHER_REASON, cmd.getResult()); + assertEquals(MessageFormat.format(JGitText.get().invalidRefName, + "refs/txn/tmp"), cmd.getMessage()); + assertEquals(txnId, getTxnCommitted()); + } + + @Test + public void testUpdate_RefusesDotLockInRefName() throws IOException { + ObjectId txnId = getTxnCommitted(); + + RefUpdate u = refdb.newUpdate("refs/heads/pu.lock", false); + u.setNewObjectId(B); + assertEquals(RefUpdate.Result.REJECTED, u.update()); + assertEquals(txnId, getTxnCommitted()); + + ReceiveCommand cmd = command(null, B, "refs/heads/pu.lock"); + BatchRefUpdate batch = refdb.newBatchUpdate(); + batch.addCommand(cmd); + batch.execute(new RevWalk(repo), NullProgressMonitor.INSTANCE); + + assertEquals(REJECTED_OTHER_REASON, cmd.getResult()); + assertEquals(JGitText.get().funnyRefname, cmd.getMessage()); + assertEquals(txnId, getTxnCommitted()); + } + + @Test + public void testBatchRefUpdate_NonFastForwardAborts() throws IOException { + update("refs/heads/master", A); + update("refs/heads/masters", B); + ObjectId txnId = getTxnCommitted(); + + List commands = Arrays.asList( + command(A, B, "refs/heads/master"), + command(B, A, "refs/heads/masters")); + BatchRefUpdate batchUpdate = refdb.newBatchUpdate(); + batchUpdate.addCommand(commands); + batchUpdate.execute(new RevWalk(repo), NullProgressMonitor.INSTANCE); + assertEquals(txnId, getTxnCommitted()); + + assertEquals(REJECTED_NONFASTFORWARD, + commands.get(1).getResult()); + assertEquals(REJECTED_OTHER_REASON, + commands.get(0).getResult()); + assertEquals(JGitText.get().transactionAborted, + commands.get(0).getMessage()); + } + + @Test + public void testBatchRefUpdate_ForceUpdate() throws IOException { + update("refs/heads/master", A); + update("refs/heads/masters", B); + ObjectId txnId = getTxnCommitted(); + + List commands = Arrays.asList( + command(A, B, "refs/heads/master"), + command(B, A, "refs/heads/masters")); + BatchRefUpdate batchUpdate = refdb.newBatchUpdate(); + batchUpdate.setAllowNonFastForwards(true); + batchUpdate.addCommand(commands); + batchUpdate.execute(new RevWalk(repo), NullProgressMonitor.INSTANCE); + assertNotEquals(txnId, getTxnCommitted()); + + Map refs = refdb.getRefs(ALL); + assertEquals(OK, commands.get(0).getResult()); + assertEquals(OK, commands.get(1).getResult()); + assertEquals( + "[refs/heads/master, refs/heads/masters]", + refs.keySet().toString()); + assertEquals(B.getId(), refs.get("refs/heads/master").getObjectId()); + assertEquals(A.getId(), refs.get("refs/heads/masters").getObjectId()); + } + + @Test + public void testBatchRefUpdate_NonFastForwardDoesNotDoExpensiveMergeCheck() + throws IOException { + update("refs/heads/master", B); + ObjectId txnId = getTxnCommitted(); + + List commands = Arrays.asList( + command(B, A, "refs/heads/master")); + BatchRefUpdate batchUpdate = refdb.newBatchUpdate(); + batchUpdate.setAllowNonFastForwards(true); + batchUpdate.addCommand(commands); + batchUpdate.execute(new RevWalk(repo) { + @Override + public boolean isMergedInto(RevCommit base, RevCommit tip) { + fail("isMergedInto() should not be called"); + return false; + } + }, NullProgressMonitor.INSTANCE); + assertNotEquals(txnId, getTxnCommitted()); + + Map refs = refdb.getRefs(ALL); + assertEquals(OK, commands.get(0).getResult()); + assertEquals(A.getId(), refs.get("refs/heads/master").getObjectId()); + } + + @Test + public void testBatchRefUpdate_ConflictCausesAbort() throws IOException { + update("refs/heads/master", A); + update("refs/heads/masters", B); + ObjectId txnId = getTxnCommitted(); + + List commands = Arrays.asList( + command(A, B, "refs/heads/master"), + command(null, A, "refs/heads/master/x"), + command(null, A, "refs/heads")); + BatchRefUpdate batchUpdate = refdb.newBatchUpdate(); + batchUpdate.setAllowNonFastForwards(true); + batchUpdate.addCommand(commands); + batchUpdate.execute(new RevWalk(repo), NullProgressMonitor.INSTANCE); + assertEquals(txnId, getTxnCommitted()); + + assertEquals(LOCK_FAILURE, commands.get(0).getResult()); + + assertEquals(REJECTED_OTHER_REASON, commands.get(1).getResult()); + assertEquals(JGitText.get().transactionAborted, + commands.get(1).getMessage()); + + assertEquals(REJECTED_OTHER_REASON, commands.get(2).getResult()); + assertEquals(JGitText.get().transactionAborted, + commands.get(2).getMessage()); + } + + @Test + public void testBatchRefUpdate_NoConflictIfDeleted() throws IOException { + update("refs/heads/master", A); + update("refs/heads/masters", B); + ObjectId txnId = getTxnCommitted(); + + List commands = Arrays.asList( + command(A, B, "refs/heads/master"), + command(null, A, "refs/heads/masters/x"), + command(B, null, "refs/heads/masters")); + BatchRefUpdate batchUpdate = refdb.newBatchUpdate(); + batchUpdate.setAllowNonFastForwards(true); + batchUpdate.addCommand(commands); + batchUpdate.execute(new RevWalk(repo), NullProgressMonitor.INSTANCE); + assertNotEquals(txnId, getTxnCommitted()); + + assertEquals(OK, commands.get(0).getResult()); + assertEquals(OK, commands.get(1).getResult()); + assertEquals(OK, commands.get(2).getResult()); + + Map refs = refdb.getRefs(ALL); + assertEquals( + "[refs/heads/master, refs/heads/masters/x]", + refs.keySet().toString()); + assertEquals(A.getId(), refs.get("refs/heads/masters/x").getObjectId()); + } + + private ObjectId getTxnCommitted() throws IOException { + Ref r = bootstrap.exactRef(refdb.getTxnCommitted()); + if (r != null && r.getObjectId() != null) { + return r.getObjectId(); + } + return ObjectId.zeroId(); + } + + private static ReceiveCommand command(AnyObjectId a, AnyObjectId b, + String name) { + return new ReceiveCommand( + a != null ? a.copy() : ObjectId.zeroId(), + b != null ? b.copy() : ObjectId.zeroId(), + name); + } + + private void symref(final String name, final String dst) + throws IOException { + commit(new Function() { + @Override + public boolean apply(ObjectReader reader, RefTree tree) + throws IOException { + Ref old = tree.exactRef(reader, name); + Command n = new Command( + old, + new SymbolicRef( + name, + new ObjectIdRef.Unpeeled(Ref.Storage.NEW, dst, null))); + return tree.apply(Collections.singleton(n)); + } + }); + } + + private void update(final String name, final ObjectId id) + throws IOException { + commit(new Function() { + @Override + public boolean apply(ObjectReader reader, RefTree tree) + throws IOException { + Ref old = tree.exactRef(reader, name); + Command n; + try (RevWalk rw = new RevWalk(repo)) { + n = new Command(old, Command.toRef(rw, id, name, true)); + } + return tree.apply(Collections.singleton(n)); + } + }); + } + + interface Function { + boolean apply(ObjectReader reader, RefTree tree) throws IOException; + } + + private void commit(Function fun) throws IOException { + try (ObjectReader reader = repo.newObjectReader(); + ObjectInserter inserter = repo.newObjectInserter(); + RevWalk rw = new RevWalk(reader)) { + RefUpdate u = bootstrap.newUpdate(refdb.getTxnCommitted(), false); + CommitBuilder cb = new CommitBuilder(); + testRepo.setAuthorAndCommitter(cb); + + Ref ref = bootstrap.exactRef(refdb.getTxnCommitted()); + RefTree tree; + if (ref != null && ref.getObjectId() != null) { + tree = RefTree.read(reader, rw.parseTree(ref.getObjectId())); + cb.setParentId(ref.getObjectId()); + u.setExpectedOldObjectId(ref.getObjectId()); + } else { + tree = RefTree.newEmptyTree(); + u.setExpectedOldObjectId(ObjectId.zeroId()); + } + + assertTrue(fun.apply(reader, tree)); + cb.setTreeId(tree.writeTree(inserter)); + u.setNewObjectId(inserter.insert(cb)); + inserter.flush(); + switch (u.update(rw)) { + case NEW: + case FAST_FORWARD: + break; + default: + fail("Expected " + u.getName() + " to update"); + } + } + } + + private class InMemRefTreeRepo extends InMemoryRepository { + private final RefTreeDatabase refs; + + InMemRefTreeRepo(DfsRepositoryDescription repoDesc) { + super(repoDesc); + refs = new RefTreeDatabase(this, super.getRefDatabase(), + "refs/txn/committed"); + RefTreeDatabaseTest.this.refdb = refs; + } + + public RefDatabase getRefDatabase() { + return refs; + } + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/RefTreeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/RefTreeTest.java new file mode 100644 index 000000000..8e0f38c69 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/RefTreeTest.java @@ -0,0 +1,303 @@ +/* + * Copyright (C) 2016, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.internal.storage.reftree; + +import static org.eclipse.jgit.lib.Constants.HEAD; +import static org.eclipse.jgit.lib.Constants.R_HEADS; +import static org.eclipse.jgit.lib.Constants.R_TAGS; +import static org.eclipse.jgit.lib.Ref.Storage.LOOSE; +import static org.eclipse.jgit.lib.Ref.Storage.NEW; +import static org.eclipse.jgit.transport.ReceiveCommand.Result.LOCK_FAILURE; +import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED; +import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; + +import org.eclipse.jgit.errors.MissingObjectException; +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.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdRef; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.SymbolicRef; +import org.eclipse.jgit.revwalk.RevBlob; +import org.eclipse.jgit.revwalk.RevTag; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.ReceiveCommand; +import org.junit.Before; +import org.junit.Test; + +public class RefTreeTest { + private static final String R_MASTER = R_HEADS + "master"; + private InMemoryRepository repo; + private TestRepository git; + + @Before + public void setUp() throws IOException { + repo = new InMemoryRepository(new DfsRepositoryDescription("RefTree")); + git = new TestRepository<>(repo); + } + + @Test + public void testEmptyTree() throws IOException { + RefTree tree = RefTree.newEmptyTree(); + try (ObjectReader reader = repo.newObjectReader()) { + assertNull(HEAD, tree.exactRef(reader, HEAD)); + assertNull("master", tree.exactRef(reader, R_MASTER)); + } + } + + @Test + public void testApplyThenReadMaster() throws Exception { + RefTree tree = RefTree.newEmptyTree(); + RevBlob id = git.blob("A"); + Command cmd = new Command(null, ref(R_MASTER, id)); + assertTrue(tree.apply(Collections.singletonList(cmd))); + assertSame(NOT_ATTEMPTED, cmd.getResult()); + + try (ObjectReader reader = repo.newObjectReader()) { + Ref m = tree.exactRef(reader, R_MASTER); + assertNotNull(R_MASTER, m); + assertEquals(R_MASTER, m.getName()); + assertEquals(id, m.getObjectId()); + assertTrue("peeled", m.isPeeled()); + } + } + + @Test + public void testUpdateMaster() throws Exception { + RefTree tree = RefTree.newEmptyTree(); + RevBlob id1 = git.blob("A"); + Command cmd1 = new Command(null, ref(R_MASTER, id1)); + assertTrue(tree.apply(Collections.singletonList(cmd1))); + assertSame(NOT_ATTEMPTED, cmd1.getResult()); + + RevBlob id2 = git.blob("B"); + Command cmd2 = new Command(ref(R_MASTER, id1), ref(R_MASTER, id2)); + assertTrue(tree.apply(Collections.singletonList(cmd2))); + assertSame(NOT_ATTEMPTED, cmd2.getResult()); + + try (ObjectReader reader = repo.newObjectReader()) { + Ref m = tree.exactRef(reader, R_MASTER); + assertNotNull(R_MASTER, m); + assertEquals(R_MASTER, m.getName()); + assertEquals(id2, m.getObjectId()); + assertTrue("peeled", m.isPeeled()); + } + } + + @Test + public void testHeadSymref() throws Exception { + RefTree tree = RefTree.newEmptyTree(); + RevBlob id = git.blob("A"); + Command cmd1 = new Command(null, ref(R_MASTER, id)); + Command cmd2 = new Command(null, symref(HEAD, R_MASTER)); + assertTrue(tree.apply(Arrays.asList(new Command[] { cmd1, cmd2 }))); + assertSame(NOT_ATTEMPTED, cmd1.getResult()); + assertSame(NOT_ATTEMPTED, cmd2.getResult()); + + try (ObjectReader reader = repo.newObjectReader()) { + Ref m = tree.exactRef(reader, HEAD); + assertNotNull(HEAD, m); + assertEquals(HEAD, m.getName()); + assertTrue("symbolic", m.isSymbolic()); + assertNotNull(m.getTarget()); + assertEquals(R_MASTER, m.getTarget().getName()); + assertEquals(id, m.getTarget().getObjectId()); + } + + // Writing flushes some buffers, re-read from blob. + ObjectId newId = write(tree); + try (ObjectReader reader = repo.newObjectReader(); + RevWalk rw = new RevWalk(reader)) { + tree = RefTree.read(reader, rw.parseTree(newId)); + Ref m = tree.exactRef(reader, HEAD); + assertEquals(R_MASTER, m.getTarget().getName()); + } + } + + @Test + public void testTagIsPeeled() throws Exception { + String name = "v1.0"; + RefTree tree = RefTree.newEmptyTree(); + RevBlob id = git.blob("A"); + RevTag tag = git.tag(name, id); + + String ref = R_TAGS + name; + Command cmd = create(ref, tag); + assertTrue(tree.apply(Collections.singletonList(cmd))); + assertSame(NOT_ATTEMPTED, cmd.getResult()); + + try (ObjectReader reader = repo.newObjectReader()) { + Ref m = tree.exactRef(reader, ref); + assertNotNull(ref, m); + assertEquals(ref, m.getName()); + assertEquals(tag, m.getObjectId()); + assertTrue("peeled", m.isPeeled()); + assertEquals(id, m.getPeeledObjectId()); + } + } + + @Test + public void testApplyAlreadyExists() throws Exception { + RefTree tree = RefTree.newEmptyTree(); + RevBlob a = git.blob("A"); + Command cmd = new Command(null, ref(R_MASTER, a)); + assertTrue(tree.apply(Collections.singletonList(cmd))); + ObjectId treeId = write(tree); + + RevBlob b = git.blob("B"); + Command cmd1 = create(R_MASTER, b); + Command cmd2 = create(R_MASTER, b); + assertFalse(tree.apply(Arrays.asList(new Command[] { cmd1, cmd2 }))); + assertSame(LOCK_FAILURE, cmd1.getResult()); + assertSame(REJECTED_OTHER_REASON, cmd2.getResult()); + assertEquals(JGitText.get().transactionAborted, cmd2.getMessage()); + assertEquals(treeId, write(tree)); + } + + @Test + public void testApplyWrongOldId() throws Exception { + RefTree tree = RefTree.newEmptyTree(); + RevBlob a = git.blob("A"); + Command cmd = new Command(null, ref(R_MASTER, a)); + assertTrue(tree.apply(Collections.singletonList(cmd))); + ObjectId treeId = write(tree); + + RevBlob b = git.blob("B"); + RevBlob c = git.blob("C"); + Command cmd1 = update(R_MASTER, b, c); + Command cmd2 = create(R_MASTER, b); + assertFalse(tree.apply(Arrays.asList(new Command[] { cmd1, cmd2 }))); + assertSame(LOCK_FAILURE, cmd1.getResult()); + assertSame(REJECTED_OTHER_REASON, cmd2.getResult()); + assertEquals(JGitText.get().transactionAborted, cmd2.getMessage()); + assertEquals(treeId, write(tree)); + } + + @Test + public void testApplyWrongOldIdButAlreadyCurrentIsNoOp() throws Exception { + RefTree tree = RefTree.newEmptyTree(); + RevBlob a = git.blob("A"); + Command cmd = new Command(null, ref(R_MASTER, a)); + assertTrue(tree.apply(Collections.singletonList(cmd))); + ObjectId treeId = write(tree); + + RevBlob b = git.blob("B"); + cmd = update(R_MASTER, b, a); + assertTrue(tree.apply(Collections.singletonList(cmd))); + assertEquals(treeId, write(tree)); + } + + @Test + public void testApplyCannotCreateSubdirectory() throws Exception { + RefTree tree = RefTree.newEmptyTree(); + RevBlob a = git.blob("A"); + Command cmd = new Command(null, ref(R_MASTER, a)); + assertTrue(tree.apply(Collections.singletonList(cmd))); + ObjectId treeId = write(tree); + + RevBlob b = git.blob("B"); + Command cmd1 = create(R_MASTER + "/fail", b); + assertFalse(tree.apply(Collections.singletonList(cmd1))); + assertSame(LOCK_FAILURE, cmd1.getResult()); + assertEquals(treeId, write(tree)); + } + + @Test + public void testApplyCannotCreateParentRef() throws Exception { + RefTree tree = RefTree.newEmptyTree(); + RevBlob a = git.blob("A"); + Command cmd = new Command(null, ref(R_MASTER, a)); + assertTrue(tree.apply(Collections.singletonList(cmd))); + ObjectId treeId = write(tree); + + RevBlob b = git.blob("B"); + Command cmd1 = create("refs/heads", b); + assertFalse(tree.apply(Collections.singletonList(cmd1))); + assertSame(LOCK_FAILURE, cmd1.getResult()); + assertEquals(treeId, write(tree)); + } + + private static Ref ref(String name, ObjectId id) { + return new ObjectIdRef.PeeledNonTag(LOOSE, name, id); + } + + private static Ref symref(String name, String dest) { + Ref d = new ObjectIdRef.PeeledNonTag(NEW, dest, null); + return new SymbolicRef(name, d); + } + + private Command create(String name, ObjectId id) + throws MissingObjectException, IOException { + return update(name, ObjectId.zeroId(), id); + } + + private Command update(String name, ObjectId oldId, ObjectId newId) + throws MissingObjectException, IOException { + try (RevWalk rw = new RevWalk(repo)) { + return new Command(rw, new ReceiveCommand(oldId, newId, name)); + } + } + + private ObjectId write(RefTree tree) throws IOException { + try (ObjectInserter ins = repo.newObjectInserter()) { + ObjectId id = tree.writeTree(ins); + ins.flush(); + return id; + } + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutTest.java index d768e0fa0..92901f826 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutTest.java @@ -1084,7 +1084,7 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { assertWorkDir(mkmap(linkName, "a", fname, "a")); Status st = git.status().call(); - assertFalse(st.isClean()); + assertTrue(st.isClean()); } @Test @@ -1213,9 +1213,7 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { assertWorkDir(mkmap(fname, "a")); Status st = git.status().call(); - assertFalse(st.isClean()); - assertEquals(1, st.getAdded().size()); - assertTrue(st.getAdded().contains(fname + "/dir/file1")); + assertTrue(st.isClean()); } @Test diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexDiffTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexDiffTest.java index a5cd7b5c0..7fcee3dc8 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexDiffTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexDiffTest.java @@ -99,8 +99,7 @@ public class IndexDiffTest extends RepositoryTestCase { public void testAdded() throws IOException { writeTrashFile("file1", "file1"); writeTrashFile("dir/subfile", "dir/subfile"); - Tree tree = new Tree(db); - tree.setId(insertTree(tree)); + ObjectId tree = insertTree(new TreeFormatter()); DirCache index = db.lockDirCache(); DirCacheEditor editor = index.editor(); @@ -108,7 +107,7 @@ public class IndexDiffTest extends RepositoryTestCase { editor.add(add(db, trash, "dir/subfile")); editor.commit(); FileTreeIterator iterator = new FileTreeIterator(db); - IndexDiff diff = new IndexDiff(db, tree.getId(), iterator); + IndexDiff diff = new IndexDiff(db, tree, iterator); diff.diff(); assertEquals(2, diff.getAdded().size()); assertTrue(diff.getAdded().contains("file1")); @@ -124,18 +123,16 @@ public class IndexDiffTest extends RepositoryTestCase { writeTrashFile("file2", "file2"); writeTrashFile("dir/file3", "dir/file3"); - Tree tree = new Tree(db); - tree.addFile("file2"); - tree.addFile("dir/file3"); - assertEquals(2, tree.memberCount()); - tree.findBlobMember("file2").setId(ObjectId.fromString("30d67d4672d5c05833b7192cc77a79eaafb5c7ad")); - Tree tree2 = (Tree) tree.findTreeMember("dir"); - tree2.findBlobMember("file3").setId(ObjectId.fromString("873fb8d667d05436d728c52b1d7a09528e6eb59b")); - tree2.setId(insertTree(tree2)); - tree.setId(insertTree(tree)); + TreeFormatter dir = new TreeFormatter(); + dir.append("file3", FileMode.REGULAR_FILE, ObjectId.fromString("873fb8d667d05436d728c52b1d7a09528e6eb59b")); + + TreeFormatter tree = new TreeFormatter(); + tree.append("file2", FileMode.REGULAR_FILE, ObjectId.fromString("30d67d4672d5c05833b7192cc77a79eaafb5c7ad")); + tree.append("dir", FileMode.TREE, insertTree(dir)); + ObjectId treeId = insertTree(tree); FileTreeIterator iterator = new FileTreeIterator(db); - IndexDiff diff = new IndexDiff(db, tree.getId(), iterator); + IndexDiff diff = new IndexDiff(db, treeId, iterator); diff.diff(); assertEquals(2, diff.getRemoved().size()); assertTrue(diff.getRemoved().contains("file2")); @@ -157,16 +154,16 @@ public class IndexDiffTest extends RepositoryTestCase { writeTrashFile("dir/file3", "changed"); - Tree tree = new Tree(db); - tree.addFile("file2").setId(ObjectId.fromString("0123456789012345678901234567890123456789")); - tree.addFile("dir/file3").setId(ObjectId.fromString("0123456789012345678901234567890123456789")); - assertEquals(2, tree.memberCount()); + TreeFormatter dir = new TreeFormatter(); + dir.append("file3", FileMode.REGULAR_FILE, ObjectId.fromString("0123456789012345678901234567890123456789")); + + TreeFormatter tree = new TreeFormatter(); + tree.append("dir", FileMode.TREE, insertTree(dir)); + tree.append("file2", FileMode.REGULAR_FILE, ObjectId.fromString("0123456789012345678901234567890123456789")); + ObjectId treeId = insertTree(tree); - Tree tree2 = (Tree) tree.findTreeMember("dir"); - tree2.setId(insertTree(tree2)); - tree.setId(insertTree(tree)); FileTreeIterator iterator = new FileTreeIterator(db); - IndexDiff diff = new IndexDiff(db, tree.getId(), iterator); + IndexDiff diff = new IndexDiff(db, treeId, iterator); diff.diff(); assertEquals(2, diff.getChanged().size()); assertTrue(diff.getChanged().contains("file2")); @@ -314,17 +311,16 @@ public class IndexDiffTest extends RepositoryTestCase { git.add().addFilepattern("a=c").call(); git.add().addFilepattern("a=d").call(); - Tree tree = new Tree(db); + TreeFormatter tree = new TreeFormatter(); // got the hash id'd from the data using echo -n a.b|git hash-object -t blob --stdin - tree.addFile("a.b").setId(ObjectId.fromString("f6f28df96c2b40c951164286e08be7c38ec74851")); - tree.addFile("a.c").setId(ObjectId.fromString("6bc0e647512d2a0bef4f26111e484dc87df7f5ca")); - tree.addFile("a=c").setId(ObjectId.fromString("06022365ddbd7fb126761319633bf73517770714")); - tree.addFile("a=d").setId(ObjectId.fromString("fa6414df3da87840700e9eeb7fc261dd77ccd5c2")); - - tree.setId(insertTree(tree)); + tree.append("a.b", FileMode.REGULAR_FILE, ObjectId.fromString("f6f28df96c2b40c951164286e08be7c38ec74851")); + tree.append("a.c", FileMode.REGULAR_FILE, ObjectId.fromString("6bc0e647512d2a0bef4f26111e484dc87df7f5ca")); + tree.append("a=c", FileMode.REGULAR_FILE, ObjectId.fromString("06022365ddbd7fb126761319633bf73517770714")); + tree.append("a=d", FileMode.REGULAR_FILE, ObjectId.fromString("fa6414df3da87840700e9eeb7fc261dd77ccd5c2")); + ObjectId treeId = insertTree(tree); FileTreeIterator iterator = new FileTreeIterator(db); - IndexDiff diff = new IndexDiff(db, tree.getId(), iterator); + IndexDiff diff = new IndexDiff(db, treeId, iterator); diff.diff(); assertEquals(0, diff.getChanged().size()); assertEquals(0, diff.getAdded().size()); @@ -356,24 +352,27 @@ public class IndexDiffTest extends RepositoryTestCase { .addFilepattern("a/c").addFilepattern("a=c") .addFilepattern("a=d").call(); - Tree tree = new Tree(db); + // got the hash id'd from the data using echo -n a.b|git hash-object -t blob --stdin - tree.addFile("a.b").setId(ObjectId.fromString("f6f28df96c2b40c951164286e08be7c38ec74851")); - tree.addFile("a.c").setId(ObjectId.fromString("6bc0e647512d2a0bef4f26111e484dc87df7f5ca")); - tree.addFile("a/b.b/b").setId(ObjectId.fromString("8d840bd4e2f3a48ff417c8e927d94996849933fd")); - tree.addFile("a/b").setId(ObjectId.fromString("db89c972fc57862eae378f45b74aca228037d415")); - tree.addFile("a/c").setId(ObjectId.fromString("52ad142a008aeb39694bafff8e8f1be75ed7f007")); - tree.addFile("a=c").setId(ObjectId.fromString("06022365ddbd7fb126761319633bf73517770714")); - tree.addFile("a=d").setId(ObjectId.fromString("fa6414df3da87840700e9eeb7fc261dd77ccd5c2")); - - Tree tree3 = (Tree) tree.findTreeMember("a/b.b"); - tree3.setId(insertTree(tree3)); - Tree tree2 = (Tree) tree.findTreeMember("a"); - tree2.setId(insertTree(tree2)); - tree.setId(insertTree(tree)); + TreeFormatter bb = new TreeFormatter(); + bb.append("b", FileMode.REGULAR_FILE, ObjectId.fromString("8d840bd4e2f3a48ff417c8e927d94996849933fd")); + + TreeFormatter a = new TreeFormatter(); + a.append("b", FileMode.REGULAR_FILE, ObjectId + .fromString("db89c972fc57862eae378f45b74aca228037d415")); + a.append("b.b", FileMode.TREE, insertTree(bb)); + a.append("c", FileMode.REGULAR_FILE, ObjectId.fromString("52ad142a008aeb39694bafff8e8f1be75ed7f007")); + + TreeFormatter tree = new TreeFormatter(); + tree.append("a.b", FileMode.REGULAR_FILE, ObjectId.fromString("f6f28df96c2b40c951164286e08be7c38ec74851")); + tree.append("a.c", FileMode.REGULAR_FILE, ObjectId.fromString("6bc0e647512d2a0bef4f26111e484dc87df7f5ca")); + tree.append("a", FileMode.TREE, insertTree(a)); + tree.append("a=c", FileMode.REGULAR_FILE, ObjectId.fromString("06022365ddbd7fb126761319633bf73517770714")); + tree.append("a=d", FileMode.REGULAR_FILE, ObjectId.fromString("fa6414df3da87840700e9eeb7fc261dd77ccd5c2")); + ObjectId treeId = insertTree(tree); FileTreeIterator iterator = new FileTreeIterator(db); - IndexDiff diff = new IndexDiff(db, tree.getId(), iterator); + IndexDiff diff = new IndexDiff(db, treeId, iterator); diff.diff(); assertEquals(0, diff.getChanged().size()); assertEquals(0, diff.getAdded().size()); @@ -383,9 +382,9 @@ public class IndexDiffTest extends RepositoryTestCase { assertEquals(Collections.EMPTY_SET, diff.getUntrackedFolders()); } - private ObjectId insertTree(Tree tree) throws IOException { + private ObjectId insertTree(TreeFormatter tree) throws IOException { try (ObjectInserter oi = db.newObjectInserter()) { - ObjectId id = oi.insert(Constants.OBJ_TREE, tree.format()); + ObjectId id = oi.insert(tree); oi.flush(); return id; } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectCheckerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectCheckerTest.java index 3abe81cf8..43160fb11 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectCheckerTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectCheckerTest.java @@ -45,8 +45,25 @@ package org.eclipse.jgit.lib; import static java.lang.Integer.valueOf; -import static java.lang.Long.valueOf; +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH; +import static org.eclipse.jgit.lib.Constants.OBJ_BAD; +import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; +import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT; +import static org.eclipse.jgit.lib.Constants.OBJ_TAG; +import static org.eclipse.jgit.lib.Constants.OBJ_TREE; +import static org.eclipse.jgit.lib.Constants.encode; +import static org.eclipse.jgit.lib.Constants.encodeASCII; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.DUPLICATE_ENTRIES; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.EMPTY_NAME; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.FULL_PATHNAME; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.HAS_DOT; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.HAS_DOTDOT; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.HAS_DOTGIT; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.NULL_SHA1; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.TREE_NOT_SORTED; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.ZERO_PADDED_FILEMODE; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; import static org.junit.Assert.fail; import java.io.UnsupportedEncodingException; @@ -67,15 +84,10 @@ public class ObjectCheckerTest { @Test public void testInvalidType() { - try { - checker.check(Constants.OBJ_BAD, new byte[0]); - fail("Did not throw CorruptObjectException"); - } catch (CorruptObjectException e) { - final String m = e.getMessage(); - assertEquals(MessageFormat.format( - JGitText.get().corruptObjectInvalidType2, - valueOf(Constants.OBJ_BAD)), m); - } + String msg = MessageFormat.format( + JGitText.get().corruptObjectInvalidType2, + valueOf(OBJ_BAD)); + assertCorrupt(msg, OBJ_BAD, new byte[0]); } @Test @@ -84,13 +96,13 @@ public class ObjectCheckerTest { checker.checkBlob(new byte[0]); checker.checkBlob(new byte[1]); - checker.check(Constants.OBJ_BLOB, new byte[0]); - checker.check(Constants.OBJ_BLOB, new byte[1]); + checker.check(OBJ_BLOB, new byte[0]); + checker.check(OBJ_BLOB, new byte[1]); } @Test public void testValidCommitNoParent() throws CorruptObjectException { - final StringBuilder b = new StringBuilder(); + StringBuilder b = new StringBuilder(); b.append("tree "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); @@ -99,14 +111,14 @@ public class ObjectCheckerTest { b.append("author A. U. Thor 1 +0000\n"); b.append("committer A. U. Thor 1 +0000\n"); - final byte[] data = Constants.encodeASCII(b.toString()); + byte[] data = encodeASCII(b.toString()); checker.checkCommit(data); - checker.check(Constants.OBJ_COMMIT, data); + checker.check(OBJ_COMMIT, data); } @Test public void testValidCommitBlankAuthor() throws CorruptObjectException { - final StringBuilder b = new StringBuilder(); + StringBuilder b = new StringBuilder(); b.append("tree "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); @@ -115,9 +127,9 @@ public class ObjectCheckerTest { b.append("author <> 0 +0000\n"); b.append("committer <> 0 +0000\n"); - final byte[] data = Constants.encodeASCII(b.toString()); + byte[] data = encodeASCII(b.toString()); checker.checkCommit(data); - checker.check(Constants.OBJ_COMMIT, data); + checker.check(OBJ_COMMIT, data); } @Test @@ -127,15 +139,13 @@ public class ObjectCheckerTest { b.append("author b 0 +0000\n"); b.append("committer <> 0 +0000\n"); - byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkCommit(data); - fail("Did not catch corrupt object"); - } catch (CorruptObjectException e) { - assertEquals("invalid author", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + assertCorrupt("bad date", OBJ_COMMIT, data); checker.setAllowInvalidPersonIdent(true); checker.checkCommit(data); + + checker.setAllowInvalidPersonIdent(false); + assertSkipListAccepts(OBJ_COMMIT, data); } @Test @@ -145,20 +155,18 @@ public class ObjectCheckerTest { b.append("author <> 0 +0000\n"); b.append("committer b 0 +0000\n"); - byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkCommit(data); - fail("Did not catch corrupt object"); - } catch (CorruptObjectException e) { - assertEquals("invalid committer", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + assertCorrupt("bad date", OBJ_COMMIT, data); checker.setAllowInvalidPersonIdent(true); checker.checkCommit(data); + + checker.setAllowInvalidPersonIdent(false); + assertSkipListAccepts(OBJ_COMMIT, data); } @Test public void testValidCommit1Parent() throws CorruptObjectException { - final StringBuilder b = new StringBuilder(); + StringBuilder b = new StringBuilder(); b.append("tree "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); @@ -171,14 +179,14 @@ public class ObjectCheckerTest { b.append("author A. U. Thor 1 +0000\n"); b.append("committer A. U. Thor 1 +0000\n"); - final byte[] data = Constants.encodeASCII(b.toString()); + byte[] data = encodeASCII(b.toString()); checker.checkCommit(data); - checker.check(Constants.OBJ_COMMIT, data); + checker.check(OBJ_COMMIT, data); } @Test public void testValidCommit2Parent() throws CorruptObjectException { - final StringBuilder b = new StringBuilder(); + StringBuilder b = new StringBuilder(); b.append("tree "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); @@ -195,14 +203,14 @@ public class ObjectCheckerTest { b.append("author A. U. Thor 1 +0000\n"); b.append("committer A. U. Thor 1 +0000\n"); - final byte[] data = Constants.encodeASCII(b.toString()); + byte[] data = encodeASCII(b.toString()); checker.checkCommit(data); - checker.check(Constants.OBJ_COMMIT, data); + checker.check(OBJ_COMMIT, data); } @Test public void testValidCommit128Parent() throws CorruptObjectException { - final StringBuilder b = new StringBuilder(); + StringBuilder b = new StringBuilder(); b.append("tree "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); @@ -217,15 +225,15 @@ public class ObjectCheckerTest { b.append("author A. U. Thor 1 +0000\n"); b.append("committer A. U. Thor 1 +0000\n"); - final byte[] data = Constants.encodeASCII(b.toString()); + byte[] data = encodeASCII(b.toString()); checker.checkCommit(data); - checker.check(Constants.OBJ_COMMIT, data); + checker.check(OBJ_COMMIT, data); } @Test public void testValidCommitNormalTime() throws CorruptObjectException { - final StringBuilder b = new StringBuilder(); - final String when = "1222757360 -0730"; + StringBuilder b = new StringBuilder(); + String when = "1222757360 -0730"; b.append("tree "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); @@ -234,843 +242,539 @@ public class ObjectCheckerTest { b.append("author A. U. Thor " + when + "\n"); b.append("committer A. U. Thor " + when + "\n"); - final byte[] data = Constants.encodeASCII(b.toString()); + byte[] data = encodeASCII(b.toString()); checker.checkCommit(data); - checker.check(Constants.OBJ_COMMIT, data); + checker.check(OBJ_COMMIT, data); } @Test public void testInvalidCommitNoTree1() { - final StringBuilder b = new StringBuilder(); - + StringBuilder b = new StringBuilder(); b.append("parent "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append('\n'); - - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkCommit(data); - fail("Did not catch corrupt object"); - } catch (CorruptObjectException e) { - assertEquals("no tree header", e.getMessage()); - } + assertCorrupt("no tree header", OBJ_COMMIT, b); } @Test public void testInvalidCommitNoTree2() { - final StringBuilder b = new StringBuilder(); - + StringBuilder b = new StringBuilder(); b.append("trie "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append('\n'); - - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkCommit(data); - fail("Did not catch corrupt object"); - } catch (CorruptObjectException e) { - assertEquals("no tree header", e.getMessage()); - } + assertCorrupt("no tree header", OBJ_COMMIT, b); } @Test public void testInvalidCommitNoTree3() { - final StringBuilder b = new StringBuilder(); - + StringBuilder b = new StringBuilder(); b.append("tree"); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append('\n'); - - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkCommit(data); - fail("Did not catch corrupt object"); - } catch (CorruptObjectException e) { - assertEquals("no tree header", e.getMessage()); - } + assertCorrupt("no tree header", OBJ_COMMIT, b); } @Test public void testInvalidCommitNoTree4() { - final StringBuilder b = new StringBuilder(); - + StringBuilder b = new StringBuilder(); b.append("tree\t"); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append('\n'); - - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkCommit(data); - fail("Did not catch corrupt object"); - } catch (CorruptObjectException e) { - assertEquals("no tree header", e.getMessage()); - } + assertCorrupt("no tree header", OBJ_COMMIT, b); } @Test public void testInvalidCommitInvalidTree1() { - final StringBuilder b = new StringBuilder(); - + StringBuilder b = new StringBuilder(); b.append("tree "); b.append("zzzzfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append('\n'); - - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkCommit(data); - fail("Did not catch corrupt object"); - } catch (CorruptObjectException e) { - assertEquals("invalid tree", e.getMessage()); - } + assertCorrupt("invalid tree", OBJ_COMMIT, b); } @Test public void testInvalidCommitInvalidTree2() { - final StringBuilder b = new StringBuilder(); - + StringBuilder b = new StringBuilder(); b.append("tree "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append("z\n"); - - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkCommit(data); - fail("Did not catch corrupt object"); - } catch (CorruptObjectException e) { - assertEquals("invalid tree", e.getMessage()); - } + assertCorrupt("invalid tree", OBJ_COMMIT, b); } @Test public void testInvalidCommitInvalidTree3() { - final StringBuilder b = new StringBuilder(); - + StringBuilder b = new StringBuilder(); b.append("tree "); b.append("be9b"); b.append("\n"); - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkCommit(data); - fail("Did not catch corrupt object"); - } catch (CorruptObjectException e) { - assertEquals("invalid tree", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + assertCorrupt("invalid tree", OBJ_COMMIT, data); } @Test public void testInvalidCommitInvalidTree4() { - final StringBuilder b = new StringBuilder(); - + StringBuilder b = new StringBuilder(); b.append("tree "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append('\n'); - - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkCommit(data); - fail("Did not catch corrupt object"); - } catch (CorruptObjectException e) { - assertEquals("invalid tree", e.getMessage()); - } + assertCorrupt("invalid tree", OBJ_COMMIT, b); } @Test public void testInvalidCommitInvalidParent1() { - final StringBuilder b = new StringBuilder(); - + StringBuilder b = new StringBuilder(); b.append("tree "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append('\n'); - b.append("parent "); b.append("\n"); - - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkCommit(data); - fail("Did not catch corrupt object"); - } catch (CorruptObjectException e) { - assertEquals("invalid parent", e.getMessage()); - } + assertCorrupt("invalid parent", OBJ_COMMIT, b); } @Test public void testInvalidCommitInvalidParent2() { - final StringBuilder b = new StringBuilder(); - + StringBuilder b = new StringBuilder(); b.append("tree "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append('\n'); - b.append("parent "); b.append("zzzzfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append("\n"); - - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkCommit(data); - fail("Did not catch corrupt object"); - } catch (CorruptObjectException e) { - assertEquals("invalid parent", e.getMessage()); - } + assertCorrupt("invalid parent", OBJ_COMMIT, b); } @Test public void testInvalidCommitInvalidParent3() { - final StringBuilder b = new StringBuilder(); - + StringBuilder b = new StringBuilder(); b.append("tree "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append('\n'); - b.append("parent "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append("\n"); - - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkCommit(data); - fail("Did not catch corrupt object"); - } catch (CorruptObjectException e) { - assertEquals("invalid parent", e.getMessage()); - } + assertCorrupt("invalid parent", OBJ_COMMIT, b); } @Test public void testInvalidCommitInvalidParent4() { - final StringBuilder b = new StringBuilder(); - + StringBuilder b = new StringBuilder(); b.append("tree "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append('\n'); - b.append("parent "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append("z\n"); - - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkCommit(data); - fail("Did not catch corrupt object"); - } catch (CorruptObjectException e) { - assertEquals("invalid parent", e.getMessage()); - } + assertCorrupt("invalid parent", OBJ_COMMIT, b); } @Test public void testInvalidCommitInvalidParent5() { - final StringBuilder b = new StringBuilder(); - + StringBuilder b = new StringBuilder(); b.append("tree "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append('\n'); - b.append("parent\t"); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append("\n"); - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkCommit(data); - fail("Did not catch corrupt object"); - } catch (CorruptObjectException e) { - // Yes, really, we complain about author not being - // found as the invalid parent line wasn't consumed. - assertEquals("no author", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + // Yes, really, we complain about author not being + // found as the invalid parent line wasn't consumed. + assertCorrupt("no author", OBJ_COMMIT, data); } @Test - public void testInvalidCommitNoAuthor() { - final StringBuilder b = new StringBuilder(); - + public void testInvalidCommitNoAuthor() throws CorruptObjectException { + StringBuilder b = new StringBuilder(); b.append("tree "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append('\n'); - b.append("committer A. U. Thor 1 +0000\n"); - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkCommit(data); - fail("Did not catch corrupt object"); - } catch (CorruptObjectException e) { - // Yes, really, we complain about author not being - // found as the invalid parent line wasn't consumed. - assertEquals("no author", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + assertCorrupt("no author", OBJ_COMMIT, data); + assertSkipListAccepts(OBJ_COMMIT, data); } @Test - public void testInvalidCommitNoCommitter1() { - final StringBuilder b = new StringBuilder(); - + public void testInvalidCommitNoCommitter1() throws CorruptObjectException { + StringBuilder b = new StringBuilder(); b.append("tree "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append('\n'); - b.append("author A. U. Thor 1 +0000\n"); - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkCommit(data); - fail("Did not catch corrupt object"); - } catch (CorruptObjectException e) { - // Yes, really, we complain about author not being - // found as the invalid parent line wasn't consumed. - assertEquals("no committer", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + assertCorrupt("no committer", OBJ_COMMIT, data); + assertSkipListAccepts(OBJ_COMMIT, data); } @Test - public void testInvalidCommitNoCommitter2() { - final StringBuilder b = new StringBuilder(); - + public void testInvalidCommitNoCommitter2() throws CorruptObjectException { + StringBuilder b = new StringBuilder(); b.append("tree "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append('\n'); - b.append("author A. U. Thor 1 +0000\n"); b.append("\n"); - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkCommit(data); - fail("Did not catch corrupt object"); - } catch (CorruptObjectException e) { - // Yes, really, we complain about author not being - // found as the invalid parent line wasn't consumed. - assertEquals("no committer", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + assertCorrupt("no committer", OBJ_COMMIT, data); + assertSkipListAccepts(OBJ_COMMIT, data); } @Test - public void testInvalidCommitInvalidAuthor1() { - final StringBuilder b = new StringBuilder(); - + public void testInvalidCommitInvalidAuthor1() + throws CorruptObjectException { + StringBuilder b = new StringBuilder(); b.append("tree "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append('\n'); - b.append("author A. U. Thor 1 +0000\n"); - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkCommit(data); - fail("Did not catch corrupt object"); - } catch (CorruptObjectException e) { - // Yes, really, we complain about author not being - // found as the invalid parent line wasn't consumed. - assertEquals("invalid author", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + assertCorrupt("missing email", OBJ_COMMIT, data); + assertSkipListAccepts(OBJ_COMMIT, data); } @Test - public void testInvalidCommitInvalidAuthor3() { - final StringBuilder b = new StringBuilder(); - + public void testInvalidCommitInvalidAuthor3() + throws CorruptObjectException { + StringBuilder b = new StringBuilder(); b.append("tree "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append('\n'); - b.append("author 1 +0000\n"); - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkCommit(data); - fail("Did not catch corrupt object"); - } catch (CorruptObjectException e) { - // Yes, really, we complain about author not being - // found as the invalid parent line wasn't consumed. - assertEquals("invalid author", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + assertCorrupt("missing email", OBJ_COMMIT, data); + assertSkipListAccepts(OBJ_COMMIT, data); } @Test - public void testInvalidCommitInvalidAuthor4() { - final StringBuilder b = new StringBuilder(); - + public void testInvalidCommitInvalidAuthor4() + throws CorruptObjectException { + StringBuilder b = new StringBuilder(); b.append("tree "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append('\n'); - b.append("author a +0000\n"); - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkCommit(data); - fail("Did not catch corrupt object"); - } catch (CorruptObjectException e) { - // Yes, really, we complain about author not being - // found as the invalid parent line wasn't consumed. - assertEquals("invalid author", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + assertCorrupt("bad date", OBJ_COMMIT, data); + assertSkipListAccepts(OBJ_COMMIT, data); } @Test - public void testInvalidCommitInvalidAuthor5() { - final StringBuilder b = new StringBuilder(); - + public void testInvalidCommitInvalidAuthor5() + throws CorruptObjectException { + StringBuilder b = new StringBuilder(); b.append("tree "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append('\n'); - b.append("author a \n"); - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkCommit(data); - fail("Did not catch corrupt object"); - } catch (CorruptObjectException e) { - // Yes, really, we complain about author not being - // found as the invalid parent line wasn't consumed. - assertEquals("invalid author", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + assertCorrupt("bad date", OBJ_COMMIT, data); + assertSkipListAccepts(OBJ_COMMIT, data); } @Test - public void testInvalidCommitInvalidAuthor6() { - final StringBuilder b = new StringBuilder(); - + public void testInvalidCommitInvalidAuthor6() + throws CorruptObjectException { + StringBuilder b = new StringBuilder(); b.append("tree "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append('\n'); - b.append("author a z"); - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkCommit(data); - fail("Did not catch corrupt object"); - } catch (CorruptObjectException e) { - // Yes, really, we complain about author not being - // found as the invalid parent line wasn't consumed. - assertEquals("invalid author", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + assertCorrupt("bad date", OBJ_COMMIT, data); + assertSkipListAccepts(OBJ_COMMIT, data); } @Test - public void testInvalidCommitInvalidAuthor7() { - final StringBuilder b = new StringBuilder(); - + public void testInvalidCommitInvalidAuthor7() + throws CorruptObjectException { + StringBuilder b = new StringBuilder(); b.append("tree "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append('\n'); - b.append("author a 1 z"); - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkCommit(data); - fail("Did not catch corrupt object"); - } catch (CorruptObjectException e) { - // Yes, really, we complain about author not being - // found as the invalid parent line wasn't consumed. - assertEquals("invalid author", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + assertCorrupt("bad time zone", OBJ_COMMIT, data); + assertSkipListAccepts(OBJ_COMMIT, data); } @Test - public void testInvalidCommitInvalidCommitter() { - final StringBuilder b = new StringBuilder(); - + public void testInvalidCommitInvalidCommitter() + throws CorruptObjectException { + StringBuilder b = new StringBuilder(); b.append("tree "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append('\n'); - b.append("author a 1 +0000\n"); b.append("committer a <"); - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkCommit(data); - fail("Did not catch corrupt object"); - } catch (CorruptObjectException e) { - // Yes, really, we complain about author not being - // found as the invalid parent line wasn't consumed. - assertEquals("invalid committer", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + assertCorrupt("bad email", OBJ_COMMIT, data); + assertSkipListAccepts(OBJ_COMMIT, data); } @Test public void testValidTag() throws CorruptObjectException { - final StringBuilder b = new StringBuilder(); - + StringBuilder b = new StringBuilder(); b.append("object "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append('\n'); - b.append("type commit\n"); b.append("tag test-tag\n"); b.append("tagger A. U. Thor 1 +0000\n"); - final byte[] data = Constants.encodeASCII(b.toString()); + byte[] data = encodeASCII(b.toString()); checker.checkTag(data); - checker.check(Constants.OBJ_TAG, data); + checker.check(OBJ_TAG, data); } @Test public void testInvalidTagNoObject1() { - final StringBuilder b = new StringBuilder(); - - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTag(data); - fail("incorrectly accepted invalid tag"); - } catch (CorruptObjectException e) { - assertEquals("no object header", e.getMessage()); - } + assertCorrupt("no object header", OBJ_TAG, new byte[0]); } @Test public void testInvalidTagNoObject2() { - final StringBuilder b = new StringBuilder(); - + StringBuilder b = new StringBuilder(); b.append("object\t"); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append('\n'); - - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTag(data); - fail("incorrectly accepted invalid tag"); - } catch (CorruptObjectException e) { - assertEquals("no object header", e.getMessage()); - } + assertCorrupt("no object header", OBJ_TAG, b); } @Test public void testInvalidTagNoObject3() { - final StringBuilder b = new StringBuilder(); - + StringBuilder b = new StringBuilder(); b.append("obejct "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append('\n'); - - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTag(data); - fail("incorrectly accepted invalid tag"); - } catch (CorruptObjectException e) { - assertEquals("no object header", e.getMessage()); - } + assertCorrupt("no object header", OBJ_TAG, b); } @Test public void testInvalidTagNoObject4() { - final StringBuilder b = new StringBuilder(); - + StringBuilder b = new StringBuilder(); b.append("object "); b.append("zz9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append('\n'); - - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTag(data); - fail("incorrectly accepted invalid tag"); - } catch (CorruptObjectException e) { - assertEquals("invalid object", e.getMessage()); - } + assertCorrupt("invalid object", OBJ_TAG, b); } @Test public void testInvalidTagNoObject5() { - final StringBuilder b = new StringBuilder(); - + StringBuilder b = new StringBuilder(); b.append("object "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append(" \n"); - - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTag(data); - fail("incorrectly accepted invalid tag"); - } catch (CorruptObjectException e) { - assertEquals("invalid object", e.getMessage()); - } + assertCorrupt("invalid object", OBJ_TAG, b); } @Test public void testInvalidTagNoObject6() { - final StringBuilder b = new StringBuilder(); - + StringBuilder b = new StringBuilder(); b.append("object "); b.append("be9"); - - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTag(data); - fail("incorrectly accepted invalid tag"); - } catch (CorruptObjectException e) { - assertEquals("invalid object", e.getMessage()); - } + assertCorrupt("invalid object", OBJ_TAG, b); } @Test public void testInvalidTagNoType1() { - final StringBuilder b = new StringBuilder(); - + StringBuilder b = new StringBuilder(); b.append("object "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append('\n'); - - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTag(data); - fail("incorrectly accepted invalid tag"); - } catch (CorruptObjectException e) { - assertEquals("no type header", e.getMessage()); - } + assertCorrupt("no type header", OBJ_TAG, b); } @Test public void testInvalidTagNoType2() { - final StringBuilder b = new StringBuilder(); - + StringBuilder b = new StringBuilder(); b.append("object "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append('\n'); - b.append("type\tcommit\n"); - - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTag(data); - fail("incorrectly accepted invalid tag"); - } catch (CorruptObjectException e) { - assertEquals("no type header", e.getMessage()); - } + assertCorrupt("no type header", OBJ_TAG, b); } @Test public void testInvalidTagNoType3() { - final StringBuilder b = new StringBuilder(); - + StringBuilder b = new StringBuilder(); b.append("object "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append('\n'); - b.append("tpye commit\n"); - - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTag(data); - fail("incorrectly accepted invalid tag"); - } catch (CorruptObjectException e) { - assertEquals("no type header", e.getMessage()); - } + assertCorrupt("no type header", OBJ_TAG, b); } @Test public void testInvalidTagNoType4() { - final StringBuilder b = new StringBuilder(); - + StringBuilder b = new StringBuilder(); b.append("object "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append('\n'); - b.append("type commit"); - - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTag(data); - fail("incorrectly accepted invalid tag"); - } catch (CorruptObjectException e) { - assertEquals("no tag header", e.getMessage()); - } + assertCorrupt("no tag header", OBJ_TAG, b); } @Test public void testInvalidTagNoTagHeader1() { - final StringBuilder b = new StringBuilder(); - + StringBuilder b = new StringBuilder(); b.append("object "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append('\n'); - b.append("type commit\n"); - - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTag(data); - fail("incorrectly accepted invalid tag"); - } catch (CorruptObjectException e) { - assertEquals("no tag header", e.getMessage()); - } + assertCorrupt("no tag header", OBJ_TAG, b); } @Test public void testInvalidTagNoTagHeader2() { - final StringBuilder b = new StringBuilder(); - + StringBuilder b = new StringBuilder(); b.append("object "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append('\n'); - b.append("type commit\n"); b.append("tag\tfoo\n"); - - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTag(data); - fail("incorrectly accepted invalid tag"); - } catch (CorruptObjectException e) { - assertEquals("no tag header", e.getMessage()); - } + assertCorrupt("no tag header", OBJ_TAG, b); } @Test public void testInvalidTagNoTagHeader3() { - final StringBuilder b = new StringBuilder(); - + StringBuilder b = new StringBuilder(); b.append("object "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append('\n'); - b.append("type commit\n"); b.append("tga foo\n"); - - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTag(data); - fail("incorrectly accepted invalid tag"); - } catch (CorruptObjectException e) { - assertEquals("no tag header", e.getMessage()); - } + assertCorrupt("no tag header", OBJ_TAG, b); } @Test public void testValidTagHasNoTaggerHeader() throws CorruptObjectException { - final StringBuilder b = new StringBuilder(); - + StringBuilder b = new StringBuilder(); b.append("object "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append('\n'); - b.append("type commit\n"); b.append("tag foo\n"); - - checker.checkTag(Constants.encodeASCII(b.toString())); + checker.checkTag(encodeASCII(b.toString())); } @Test public void testInvalidTagInvalidTaggerHeader1() throws CorruptObjectException { - final StringBuilder b = new StringBuilder(); - + StringBuilder b = new StringBuilder(); b.append("object "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append('\n'); - b.append("type commit\n"); b.append("tag foo\n"); b.append("tagger \n"); - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTag(data); - fail("incorrectly accepted invalid tag"); - } catch (CorruptObjectException e) { - assertEquals("invalid tagger", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + assertCorrupt("missing email", OBJ_TAG, data); checker.setAllowInvalidPersonIdent(true); checker.checkTag(data); + + checker.setAllowInvalidPersonIdent(false); + assertSkipListAccepts(OBJ_TAG, data); } @Test - public void testInvalidTagInvalidTaggerHeader3() { - final StringBuilder b = new StringBuilder(); - + public void testInvalidTagInvalidTaggerHeader3() + throws CorruptObjectException { + StringBuilder b = new StringBuilder(); b.append("object "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append('\n'); - b.append("type commit\n"); b.append("tag foo\n"); b.append("tagger a < 1 +000\n"); - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTag(data); - fail("incorrectly accepted invalid tag"); - } catch (CorruptObjectException e) { - assertEquals("invalid tagger", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + assertCorrupt("bad email", OBJ_TAG, data); + assertSkipListAccepts(OBJ_TAG, data); } @Test public void testValidEmptyTree() throws CorruptObjectException { checker.checkTree(new byte[0]); - checker.check(Constants.OBJ_TREE, new byte[0]); + checker.check(OBJ_TREE, new byte[0]); } @Test public void testValidTree1() throws CorruptObjectException { - final StringBuilder b = new StringBuilder(); + StringBuilder b = new StringBuilder(); entry(b, "100644 regular-file"); - final byte[] data = Constants.encodeASCII(b.toString()); - checker.checkTree(data); + checker.checkTree(encodeASCII(b.toString())); } @Test public void testValidTree2() throws CorruptObjectException { - final StringBuilder b = new StringBuilder(); + StringBuilder b = new StringBuilder(); entry(b, "100755 executable"); - final byte[] data = Constants.encodeASCII(b.toString()); - checker.checkTree(data); + checker.checkTree(encodeASCII(b.toString())); } @Test public void testValidTree3() throws CorruptObjectException { - final StringBuilder b = new StringBuilder(); + StringBuilder b = new StringBuilder(); entry(b, "40000 tree"); - final byte[] data = Constants.encodeASCII(b.toString()); - checker.checkTree(data); + checker.checkTree(encodeASCII(b.toString())); } @Test public void testValidTree4() throws CorruptObjectException { - final StringBuilder b = new StringBuilder(); + StringBuilder b = new StringBuilder(); entry(b, "120000 symlink"); - final byte[] data = Constants.encodeASCII(b.toString()); - checker.checkTree(data); + checker.checkTree(encodeASCII(b.toString())); } @Test public void testValidTree5() throws CorruptObjectException { - final StringBuilder b = new StringBuilder(); + StringBuilder b = new StringBuilder(); entry(b, "160000 git link"); - final byte[] data = Constants.encodeASCII(b.toString()); - checker.checkTree(data); + checker.checkTree(encodeASCII(b.toString())); + } + + @Test + public void testValidTree6() throws CorruptObjectException { + StringBuilder b = new StringBuilder(); + entry(b, "100644 .a"); + checker.checkTree(encodeASCII(b.toString())); } @Test - public void testValidTree6() throws CorruptObjectException { - final StringBuilder b = new StringBuilder(); - entry(b, "100644 .a"); - final byte[] data = Constants.encodeASCII(b.toString()); + public void testNullSha1InTreeEntry() throws CorruptObjectException { + byte[] data = concat( + encodeASCII("100644 A"), new byte[] { '\0' }, + new byte[OBJECT_ID_LENGTH]); + assertCorrupt("entry points to null SHA-1", OBJ_TREE, data); + assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(NULL_SHA1, true); checker.checkTree(data); } @@ -1084,357 +788,326 @@ public class ObjectCheckerTest { @Test public void testValidTreeSorting1() throws CorruptObjectException { - final StringBuilder b = new StringBuilder(); + StringBuilder b = new StringBuilder(); entry(b, "100644 fooaaa"); entry(b, "100755 foobar"); - final byte[] data = Constants.encodeASCII(b.toString()); - checker.checkTree(data); + checker.checkTree(encodeASCII(b.toString())); } @Test public void testValidTreeSorting2() throws CorruptObjectException { - final StringBuilder b = new StringBuilder(); + StringBuilder b = new StringBuilder(); entry(b, "100755 fooaaa"); entry(b, "100644 foobar"); - final byte[] data = Constants.encodeASCII(b.toString()); - checker.checkTree(data); + checker.checkTree(encodeASCII(b.toString())); } @Test public void testValidTreeSorting3() throws CorruptObjectException { - final StringBuilder b = new StringBuilder(); + StringBuilder b = new StringBuilder(); entry(b, "40000 a"); entry(b, "100644 b"); - final byte[] data = Constants.encodeASCII(b.toString()); - checker.checkTree(data); + checker.checkTree(encodeASCII(b.toString())); } @Test public void testValidTreeSorting4() throws CorruptObjectException { - final StringBuilder b = new StringBuilder(); + StringBuilder b = new StringBuilder(); entry(b, "100644 a"); entry(b, "40000 b"); - final byte[] data = Constants.encodeASCII(b.toString()); - checker.checkTree(data); + checker.checkTree(encodeASCII(b.toString())); } @Test public void testValidTreeSorting5() throws CorruptObjectException { - final StringBuilder b = new StringBuilder(); + StringBuilder b = new StringBuilder(); entry(b, "100644 a.c"); entry(b, "40000 a"); entry(b, "100644 a0c"); - final byte[] data = Constants.encodeASCII(b.toString()); - checker.checkTree(data); + checker.checkTree(encodeASCII(b.toString())); } @Test public void testValidTreeSorting6() throws CorruptObjectException { - final StringBuilder b = new StringBuilder(); + StringBuilder b = new StringBuilder(); entry(b, "40000 a"); entry(b, "100644 apple"); - final byte[] data = Constants.encodeASCII(b.toString()); - checker.checkTree(data); + checker.checkTree(encodeASCII(b.toString())); } @Test public void testValidTreeSorting7() throws CorruptObjectException { - final StringBuilder b = new StringBuilder(); + StringBuilder b = new StringBuilder(); entry(b, "40000 an orang"); entry(b, "40000 an orange"); - final byte[] data = Constants.encodeASCII(b.toString()); - checker.checkTree(data); + checker.checkTree(encodeASCII(b.toString())); } @Test public void testValidTreeSorting8() throws CorruptObjectException { - final StringBuilder b = new StringBuilder(); + StringBuilder b = new StringBuilder(); entry(b, "100644 a"); entry(b, "100644 a0c"); entry(b, "100644 b"); - final byte[] data = Constants.encodeASCII(b.toString()); - checker.checkTree(data); + checker.checkTree(encodeASCII(b.toString())); } @Test public void testAcceptTreeModeWithZero() throws CorruptObjectException { StringBuilder b = new StringBuilder(); entry(b, "040000 a"); + byte[] data = encodeASCII(b.toString()); checker.setAllowLeadingZeroFileMode(true); - checker.checkTree(Constants.encodeASCII(b.toString())); + checker.checkTree(data); + + checker.setAllowLeadingZeroFileMode(false); + assertSkipListAccepts(OBJ_TREE, data); + + checker.setIgnore(ZERO_PADDED_FILEMODE, true); + checker.checkTree(data); } @Test public void testInvalidTreeModeStartsWithZero1() { - final StringBuilder b = new StringBuilder(); + StringBuilder b = new StringBuilder(); entry(b, "0 a"); - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTree(data); - fail("incorrectly accepted an invalid tree"); - } catch (CorruptObjectException e) { - assertEquals("mode starts with '0'", e.getMessage()); - } + assertCorrupt("mode starts with '0'", OBJ_TREE, b); } @Test public void testInvalidTreeModeStartsWithZero2() { - final StringBuilder b = new StringBuilder(); + StringBuilder b = new StringBuilder(); entry(b, "0100644 a"); - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTree(data); - fail("incorrectly accepted an invalid tree"); - } catch (CorruptObjectException e) { - assertEquals("mode starts with '0'", e.getMessage()); - } + assertCorrupt("mode starts with '0'", OBJ_TREE, b); } @Test public void testInvalidTreeModeStartsWithZero3() { - final StringBuilder b = new StringBuilder(); + StringBuilder b = new StringBuilder(); entry(b, "040000 a"); - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTree(data); - fail("incorrectly accepted an invalid tree"); - } catch (CorruptObjectException e) { - assertEquals("mode starts with '0'", e.getMessage()); - } + assertCorrupt("mode starts with '0'", OBJ_TREE, b); } @Test public void testInvalidTreeModeNotOctal1() { - final StringBuilder b = new StringBuilder(); + StringBuilder b = new StringBuilder(); entry(b, "8 a"); - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTree(data); - fail("incorrectly accepted an invalid tree"); - } catch (CorruptObjectException e) { - assertEquals("invalid mode character", e.getMessage()); - } + assertCorrupt("invalid mode character", OBJ_TREE, b); } @Test public void testInvalidTreeModeNotOctal2() { - final StringBuilder b = new StringBuilder(); + StringBuilder b = new StringBuilder(); entry(b, "Z a"); - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTree(data); - fail("incorrectly accepted an invalid tree"); - } catch (CorruptObjectException e) { - assertEquals("invalid mode character", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + assertCorrupt("invalid mode character", OBJ_TREE, data); + assertSkipListRejects("invalid mode character", OBJ_TREE, data); } @Test public void testInvalidTreeModeNotSupportedMode1() { - final StringBuilder b = new StringBuilder(); + StringBuilder b = new StringBuilder(); entry(b, "1 a"); - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTree(data); - fail("incorrectly accepted an invalid tree"); - } catch (CorruptObjectException e) { - assertEquals("invalid mode 1", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + assertCorrupt("invalid mode 1", OBJ_TREE, data); + assertSkipListRejects("invalid mode 1", OBJ_TREE, data); } @Test public void testInvalidTreeModeNotSupportedMode2() { - final StringBuilder b = new StringBuilder(); + StringBuilder b = new StringBuilder(); entry(b, "170000 a"); - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTree(data); - fail("incorrectly accepted an invalid tree"); - } catch (CorruptObjectException e) { - assertEquals("invalid mode " + 0170000, e.getMessage()); - } + assertCorrupt("invalid mode " + 0170000, OBJ_TREE, b); } @Test public void testInvalidTreeModeMissingName() { - final StringBuilder b = new StringBuilder(); + StringBuilder b = new StringBuilder(); b.append("100644"); - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTree(data); - fail("incorrectly accepted an invalid tree"); - } catch (CorruptObjectException e) { - assertEquals("truncated in mode", e.getMessage()); - } + assertCorrupt("truncated in mode", OBJ_TREE, b); } @Test - public void testInvalidTreeNameContainsSlash() { - final StringBuilder b = new StringBuilder(); + public void testInvalidTreeNameContainsSlash() + throws CorruptObjectException { + StringBuilder b = new StringBuilder(); entry(b, "100644 a/b"); - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTree(data); - fail("incorrectly accepted an invalid tree"); - } catch (CorruptObjectException e) { - assertEquals("name contains '/'", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + assertCorrupt("name contains '/'", OBJ_TREE, data); + assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(FULL_PATHNAME, true); + checker.checkTree(data); } @Test - public void testInvalidTreeNameIsEmpty() { - final StringBuilder b = new StringBuilder(); + public void testInvalidTreeNameIsEmpty() throws CorruptObjectException { + StringBuilder b = new StringBuilder(); entry(b, "100644 "); - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTree(data); - fail("incorrectly accepted an invalid tree"); - } catch (CorruptObjectException e) { - assertEquals("zero length name", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + assertCorrupt("zero length name", OBJ_TREE, data); + assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(EMPTY_NAME, true); + checker.checkTree(data); } @Test - public void testInvalidTreeNameIsDot() { - final StringBuilder b = new StringBuilder(); + public void testInvalidTreeNameIsDot() throws CorruptObjectException { + StringBuilder b = new StringBuilder(); entry(b, "100644 ."); - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTree(data); - fail("incorrectly accepted an invalid tree"); - } catch (CorruptObjectException e) { - assertEquals("invalid name '.'", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + assertCorrupt("invalid name '.'", OBJ_TREE, data); + assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(HAS_DOT, true); + checker.checkTree(data); } @Test - public void testInvalidTreeNameIsDotDot() { - final StringBuilder b = new StringBuilder(); + public void testInvalidTreeNameIsDotDot() throws CorruptObjectException { + StringBuilder b = new StringBuilder(); entry(b, "100644 .."); - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTree(data); - fail("incorrectly accepted an invalid tree"); - } catch (CorruptObjectException e) { - assertEquals("invalid name '..'", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + assertCorrupt("invalid name '..'", OBJ_TREE, data); + assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(HAS_DOTDOT, true); + checker.checkTree(data); } @Test - public void testInvalidTreeNameIsGit() { + public void testInvalidTreeNameIsGit() throws CorruptObjectException { StringBuilder b = new StringBuilder(); entry(b, "100644 .git"); - byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTree(data); - fail("incorrectly accepted an invalid tree"); - } catch (CorruptObjectException e) { - assertEquals("invalid name '.git'", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + assertCorrupt("invalid name '.git'", OBJ_TREE, data); + assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(HAS_DOTGIT, true); + checker.checkTree(data); } @Test - public void testInvalidTreeNameIsMixedCaseGit() { + public void testInvalidTreeNameIsMixedCaseGit() + throws CorruptObjectException { StringBuilder b = new StringBuilder(); entry(b, "100644 .GiT"); - byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTree(data); - fail("incorrectly accepted an invalid tree"); - } catch (CorruptObjectException e) { - assertEquals("invalid name '.GiT'", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + assertCorrupt("invalid name '.GiT'", OBJ_TREE, data); + assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(HAS_DOTGIT, true); + checker.checkTree(data); } @Test - public void testInvalidTreeNameIsMacHFSGit() { + public void testInvalidTreeNameIsMacHFSGit() throws CorruptObjectException { StringBuilder b = new StringBuilder(); entry(b, "100644 .gi\u200Ct"); - byte[] data = Constants.encode(b.toString()); - try { - checker.setSafeForMacOS(true); - checker.checkTree(data); - fail("incorrectly accepted an invalid tree"); - } catch (CorruptObjectException e) { - assertEquals( - "invalid name '.gi\u200Ct' contains ignorable Unicode characters", - e.getMessage()); - } + byte[] data = encode(b.toString()); + + // Fine on POSIX. + checker.checkTree(data); + + // Rejected on Mac OS. + checker.setSafeForMacOS(true); + assertCorrupt( + "invalid name '.gi\u200Ct' contains ignorable Unicode characters", + OBJ_TREE, data); + assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(HAS_DOTGIT, true); + checker.checkTree(data); } @Test - public void testInvalidTreeNameIsMacHFSGit2() { + public void testInvalidTreeNameIsMacHFSGit2() + throws CorruptObjectException { StringBuilder b = new StringBuilder(); entry(b, "100644 \u206B.git"); - byte[] data = Constants.encode(b.toString()); - try { - checker.setSafeForMacOS(true); - checker.checkTree(data); - fail("incorrectly accepted an invalid tree"); - } catch (CorruptObjectException e) { - assertEquals( - "invalid name '\u206B.git' contains ignorable Unicode characters", - e.getMessage()); - } + byte[] data = encode(b.toString()); + + // Fine on POSIX. + checker.checkTree(data); + + // Rejected on Mac OS. + checker.setSafeForMacOS(true); + assertCorrupt( + "invalid name '\u206B.git' contains ignorable Unicode characters", + OBJ_TREE, data); + assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(HAS_DOTGIT, true); + checker.checkTree(data); } @Test - public void testInvalidTreeNameIsMacHFSGit3() { + public void testInvalidTreeNameIsMacHFSGit3() + throws CorruptObjectException { StringBuilder b = new StringBuilder(); entry(b, "100644 .git\uFEFF"); - byte[] data = Constants.encode(b.toString()); - try { - checker.setSafeForMacOS(true); - checker.checkTree(data); - fail("incorrectly accepted an invalid tree"); - } catch (CorruptObjectException e) { - assertEquals( - "invalid name '.git\uFEFF' contains ignorable Unicode characters", - e.getMessage()); - } + byte[] data = encode(b.toString()); + + // Fine on POSIX. + checker.checkTree(data); + + // Rejected on Mac OS. + checker.setSafeForMacOS(true); + assertCorrupt( + "invalid name '.git\uFEFF' contains ignorable Unicode characters", + OBJ_TREE, data); + assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(HAS_DOTGIT, true); + checker.checkTree(data); } - private static byte[] concat(byte[] b1, byte[] b2) { - byte[] data = new byte[b1.length + b2.length]; - System.arraycopy(b1, 0, data, 0, b1.length); - System.arraycopy(b2, 0, data, b1.length, b2.length); + private static byte[] concat(byte[]... b) { + int n = 0; + for (byte[] a : b) { + n += a.length; + } + + byte[] data = new byte[n]; + n = 0; + for (byte[] a : b) { + System.arraycopy(a, 0, data, n, a.length); + n += a.length; + } return data; } @Test - public void testInvalidTreeNameIsMacHFSGitCorruptUTF8AtEnd() { - byte[] data = concat(Constants.encode("100644 .git"), + public void testInvalidTreeNameIsMacHFSGitCorruptUTF8AtEnd() + throws CorruptObjectException { + byte[] data = concat(encode("100644 .git"), new byte[] { (byte) 0xef }); StringBuilder b = new StringBuilder(); entry(b, ""); - data = concat(data, Constants.encode(b.toString())); - try { - checker.setSafeForMacOS(true); - checker.checkTree(data); - fail("incorrectly accepted an invalid tree"); - } catch (CorruptObjectException e) { - assertEquals( - "invalid name contains byte sequence '0xef' which is not a valid UTF-8 character", - e.getMessage()); - } + data = concat(data, encode(b.toString())); + + // Fine on POSIX. + checker.checkTree(data); + + // Rejected on Mac OS. + checker.setSafeForMacOS(true); + assertCorrupt( + "invalid name contains byte sequence '0xef' which is not a valid UTF-8 character", + OBJ_TREE, data); + assertSkipListAccepts(OBJ_TREE, data); } @Test - public void testInvalidTreeNameIsMacHFSGitCorruptUTF8AtEnd2() { - byte[] data = concat(Constants.encode("100644 .git"), new byte[] { + public void testInvalidTreeNameIsMacHFSGitCorruptUTF8AtEnd2() + throws CorruptObjectException { + byte[] data = concat(encode("100644 .git"), + new byte[] { (byte) 0xe2, (byte) 0xab }); StringBuilder b = new StringBuilder(); entry(b, ""); - data = concat(data, Constants.encode(b.toString())); - try { - checker.setSafeForMacOS(true); - checker.checkTree(data); - fail("incorrectly accepted an invalid tree"); - } catch (CorruptObjectException e) { - assertEquals( - "invalid name contains byte sequence '0xe2ab' which is not a valid UTF-8 character", - e.getMessage()); - } + data = concat(data, encode(b.toString())); + + // Fine on POSIX. + checker.checkTree(data); + + // Rejected on Mac OS. + checker.setSafeForMacOS(true); + assertCorrupt( + "invalid name contains byte sequence '0xe2ab' which is not a valid UTF-8 character", + OBJ_TREE, data); + assertSkipListAccepts(OBJ_TREE, data); } @Test @@ -1442,7 +1115,7 @@ public class ObjectCheckerTest { throws CorruptObjectException { StringBuilder b = new StringBuilder(); entry(b, "100644 .git\u200Cx"); - byte[] data = Constants.encode(b.toString()); + byte[] data = encode(b.toString()); checker.setSafeForMacOS(true); checker.checkTree(data); } @@ -1452,7 +1125,7 @@ public class ObjectCheckerTest { throws CorruptObjectException { StringBuilder b = new StringBuilder(); entry(b, "100644 .kit\u200C"); - byte[] data = Constants.encode(b.toString()); + byte[] data = encode(b.toString()); checker.setSafeForMacOS(true); checker.checkTree(data); } @@ -1462,21 +1135,19 @@ public class ObjectCheckerTest { throws CorruptObjectException { StringBuilder b = new StringBuilder(); entry(b, "100644 .git\u200C"); - byte[] data = Constants.encode(b.toString()); + byte[] data = encode(b.toString()); checker.checkTree(data); } @Test - public void testInvalidTreeNameIsDotGitDot() { + public void testInvalidTreeNameIsDotGitDot() throws CorruptObjectException { StringBuilder b = new StringBuilder(); entry(b, "100644 .git."); - byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTree(data); - fail("incorrectly accepted an invalid tree"); - } catch (CorruptObjectException e) { - assertEquals("invalid name '.git.'", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + assertCorrupt("invalid name '.git.'", OBJ_TREE, data); + assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(HAS_DOTGIT, true); + checker.checkTree(data); } @Test @@ -1484,20 +1155,19 @@ public class ObjectCheckerTest { throws CorruptObjectException { StringBuilder b = new StringBuilder(); entry(b, "100644 .git.."); - checker.checkTree(Constants.encodeASCII(b.toString())); + checker.checkTree(encodeASCII(b.toString())); } @Test - public void testInvalidTreeNameIsDotGitSpace() { + public void testInvalidTreeNameIsDotGitSpace() + throws CorruptObjectException { StringBuilder b = new StringBuilder(); entry(b, "100644 .git "); - byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTree(data); - fail("incorrectly accepted an invalid tree"); - } catch (CorruptObjectException e) { - assertEquals("invalid name '.git '", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + assertCorrupt("invalid name '.git '", OBJ_TREE, data); + assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(HAS_DOTGIT, true); + checker.checkTree(data); } @Test @@ -1505,7 +1175,7 @@ public class ObjectCheckerTest { throws CorruptObjectException { StringBuilder b = new StringBuilder(); entry(b, "100644 .gitfoobar"); - byte[] data = Constants.encodeASCII(b.toString()); + byte[] data = encodeASCII(b.toString()); checker.checkTree(data); } @@ -1514,7 +1184,7 @@ public class ObjectCheckerTest { throws CorruptObjectException { StringBuilder b = new StringBuilder(); entry(b, "100644 .gitfoo bar"); - byte[] data = Constants.encodeASCII(b.toString()); + byte[] data = encodeASCII(b.toString()); checker.checkTree(data); } @@ -1523,7 +1193,7 @@ public class ObjectCheckerTest { throws CorruptObjectException { StringBuilder b = new StringBuilder(); entry(b, "100644 .gitfoobar."); - byte[] data = Constants.encodeASCII(b.toString()); + byte[] data = encodeASCII(b.toString()); checker.checkTree(data); } @@ -1532,251 +1202,236 @@ public class ObjectCheckerTest { throws CorruptObjectException { StringBuilder b = new StringBuilder(); entry(b, "100644 .gitfoobar.."); - byte[] data = Constants.encodeASCII(b.toString()); + byte[] data = encodeASCII(b.toString()); checker.checkTree(data); } @Test - public void testInvalidTreeNameIsDotGitDotSpace() { + public void testInvalidTreeNameIsDotGitDotSpace() + throws CorruptObjectException { StringBuilder b = new StringBuilder(); entry(b, "100644 .git. "); - byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTree(data); - fail("incorrectly accepted an invalid tree"); - } catch (CorruptObjectException e) { - assertEquals("invalid name '.git. '", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + assertCorrupt("invalid name '.git. '", OBJ_TREE, data); + assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(HAS_DOTGIT, true); + checker.checkTree(data); } @Test - public void testInvalidTreeNameIsDotGitSpaceDot() { + public void testInvalidTreeNameIsDotGitSpaceDot() + throws CorruptObjectException { StringBuilder b = new StringBuilder(); entry(b, "100644 .git . "); - byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTree(data); - fail("incorrectly accepted an invalid tree"); - } catch (CorruptObjectException e) { - assertEquals("invalid name '.git . '", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + assertCorrupt("invalid name '.git . '", OBJ_TREE, data); + assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(HAS_DOTGIT, true); + checker.checkTree(data); } @Test - public void testInvalidTreeNameIsGITTilde1() { + public void testInvalidTreeNameIsGITTilde1() throws CorruptObjectException { StringBuilder b = new StringBuilder(); entry(b, "100644 GIT~1"); - byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTree(data); - fail("incorrectly accepted an invalid tree"); - } catch (CorruptObjectException e) { - assertEquals("invalid name 'GIT~1'", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + assertCorrupt("invalid name 'GIT~1'", OBJ_TREE, data); + assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(HAS_DOTGIT, true); + checker.checkTree(data); } @Test - public void testInvalidTreeNameIsGiTTilde1() { + public void testInvalidTreeNameIsGiTTilde1() throws CorruptObjectException { StringBuilder b = new StringBuilder(); entry(b, "100644 GiT~1"); - byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTree(data); - fail("incorrectly accepted an invalid tree"); - } catch (CorruptObjectException e) { - assertEquals("invalid name 'GiT~1'", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + assertCorrupt("invalid name 'GiT~1'", OBJ_TREE, data); + assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(HAS_DOTGIT, true); + checker.checkTree(data); } @Test public void testValidTreeNameIsGitTilde11() throws CorruptObjectException { StringBuilder b = new StringBuilder(); entry(b, "100644 GIT~11"); - byte[] data = Constants.encodeASCII(b.toString()); + byte[] data = encodeASCII(b.toString()); checker.checkTree(data); } @Test public void testInvalidTreeTruncatedInName() { - final StringBuilder b = new StringBuilder(); + StringBuilder b = new StringBuilder(); b.append("100644 b"); - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTree(data); - fail("incorrectly accepted an invalid tree"); - } catch (CorruptObjectException e) { - assertEquals("truncated in name", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + assertCorrupt("truncated in name", OBJ_TREE, data); + assertSkipListRejects("truncated in name", OBJ_TREE, data); } @Test public void testInvalidTreeTruncatedInObjectId() { - final StringBuilder b = new StringBuilder(); + StringBuilder b = new StringBuilder(); b.append("100644 b\0\1\2"); - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTree(data); - fail("incorrectly accepted an invalid tree"); - } catch (CorruptObjectException e) { - assertEquals("truncated in object id", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + assertCorrupt("truncated in object id", OBJ_TREE, data); + assertSkipListRejects("truncated in object id", OBJ_TREE, data); } @Test - public void testInvalidTreeBadSorting1() { - final StringBuilder b = new StringBuilder(); + public void testInvalidTreeBadSorting1() throws CorruptObjectException { + StringBuilder b = new StringBuilder(); entry(b, "100644 foobar"); entry(b, "100644 fooaaa"); - final byte[] data = Constants.encodeASCII(b.toString()); + byte[] data = encodeASCII(b.toString()); + + assertCorrupt("incorrectly sorted", OBJ_TREE, data); + + ObjectId id = idFor(OBJ_TREE, data); try { - checker.checkTree(data); - fail("incorrectly accepted an invalid tree"); + checker.check(id, OBJ_TREE, data); + fail("Did not throw CorruptObjectException"); } catch (CorruptObjectException e) { - assertEquals("incorrectly sorted", e.getMessage()); + assertSame(TREE_NOT_SORTED, e.getErrorType()); + assertEquals("treeNotSorted: object " + id.name() + + ": incorrectly sorted", e.getMessage()); } + + assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(TREE_NOT_SORTED, true); + checker.checkTree(data); } @Test - public void testInvalidTreeBadSorting2() { - final StringBuilder b = new StringBuilder(); + public void testInvalidTreeBadSorting2() throws CorruptObjectException { + StringBuilder b = new StringBuilder(); entry(b, "40000 a"); entry(b, "100644 a.c"); - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTree(data); - fail("incorrectly accepted an invalid tree"); - } catch (CorruptObjectException e) { - assertEquals("incorrectly sorted", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + assertCorrupt("incorrectly sorted", OBJ_TREE, data); + assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(TREE_NOT_SORTED, true); + checker.checkTree(data); } @Test - public void testInvalidTreeBadSorting3() { - final StringBuilder b = new StringBuilder(); + public void testInvalidTreeBadSorting3() throws CorruptObjectException { + StringBuilder b = new StringBuilder(); entry(b, "100644 a0c"); entry(b, "40000 a"); - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTree(data); - fail("incorrectly accepted an invalid tree"); - } catch (CorruptObjectException e) { - assertEquals("incorrectly sorted", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + assertCorrupt("incorrectly sorted", OBJ_TREE, data); + assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(TREE_NOT_SORTED, true); + checker.checkTree(data); } @Test - public void testInvalidTreeDuplicateNames1() { - final StringBuilder b = new StringBuilder(); + public void testInvalidTreeDuplicateNames1_File() + throws CorruptObjectException { + StringBuilder b = new StringBuilder(); entry(b, "100644 a"); entry(b, "100644 a"); - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTree(data); - fail("incorrectly accepted an invalid tree"); - } catch (CorruptObjectException e) { - assertEquals("duplicate entry names", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + assertCorrupt("duplicate entry names", OBJ_TREE, data); + assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(DUPLICATE_ENTRIES, true); + checker.checkTree(data); + } + + @Test + public void testInvalidTreeDuplicateNames1_Tree() + throws CorruptObjectException { + StringBuilder b = new StringBuilder(); + entry(b, "40000 a"); + entry(b, "40000 a"); + byte[] data = encodeASCII(b.toString()); + assertCorrupt("duplicate entry names", OBJ_TREE, data); + assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(DUPLICATE_ENTRIES, true); + checker.checkTree(data); } @Test - public void testInvalidTreeDuplicateNames2() { - final StringBuilder b = new StringBuilder(); + public void testInvalidTreeDuplicateNames2() throws CorruptObjectException { + StringBuilder b = new StringBuilder(); entry(b, "100644 a"); entry(b, "100755 a"); - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTree(data); - fail("incorrectly accepted an invalid tree"); - } catch (CorruptObjectException e) { - assertEquals("duplicate entry names", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + assertCorrupt("duplicate entry names", OBJ_TREE, data); + assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(DUPLICATE_ENTRIES, true); + checker.checkTree(data); } @Test - public void testInvalidTreeDuplicateNames3() { - final StringBuilder b = new StringBuilder(); + public void testInvalidTreeDuplicateNames3() throws CorruptObjectException { + StringBuilder b = new StringBuilder(); entry(b, "100644 a"); entry(b, "40000 a"); - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTree(data); - fail("incorrectly accepted an invalid tree"); - } catch (CorruptObjectException e) { - assertEquals("duplicate entry names", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + assertCorrupt("duplicate entry names", OBJ_TREE, data); + assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(DUPLICATE_ENTRIES, true); + checker.checkTree(data); } @Test - public void testInvalidTreeDuplicateNames4() { - final StringBuilder b = new StringBuilder(); + public void testInvalidTreeDuplicateNames4() throws CorruptObjectException { + StringBuilder b = new StringBuilder(); entry(b, "100644 a"); entry(b, "100644 a.c"); entry(b, "100644 a.d"); entry(b, "100644 a.e"); entry(b, "40000 a"); entry(b, "100644 zoo"); - final byte[] data = Constants.encodeASCII(b.toString()); - try { - checker.checkTree(data); - fail("incorrectly accepted an invalid tree"); - } catch (CorruptObjectException e) { - assertEquals("duplicate entry names", e.getMessage()); - } + byte[] data = encodeASCII(b.toString()); + assertCorrupt("duplicate entry names", OBJ_TREE, data); + assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(DUPLICATE_ENTRIES, true); + checker.checkTree(data); } @Test public void testInvalidTreeDuplicateNames5() - throws UnsupportedEncodingException { + throws UnsupportedEncodingException, CorruptObjectException { StringBuilder b = new StringBuilder(); - entry(b, "100644 a"); entry(b, "100644 A"); + entry(b, "100644 a"); byte[] data = b.toString().getBytes("UTF-8"); - try { - checker.setSafeForWindows(true); - checker.checkTree(data); - fail("incorrectly accepted an invalid tree"); - } catch (CorruptObjectException e) { - assertEquals("duplicate entry names", e.getMessage()); - } + checker.setSafeForWindows(true); + assertCorrupt("duplicate entry names", OBJ_TREE, data); + assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(DUPLICATE_ENTRIES, true); + checker.checkTree(data); } @Test public void testInvalidTreeDuplicateNames6() - throws UnsupportedEncodingException { + throws UnsupportedEncodingException, CorruptObjectException { StringBuilder b = new StringBuilder(); - entry(b, "100644 a"); entry(b, "100644 A"); + entry(b, "100644 a"); byte[] data = b.toString().getBytes("UTF-8"); - try { - checker.setSafeForMacOS(true); - checker.checkTree(data); - fail("incorrectly accepted an invalid tree"); - } catch (CorruptObjectException e) { - assertEquals("duplicate entry names", e.getMessage()); - } + checker.setSafeForMacOS(true); + assertCorrupt("duplicate entry names", OBJ_TREE, data); + assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(DUPLICATE_ENTRIES, true); + checker.checkTree(data); } @Test public void testInvalidTreeDuplicateNames7() - throws UnsupportedEncodingException { - try { - Class.forName("java.text.Normalizer"); - } catch (ClassNotFoundException e) { - // Ignore this test on Java 5 platform. - return; - } - + throws UnsupportedEncodingException, CorruptObjectException { StringBuilder b = new StringBuilder(); entry(b, "100644 \u0065\u0301"); entry(b, "100644 \u00e9"); byte[] data = b.toString().getBytes("UTF-8"); - try { - checker.setSafeForMacOS(true); - checker.checkTree(data); - fail("incorrectly accepted an invalid tree"); - } catch (CorruptObjectException e) { - assertEquals("duplicate entry names", e.getMessage()); - } + checker.setSafeForMacOS(true); + assertCorrupt("duplicate entry names", OBJ_TREE, data); + assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(DUPLICATE_ENTRIES, true); + checker.checkTree(data); } @Test @@ -1791,7 +1446,7 @@ public class ObjectCheckerTest { @Test public void testRejectNulInPathSegment() { try { - checker.checkPathSegment(Constants.encodeASCII("a\u0000b"), 0, 3); + checker.checkPathSegment(encodeASCII("a\u0000b"), 0, 3); fail("incorrectly accepted NUL in middle of name"); } catch (CorruptObjectException e) { assertEquals("name contains byte 0x00", e.getMessage()); @@ -1893,13 +1548,65 @@ public class ObjectCheckerTest { private void checkOneName(String name) throws CorruptObjectException { StringBuilder b = new StringBuilder(); entry(b, "100644 " + name); - checker.checkTree(Constants.encodeASCII(b.toString())); + checker.checkTree(encodeASCII(b.toString())); } - private static void entry(final StringBuilder b, final String modeName) { + private static void entry(StringBuilder b, final String modeName) { b.append(modeName); b.append('\0'); - for (int i = 0; i < Constants.OBJECT_ID_LENGTH; i++) + for (int i = 0; i < OBJECT_ID_LENGTH; i++) b.append((char) i); } + + private void assertCorrupt(String msg, int type, StringBuilder b) { + assertCorrupt(msg, type, encodeASCII(b.toString())); + } + + private void assertCorrupt(String msg, int type, byte[] data) { + try { + checker.check(type, data); + fail("Did not throw CorruptObjectException"); + } catch (CorruptObjectException e) { + assertEquals(msg, e.getMessage()); + } + } + + private void assertSkipListAccepts(int type, byte[] data) + throws CorruptObjectException { + ObjectId id = idFor(type, data); + checker.setSkipList(set(id)); + checker.check(id, type, data); + checker.setSkipList(null); + } + + private void assertSkipListRejects(String msg, int type, byte[] data) { + ObjectId id = idFor(type, data); + checker.setSkipList(set(id)); + try { + checker.check(id, type, data); + fail("Did not throw CorruptObjectException"); + } catch (CorruptObjectException e) { + assertEquals(msg, e.getMessage()); + } + checker.setSkipList(null); + } + + private static ObjectIdSet set(final ObjectId... ids) { + return new ObjectIdSet() { + @Override + public boolean contains(AnyObjectId objectId) { + for (ObjectId id : ids) { + if (id.equals(objectId)) { + return true; + } + } + return false; + } + }; + } + + @SuppressWarnings("resource") + private static ObjectId idFor(int type, byte[] raw) { + return new ObjectInserter.Formatter().idFor(type, raw); + } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/T0002_TreeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/T0002_TreeTest.java deleted file mode 100644 index 651e62c9c..000000000 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/T0002_TreeTest.java +++ /dev/null @@ -1,319 +0,0 @@ -/* - * Copyright (C) 2008, Robin Rosenberg - * Copyright (C) 2006-2008, Shawn O. Pearce - * and other copyright owners as documented in the project's IP log. - * - * This program and the accompanying materials are made available - * under the terms of the Eclipse Distribution License v1.0 which - * accompanies this distribution, is reproduced below, and is - * available at http://www.eclipse.org/org/documents/edl-v10.php - * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or - * without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * - Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * - * - Neither the name of the Eclipse Foundation, Inc. nor the - * names of its contributors may be used to endorse or promote - * products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND - * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, - * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package org.eclipse.jgit.lib; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; - -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.util.ArrayList; -import java.util.List; - -import org.eclipse.jgit.test.resources.SampleDataRepositoryTestCase; -import org.junit.Test; - -@SuppressWarnings("deprecation") -public class T0002_TreeTest extends SampleDataRepositoryTestCase { - private static final ObjectId SOME_FAKE_ID = ObjectId.fromString( - "0123456789abcdef0123456789abcdef01234567"); - - private static int compareNamesUsingSpecialCompare(String a, String b) - throws UnsupportedEncodingException { - char lasta = '\0'; - byte[] abytes; - if (a.length() > 0 && a.charAt(a.length()-1) == '/') { - lasta = '/'; - a = a.substring(0, a.length() - 1); - } - abytes = a.getBytes("ISO-8859-1"); - char lastb = '\0'; - byte[] bbytes; - if (b.length() > 0 && b.charAt(b.length()-1) == '/') { - lastb = '/'; - b = b.substring(0, b.length() - 1); - } - bbytes = b.getBytes("ISO-8859-1"); - return Tree.compareNames(abytes, bbytes, lasta, lastb); - } - - @Test - public void test000_sort_01() throws UnsupportedEncodingException { - assertEquals(0, compareNamesUsingSpecialCompare("a","a")); - } - - @Test - public void test000_sort_02() throws UnsupportedEncodingException { - assertEquals(-1, compareNamesUsingSpecialCompare("a","b")); - assertEquals(1, compareNamesUsingSpecialCompare("b","a")); - } - - @Test - public void test000_sort_03() throws UnsupportedEncodingException { - assertEquals(1, compareNamesUsingSpecialCompare("a:","a")); - assertEquals(1, compareNamesUsingSpecialCompare("a/","a")); - assertEquals(-1, compareNamesUsingSpecialCompare("a","a/")); - assertEquals(-1, compareNamesUsingSpecialCompare("a","a:")); - assertEquals(1, compareNamesUsingSpecialCompare("a:","a/")); - assertEquals(-1, compareNamesUsingSpecialCompare("a/","a:")); - } - - @Test - public void test000_sort_04() throws UnsupportedEncodingException { - assertEquals(-1, compareNamesUsingSpecialCompare("a.a","a/a")); - assertEquals(1, compareNamesUsingSpecialCompare("a/a","a.a")); - } - - @Test - public void test000_sort_05() throws UnsupportedEncodingException { - assertEquals(-1, compareNamesUsingSpecialCompare("a.","a/")); - assertEquals(1, compareNamesUsingSpecialCompare("a/","a.")); - - } - - @Test - public void test001_createEmpty() throws IOException { - final Tree t = new Tree(db); - assertTrue("isLoaded", t.isLoaded()); - assertTrue("isModified", t.isModified()); - assertTrue("no parent", t.getParent() == null); - assertTrue("isRoot", t.isRoot()); - assertTrue("no name", t.getName() == null); - assertTrue("no nameUTF8", t.getNameUTF8() == null); - assertTrue("has entries array", t.members() != null); - assertEquals("entries is empty", 0, t.members().length); - assertEquals("full name is empty", "", t.getFullName()); - assertTrue("no id", t.getId() == null); - assertTrue("database is r", t.getRepository() == db); - assertTrue("no foo child", t.findTreeMember("foo") == null); - assertTrue("no foo child", t.findBlobMember("foo") == null); - } - - @Test - public void test002_addFile() throws IOException { - final Tree t = new Tree(db); - t.setId(SOME_FAKE_ID); - assertTrue("has id", t.getId() != null); - assertFalse("not modified", t.isModified()); - - final String n = "bob"; - final FileTreeEntry f = t.addFile(n); - assertNotNull("have file", f); - assertEquals("name matches", n, f.getName()); - assertEquals("name matches", f.getName(), new String(f.getNameUTF8(), - "UTF-8")); - assertEquals("full name matches", n, f.getFullName()); - assertTrue("no id", f.getId() == null); - assertTrue("is modified", t.isModified()); - assertTrue("has no id", t.getId() == null); - assertTrue("found bob", t.findBlobMember(f.getName()) == f); - - final TreeEntry[] i = t.members(); - assertNotNull("members array not null", i); - assertTrue("iterator is not empty", i != null && i.length > 0); - assertTrue("iterator returns file", i != null && i[0] == f); - assertTrue("iterator is empty", i != null && i.length == 1); - } - - @Test - public void test004_addTree() throws IOException { - final Tree t = new Tree(db); - t.setId(SOME_FAKE_ID); - assertTrue("has id", t.getId() != null); - assertFalse("not modified", t.isModified()); - - final String n = "bob"; - final Tree f = t.addTree(n); - assertNotNull("have tree", f); - assertEquals("name matches", n, f.getName()); - assertEquals("name matches", f.getName(), new String(f.getNameUTF8(), - "UTF-8")); - assertEquals("full name matches", n, f.getFullName()); - assertTrue("no id", f.getId() == null); - assertTrue("parent matches", f.getParent() == t); - assertTrue("repository matches", f.getRepository() == db); - assertTrue("isLoaded", f.isLoaded()); - assertFalse("has items", f.members().length > 0); - assertFalse("is root", f.isRoot()); - assertTrue("parent is modified", t.isModified()); - assertTrue("parent has no id", t.getId() == null); - assertTrue("found bob child", t.findTreeMember(f.getName()) == f); - - final TreeEntry[] i = t.members(); - assertTrue("iterator is not empty", i.length > 0); - assertTrue("iterator returns file", i[0] == f); - assertEquals("iterator is empty", 1, i.length); - } - - @Test - public void test005_addRecursiveFile() throws IOException { - final Tree t = new Tree(db); - final FileTreeEntry f = t.addFile("a/b/c"); - assertNotNull("created f", f); - assertEquals("c", f.getName()); - assertEquals("b", f.getParent().getName()); - assertEquals("a", f.getParent().getParent().getName()); - assertTrue("t is great-grandparent", t == f.getParent().getParent() - .getParent()); - } - - @Test - public void test005_addRecursiveTree() throws IOException { - final Tree t = new Tree(db); - final Tree f = t.addTree("a/b/c"); - assertNotNull("created f", f); - assertEquals("c", f.getName()); - assertEquals("b", f.getParent().getName()); - assertEquals("a", f.getParent().getParent().getName()); - assertTrue("t is great-grandparent", t == f.getParent().getParent() - .getParent()); - } - - @Test - public void test006_addDeepTree() throws IOException { - final Tree t = new Tree(db); - - final Tree e = t.addTree("e"); - assertNotNull("have e", e); - assertTrue("e.parent == t", e.getParent() == t); - final Tree f = t.addTree("f"); - assertNotNull("have f", f); - assertTrue("f.parent == t", f.getParent() == t); - final Tree g = f.addTree("g"); - assertNotNull("have g", g); - assertTrue("g.parent == f", g.getParent() == f); - final Tree h = g.addTree("h"); - assertNotNull("have h", h); - assertTrue("h.parent = g", h.getParent() == g); - - h.setId(SOME_FAKE_ID); - assertTrue("h not modified", !h.isModified()); - g.setId(SOME_FAKE_ID); - assertTrue("g not modified", !g.isModified()); - f.setId(SOME_FAKE_ID); - assertTrue("f not modified", !f.isModified()); - e.setId(SOME_FAKE_ID); - assertTrue("e not modified", !e.isModified()); - t.setId(SOME_FAKE_ID); - assertTrue("t not modified.", !t.isModified()); - - assertEquals("full path of h ok", "f/g/h", h.getFullName()); - assertTrue("Can find h", t.findTreeMember(h.getFullName()) == h); - assertTrue("Can't find f/z", t.findBlobMember("f/z") == null); - assertTrue("Can't find y/z", t.findBlobMember("y/z") == null); - - final FileTreeEntry i = h.addFile("i"); - assertNotNull(i); - assertEquals("full path of i ok", "f/g/h/i", i.getFullName()); - assertTrue("Can find i", t.findBlobMember(i.getFullName()) == i); - assertTrue("h modified", h.isModified()); - assertTrue("g modified", g.isModified()); - assertTrue("f modified", f.isModified()); - assertTrue("e not modified", !e.isModified()); - assertTrue("t modified", t.isModified()); - - assertTrue("h no id", h.getId() == null); - assertTrue("g no id", g.getId() == null); - assertTrue("f no id", f.getId() == null); - assertTrue("e has id", e.getId() != null); - assertTrue("t no id", t.getId() == null); - } - - @Test - public void test007_manyFileLookup() throws IOException { - final Tree t = new Tree(db); - final List files = new ArrayList(26 * 26); - for (char level1 = 'a'; level1 <= 'z'; level1++) { - for (char level2 = 'a'; level2 <= 'z'; level2++) { - final String n = "." + level1 + level2 + "9"; - final FileTreeEntry f = t.addFile(n); - assertNotNull("File " + n + " added.", f); - assertEquals(n, f.getName()); - files.add(f); - } - } - assertEquals(files.size(), t.memberCount()); - final TreeEntry[] ents = t.members(); - assertNotNull(ents); - assertEquals(files.size(), ents.length); - for (int k = 0; k < ents.length; k++) { - assertTrue("File " + files.get(k).getName() - + " is at " + k + ".", files.get(k) == ents[k]); - } - } - - @Test - public void test008_SubtreeInternalSorting() throws IOException { - final Tree t = new Tree(db); - final FileTreeEntry e0 = t.addFile("a-b"); - final FileTreeEntry e1 = t.addFile("a-"); - final FileTreeEntry e2 = t.addFile("a=b"); - final Tree e3 = t.addTree("a"); - final FileTreeEntry e4 = t.addFile("a="); - - final TreeEntry[] ents = t.members(); - assertSame(e1, ents[0]); - assertSame(e0, ents[1]); - assertSame(e3, ents[2]); - assertSame(e4, ents[3]); - assertSame(e2, ents[4]); - } - - @Test - public void test009_SymlinkAndGitlink() throws IOException { - final Tree symlinkTree = mapTree("symlink"); - assertTrue("Symlink entry exists", symlinkTree.existsBlob("symlink.txt")); - final Tree gitlinkTree = mapTree("gitlink"); - assertTrue("Gitlink entry exists", gitlinkTree.existsBlob("submodule")); - } - - private Tree mapTree(String name) throws IOException { - ObjectId id = db.resolve(name + "^{tree}"); - return new Tree(db, id, db.open(id).getCachedBytes()); - } -} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/ObjectWalkTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/ObjectWalkTest.java index 2a59f58c6..9c9edc147 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/ObjectWalkTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/ObjectWalkTest.java @@ -47,11 +47,10 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; -import org.eclipse.jgit.lib.Constants; -import org.eclipse.jgit.lib.FileTreeEntry; +import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; -import org.eclipse.jgit.lib.Tree; +import org.eclipse.jgit.lib.TreeFormatter; import org.junit.Test; @SuppressWarnings("deprecation") @@ -220,28 +219,24 @@ public class ObjectWalkTest extends RevWalkTestCase { .fromString("abbbfafe3129f85747aba7bfac992af77134c607"); final RevTree tree_root, tree_A, tree_AB; final RevCommit b; - { - Tree root = new Tree(db); - Tree A = root.addTree("A"); - FileTreeEntry B = root.addFile("B"); - B.setId(bId); - - Tree A_A = A.addTree("A"); - Tree A_B = A.addTree("B"); - - try (final ObjectInserter inserter = db.newObjectInserter()) { - A_A.setId(inserter.insert(Constants.OBJ_TREE, A_A.format())); - A_B.setId(inserter.insert(Constants.OBJ_TREE, A_B.format())); - A.setId(inserter.insert(Constants.OBJ_TREE, A.format())); - root.setId(inserter.insert(Constants.OBJ_TREE, root.format())); - inserter.flush(); - } - - tree_root = rw.parseTree(root.getId()); - tree_A = rw.parseTree(A.getId()); - tree_AB = rw.parseTree(A_A.getId()); - assertSame(tree_AB, rw.parseTree(A_B.getId())); - b = commit(rw.parseTree(root.getId())); + try (ObjectInserter inserter = db.newObjectInserter()) { + ObjectId empty = inserter.insert(new TreeFormatter()); + + TreeFormatter A = new TreeFormatter(); + A.append("A", FileMode.TREE, empty); + A.append("B", FileMode.TREE, empty); + ObjectId idA = inserter.insert(A); + + TreeFormatter root = new TreeFormatter(); + root.append("A", FileMode.TREE, idA); + root.append("B", FileMode.REGULAR_FILE, bId); + ObjectId idRoot = inserter.insert(root); + inserter.flush(); + + tree_root = objw.parseTree(idRoot); + tree_A = objw.parseTree(idA); + tree_AB = objw.parseTree(empty); + b = commit(tree_root); } markStart(b); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevCommitParseTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevCommitParseTest.java index beda2a7b9..885c1b5b2 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevCommitParseTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevCommitParseTest.java @@ -43,13 +43,18 @@ package org.eclipse.jgit.revwalk; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import java.io.ByteArrayOutputStream; import java.io.UnsupportedEncodingException; +import java.nio.charset.IllegalCharsetNameException; +import java.nio.charset.UnsupportedCharsetException; import java.util.TimeZone; import org.eclipse.jgit.junit.RepositoryTestCase; @@ -303,6 +308,86 @@ public class RevCommitParseTest extends RepositoryTestCase { assertEquals("\u304d\u308c\u3044\n\nHi\n", c.getFullMessage()); } + @Test + public void testParse_incorrectUtf8Name() throws Exception { + ByteArrayOutputStream b = new ByteArrayOutputStream(); + b.write("tree 9788669ad918b6fcce64af8882fc9a81cb6aba67\n" + .getBytes(UTF_8)); + b.write("author au 1218123387 +0700\n".getBytes(UTF_8)); + b.write("committer co 1218123390 -0500\n" + .getBytes(UTF_8)); + b.write("encoding 'utf8'\n".getBytes(UTF_8)); + b.write("\n".getBytes(UTF_8)); + b.write("Sm\u00f6rg\u00e5sbord\n".getBytes(UTF_8)); + + RevCommit c = new RevCommit( + id("9473095c4cb2f12aefe1db8a355fe3fafba42f67")); + c.parseCanonical(new RevWalk(db), b.toByteArray()); + assertEquals("'utf8'", c.getEncodingName()); + assertEquals("Sm\u00f6rg\u00e5sbord\n", c.getFullMessage()); + + try { + c.getEncoding(); + fail("Expected " + IllegalCharsetNameException.class); + } catch (IllegalCharsetNameException badName) { + assertEquals("'utf8'", badName.getMessage()); + } + } + + @Test + public void testParse_illegalEncoding() throws Exception { + ByteArrayOutputStream b = new ByteArrayOutputStream(); + b.write("tree 9788669ad918b6fcce64af8882fc9a81cb6aba67\n".getBytes(UTF_8)); + b.write("author au 1218123387 +0700\n".getBytes(UTF_8)); + b.write("committer co 1218123390 -0500\n".getBytes(UTF_8)); + b.write("encoding utf-8logoutputencoding=gbk\n".getBytes(UTF_8)); + b.write("\n".getBytes(UTF_8)); + b.write("message\n".getBytes(UTF_8)); + + RevCommit c = new RevCommit( + id("9473095c4cb2f12aefe1db8a355fe3fafba42f67")); + c.parseCanonical(new RevWalk(db), b.toByteArray()); + assertEquals("utf-8logoutputencoding=gbk", c.getEncodingName()); + assertEquals("message\n", c.getFullMessage()); + assertEquals("message", c.getShortMessage()); + assertTrue(c.getFooterLines().isEmpty()); + assertEquals("au", c.getAuthorIdent().getName()); + + try { + c.getEncoding(); + fail("Expected " + IllegalCharsetNameException.class); + } catch (IllegalCharsetNameException badName) { + assertEquals("utf-8logoutputencoding=gbk", badName.getMessage()); + } + } + + @Test + public void testParse_unsupportedEncoding() throws Exception { + ByteArrayOutputStream b = new ByteArrayOutputStream(); + b.write("tree 9788669ad918b6fcce64af8882fc9a81cb6aba67\n".getBytes(UTF_8)); + b.write("author au 1218123387 +0700\n".getBytes(UTF_8)); + b.write("committer co 1218123390 -0500\n".getBytes(UTF_8)); + b.write("encoding it_IT.UTF8\n".getBytes(UTF_8)); + b.write("\n".getBytes(UTF_8)); + b.write("message\n".getBytes(UTF_8)); + + RevCommit c = new RevCommit( + id("9473095c4cb2f12aefe1db8a355fe3fafba42f67")); + c.parseCanonical(new RevWalk(db), b.toByteArray()); + assertEquals("it_IT.UTF8", c.getEncodingName()); + assertEquals("message\n", c.getFullMessage()); + assertEquals("message", c.getShortMessage()); + assertTrue(c.getFooterLines().isEmpty()); + assertEquals("au", c.getAuthorIdent().getName()); + + try { + c.getEncoding(); + fail("Expected " + UnsupportedCharsetException.class); + } catch (UnsupportedCharsetException badName) { + assertEquals("it_IT.UTF8", badName.getMessage()); + } + } + @Test public void testParse_NoMessage() throws Exception { final String msg = ""; diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevTagParseTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevTagParseTest.java index 614f49bf0..82505caf2 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevTagParseTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevTagParseTest.java @@ -43,6 +43,7 @@ package org.eclipse.jgit.revwalk; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -361,6 +362,44 @@ public class RevTagParseTest extends RepositoryTestCase { assertEquals("\u304d\u308c\u3044\n\nHi\n", c.getFullMessage()); } + @Test + public void testParse_illegalEncoding() throws Exception { + ByteArrayOutputStream b = new ByteArrayOutputStream(); + b.write("object 9788669ad918b6fcce64af8882fc9a81cb6aba67\n".getBytes(UTF_8)); + b.write("type tree\n".getBytes(UTF_8)); + b.write("tag v1.0\n".getBytes(UTF_8)); + b.write("tagger t 1218123387 +0700\n".getBytes(UTF_8)); + b.write("encoding utf-8logoutputencoding=gbk\n".getBytes(UTF_8)); + b.write("\n".getBytes(UTF_8)); + b.write("message\n".getBytes(UTF_8)); + + RevTag t = new RevTag(id("9473095c4cb2f12aefe1db8a355fe3fafba42f67")); + t.parseCanonical(new RevWalk(db), b.toByteArray()); + + assertEquals("t", t.getTaggerIdent().getName()); + assertEquals("message", t.getShortMessage()); + assertEquals("message\n", t.getFullMessage()); + } + + @Test + public void testParse_unsupportedEncoding() throws Exception { + ByteArrayOutputStream b = new ByteArrayOutputStream(); + b.write("object 9788669ad918b6fcce64af8882fc9a81cb6aba67\n".getBytes(UTF_8)); + b.write("type tree\n".getBytes(UTF_8)); + b.write("tag v1.0\n".getBytes(UTF_8)); + b.write("tagger t 1218123387 +0700\n".getBytes(UTF_8)); + b.write("encoding it_IT.UTF8\n".getBytes(UTF_8)); + b.write("\n".getBytes(UTF_8)); + b.write("message\n".getBytes(UTF_8)); + + RevTag t = new RevTag(id("9473095c4cb2f12aefe1db8a355fe3fafba42f67")); + t.parseCanonical(new RevWalk(db), b.toByteArray()); + + assertEquals("t", t.getTaggerIdent().getName()); + assertEquals("message", t.getShortMessage()); + assertEquals("message\n", t.getFullMessage()); + } + @Test public void testParse_NoMessage() throws Exception { final String msg = ""; diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/AtomicPushTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/AtomicPushTest.java index 782e414b6..c1e078d10 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/AtomicPushTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/AtomicPushTest.java @@ -112,12 +112,9 @@ public class AtomicPushTest { public void pushNonAtomic() throws Exception { PushResult r; server.setPerformsAtomicTransactions(false); - Transport tn = testProtocol.open(uri, client, "server"); - try { + try (Transport tn = testProtocol.open(uri, client, "server")) { tn.setPushAtomic(false); r = tn.push(NullProgressMonitor.INSTANCE, commands()); - } finally { - tn.close(); } RemoteRefUpdate one = r.getRemoteUpdate("refs/heads/one"); @@ -131,12 +128,9 @@ public class AtomicPushTest { @Test public void pushAtomicClientGivesUpEarly() throws Exception { PushResult r; - Transport tn = testProtocol.open(uri, client, "server"); - try { + try (Transport tn = testProtocol.open(uri, client, "server")) { tn.setPushAtomic(true); r = tn.push(NullProgressMonitor.INSTANCE, commands()); - } finally { - tn.close(); } RemoteRefUpdate one = r.getRemoteUpdate("refs/heads/one"); @@ -167,8 +161,7 @@ public class AtomicPushTest { ObjectId.zeroId())); server.setPerformsAtomicTransactions(false); - Transport tn = testProtocol.open(uri, client, "server"); - try { + try (Transport tn = testProtocol.open(uri, client, "server")) { tn.setPushAtomic(true); tn.push(NullProgressMonitor.INSTANCE, cmds); fail("did not throw TransportException"); @@ -176,8 +169,6 @@ public class AtomicPushTest { assertEquals( uri + ": " + JGitText.get().atomicPushNotSupported, e.getMessage()); - } finally { - tn.close(); } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BundleWriterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BundleWriterTest.java index 461530896..a83a99333 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BundleWriterTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BundleWriterTest.java @@ -168,8 +168,10 @@ public class BundleWriterTest extends SampleDataRepositoryTestCase { final ByteArrayInputStream in = new ByteArrayInputStream(bundle); final RefSpec rs = new RefSpec("refs/heads/*:refs/heads/*"); final Set refs = Collections.singleton(rs); - return new TransportBundleStream(newRepo, uri, in).fetch( - NullProgressMonitor.INSTANCE, refs); + try (TransportBundleStream transport = new TransportBundleStream( + newRepo, uri, in)) { + return transport.fetch(NullProgressMonitor.INSTANCE, refs); + } } private byte[] makeBundle(final String name, diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ReceivePackAdvertiseRefsHookTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ReceivePackAdvertiseRefsHookTest.java index aa5914fe0..94bc383db 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ReceivePackAdvertiseRefsHookTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ReceivePackAdvertiseRefsHookTest.java @@ -116,12 +116,9 @@ public class ReceivePackAdvertiseRefsHookTest extends LocalDiskRepositoryTestCas // Clone from dst into src // - Transport t = Transport.open(src, uriOf(dst)); - try { + try (Transport t = Transport.open(src, uriOf(dst))) { t.fetch(PM, Collections.singleton(new RefSpec("+refs/*:refs/*"))); assertEquals(B, src.resolve(R_MASTER)); - } finally { - t.close(); } // Now put private stuff into dst. @@ -144,7 +141,8 @@ public class ReceivePackAdvertiseRefsHookTest extends LocalDiskRepositoryTestCas @Test public void testFilterHidesPrivate() throws Exception { Map refs; - TransportLocal t = new TransportLocal(src, uriOf(dst), dst.getDirectory()) { + try (TransportLocal t = new TransportLocal(src, uriOf(dst), + dst.getDirectory()) { @Override ReceivePack createReceivePack(final Repository db) { db.close(); @@ -154,16 +152,10 @@ public class ReceivePackAdvertiseRefsHookTest extends LocalDiskRepositoryTestCas rp.setAdvertiseRefsHook(new HidePrivateHook()); return rp; } - }; - try { - PushConnection c = t.openPush(); - try { + }) { + try (PushConnection c = t.openPush()) { refs = c.getRefsMap(); - } finally { - c.close(); } - } finally { - t.close(); } assertNotNull(refs); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RefSpecTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RefSpecTest.java index 3f5fcbbf0..4f833509d 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RefSpecTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RefSpecTest.java @@ -340,6 +340,41 @@ public class RefSpecTest { assertEquals("refs/heads/foo", c.getSource()); } + @Test + public void testWildcardAfterText1() { + RefSpec a = new RefSpec("refs/heads/*/for-linus:refs/remotes/mine/*-blah"); + assertTrue(a.isWildcard()); + assertTrue(a.matchDestination("refs/remotes/mine/x-blah")); + assertTrue(a.matchDestination("refs/remotes/mine/foo-blah")); + assertTrue(a.matchDestination("refs/remotes/mine/foo/x-blah")); + assertFalse(a.matchDestination("refs/remotes/origin/foo/x-blah")); + + RefSpec b = a.expandFromSource("refs/heads/foo/for-linus"); + assertEquals("refs/remotes/mine/foo-blah", b.getDestination()); + RefSpec c = a.expandFromDestination("refs/remotes/mine/foo-blah"); + assertEquals("refs/heads/foo/for-linus", c.getSource()); + } + + @Test + public void testWildcardAfterText2() { + RefSpec a = new RefSpec("refs/heads*/for-linus:refs/remotes/mine/*"); + assertTrue(a.isWildcard()); + assertTrue(a.matchSource("refs/headsx/for-linus")); + assertTrue(a.matchSource("refs/headsfoo/for-linus")); + assertTrue(a.matchSource("refs/headsx/foo/for-linus")); + assertFalse(a.matchSource("refs/headx/for-linus")); + + RefSpec b = a.expandFromSource("refs/headsx/for-linus"); + assertEquals("refs/remotes/mine/x", b.getDestination()); + RefSpec c = a.expandFromDestination("refs/remotes/mine/x"); + assertEquals("refs/headsx/for-linus", c.getSource()); + + RefSpec d = a.expandFromSource("refs/headsx/foo/for-linus"); + assertEquals("refs/remotes/mine/x/foo", d.getDestination()); + RefSpec e = a.expandFromDestination("refs/remotes/mine/x/foo"); + assertEquals("refs/headsx/foo/for-linus", e.getSource()); + } + @Test public void testWildcardMirror() { RefSpec a = new RefSpec("*:*"); @@ -403,21 +438,6 @@ public class RefSpecTest { assertNotNull(new RefSpec("refs/heads/*:refs/heads/*/*")); } - @Test(expected = IllegalArgumentException.class) - public void invalidWhenWildcardAfterText() { - assertNotNull(new RefSpec("refs/heads/wrong*:refs/heads/right/*")); - } - - @Test(expected = IllegalArgumentException.class) - public void invalidWhenWildcardBeforeText() { - assertNotNull(new RefSpec("*wrong:right/*")); - } - - @Test(expected = IllegalArgumentException.class) - public void invalidWhenWildcardBeforeTextAtEnd() { - assertNotNull(new RefSpec("refs/heads/*wrong:right/*")); - } - @Test(expected = IllegalArgumentException.class) public void invalidSourceDoubleSlashes() { assertNotNull(new RefSpec("refs/heads//wrong")); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportTest.java index 55e1e4420..5519f61ac 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportTest.java @@ -61,13 +61,10 @@ import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.test.resources.SampleDataRepositoryTestCase; -import org.junit.After; import org.junit.Before; import org.junit.Test; public class TransportTest extends SampleDataRepositoryTestCase { - private Transport transport; - private RemoteConfig remoteConfig; @Override @@ -77,17 +74,6 @@ public class TransportTest extends SampleDataRepositoryTestCase { final Config config = db.getConfig(); remoteConfig = new RemoteConfig(config, "test"); remoteConfig.addURI(new URIish("http://everyones.loves.git/u/2")); - transport = null; - } - - @Override - @After - public void tearDown() throws Exception { - if (transport != null) { - transport.close(); - transport = null; - } - super.tearDown(); } /** @@ -99,10 +85,11 @@ public class TransportTest extends SampleDataRepositoryTestCase { @Test public void testFindRemoteRefUpdatesNoWildcardNoTracking() throws IOException { - transport = Transport.open(db, remoteConfig); - final Collection result = transport - .findRemoteRefUpdatesFor(Collections.nCopies(1, new RefSpec( - "refs/heads/master:refs/heads/x"))); + Collection result; + try (Transport transport = Transport.open(db, remoteConfig)) { + result = transport.findRemoteRefUpdatesFor(Collections.nCopies(1, + new RefSpec("refs/heads/master:refs/heads/x"))); + } assertEquals(1, result.size()); final RemoteRefUpdate rru = result.iterator().next(); @@ -122,10 +109,11 @@ public class TransportTest extends SampleDataRepositoryTestCase { @Test public void testFindRemoteRefUpdatesNoWildcardNoDestination() throws IOException { - transport = Transport.open(db, remoteConfig); - final Collection result = transport - .findRemoteRefUpdatesFor(Collections.nCopies(1, new RefSpec( - "+refs/heads/master"))); + Collection result; + try (Transport transport = Transport.open(db, remoteConfig)) { + result = transport.findRemoteRefUpdatesFor( + Collections.nCopies(1, new RefSpec("+refs/heads/master"))); + } assertEquals(1, result.size()); final RemoteRefUpdate rru = result.iterator().next(); @@ -143,10 +131,11 @@ public class TransportTest extends SampleDataRepositoryTestCase { */ @Test public void testFindRemoteRefUpdatesWildcardNoTracking() throws IOException { - transport = Transport.open(db, remoteConfig); - final Collection result = transport - .findRemoteRefUpdatesFor(Collections.nCopies(1, new RefSpec( - "+refs/heads/*:refs/heads/test/*"))); + Collection result; + try (Transport transport = Transport.open(db, remoteConfig)) { + result = transport.findRemoteRefUpdatesFor(Collections.nCopies(1, + new RefSpec("+refs/heads/*:refs/heads/test/*"))); + } assertEquals(12, result.size()); boolean foundA = false; @@ -171,12 +160,14 @@ public class TransportTest extends SampleDataRepositoryTestCase { */ @Test public void testFindRemoteRefUpdatesTwoRefSpecs() throws IOException { - transport = Transport.open(db, remoteConfig); final RefSpec specA = new RefSpec("+refs/heads/a:refs/heads/b"); final RefSpec specC = new RefSpec("+refs/heads/c:refs/heads/d"); final Collection specs = Arrays.asList(specA, specC); - final Collection result = transport - .findRemoteRefUpdatesFor(specs); + + Collection result; + try (Transport transport = Transport.open(db, remoteConfig)) { + result = transport.findRemoteRefUpdatesFor(specs); + } assertEquals(2, result.size()); boolean foundA = false; @@ -202,10 +193,12 @@ public class TransportTest extends SampleDataRepositoryTestCase { public void testFindRemoteRefUpdatesTrackingRef() throws IOException { remoteConfig.addFetchRefSpec(new RefSpec( "refs/heads/*:refs/remotes/test/*")); - transport = Transport.open(db, remoteConfig); - final Collection result = transport - .findRemoteRefUpdatesFor(Collections.nCopies(1, new RefSpec( - "+refs/heads/a:refs/heads/a"))); + + Collection result; + try (Transport transport = Transport.open(db, remoteConfig)) { + result = transport.findRemoteRefUpdatesFor(Collections.nCopies(1, + new RefSpec("+refs/heads/a:refs/heads/a"))); + } assertEquals(1, result.size()); final TrackingRefUpdate tru = result.iterator().next() @@ -225,20 +218,18 @@ public class TransportTest extends SampleDataRepositoryTestCase { config.addURI(new URIish("../" + otherDir)); // Should not throw NoRemoteRepositoryException - transport = Transport.open(db, config); + Transport.open(db, config).close(); } @Test public void testLocalTransportFetchWithoutLocalRepository() throws Exception { URIish uri = new URIish("file://" + db.getWorkTree().getAbsolutePath()); - transport = Transport.open(uri); - FetchConnection fetchConnection = transport.openFetch(); - try { - Ref head = fetchConnection.getRef(Constants.HEAD); - assertNotNull(head); - } finally { - fetchConnection.close(); + try (Transport transport = Transport.open(uri)) { + try (FetchConnection fetchConnection = transport.openFetch()) { + Ref head = fetchConnection.getRef(Constants.HEAD); + assertNotNull(head); + } } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/URIishTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/URIishTest.java index 2078dd337..e55d37334 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/URIishTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/URIishTest.java @@ -459,6 +459,48 @@ public class URIishTest { assertEquals(u, new URIish(str)); } + @Test + public void testSshProtoWithEmailUserAndPort() throws Exception { + final String str = "ssh://user.name@email.com@example.com:33/some/p ath"; + URIish u = new URIish(str); + assertEquals("ssh", u.getScheme()); + assertTrue(u.isRemote()); + assertEquals("/some/p ath", u.getRawPath()); + assertEquals("/some/p ath", u.getPath()); + assertEquals("example.com", u.getHost()); + assertEquals("user.name@email.com", u.getUser()); + assertNull(u.getPass()); + assertEquals(33, u.getPort()); + assertEquals("ssh://user.name%40email.com@example.com:33/some/p ath", + u.toPrivateString()); + assertEquals("ssh://user.name%40email.com@example.com:33/some/p%20ath", + u.toPrivateASCIIString()); + assertEquals(u.setPass(null).toPrivateString(), u.toString()); + assertEquals(u.setPass(null).toPrivateASCIIString(), u.toASCIIString()); + assertEquals(u, new URIish(str)); + } + + @Test + public void testSshProtoWithEmailUserPassAndPort() throws Exception { + final String str = "ssh://user.name@email.com:pass@wor:d@example.com:33/some/p ath"; + URIish u = new URIish(str); + assertEquals("ssh", u.getScheme()); + assertTrue(u.isRemote()); + assertEquals("/some/p ath", u.getRawPath()); + assertEquals("/some/p ath", u.getPath()); + assertEquals("example.com", u.getHost()); + assertEquals("user.name@email.com", u.getUser()); + assertEquals("pass@wor:d", u.getPass()); + assertEquals(33, u.getPort()); + assertEquals("ssh://user.name%40email.com:pass%40wor%3ad@example.com:33/some/p ath", + u.toPrivateString()); + assertEquals("ssh://user.name%40email.com:pass%40wor%3ad@example.com:33/some/p%20ath", + u.toPrivateASCIIString()); + assertEquals(u.setPass(null).toPrivateString(), u.toString()); + assertEquals(u.setPass(null).toPrivateASCIIString(), u.toASCIIString()); + assertEquals(u, new URIish(str)); + } + @Test public void testSshProtoWithADUserPassAndPort() throws Exception { final String str = "ssh://DOMAIN\\user:pass@example.com:33/some/p ath"; diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/TreeWalkBasicDiffTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/TreeWalkBasicDiffTest.java index aca7c80fd..c3ff7df8f 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/TreeWalkBasicDiffTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/TreeWalkBasicDiffTest.java @@ -44,7 +44,6 @@ package org.eclipse.jgit.treewalk; import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; -import static org.eclipse.jgit.lib.Constants.OBJ_TREE; import static org.eclipse.jgit.lib.Constants.encode; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -54,11 +53,10 @@ import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; -import org.eclipse.jgit.lib.Tree; +import org.eclipse.jgit.lib.TreeFormatter; import org.eclipse.jgit.treewalk.filter.TreeFilter; import org.junit.Test; -@SuppressWarnings("deprecation") public class TreeWalkBasicDiffTest extends RepositoryTestCase { @Test public void testMissingSubtree_DetectFileAdded_FileModified() @@ -72,62 +70,63 @@ public class TreeWalkBasicDiffTest extends RepositoryTestCase { // Create sub-a/empty, sub-c/empty = hello. { - final Tree root = new Tree(db); + TreeFormatter root = new TreeFormatter(); { - final Tree subA = root.addTree("sub-a"); - subA.addFile("empty").setId(aFileId); - subA.setId(inserter.insert(OBJ_TREE, subA.format())); + TreeFormatter subA = new TreeFormatter(); + subA.append("empty", FileMode.REGULAR_FILE, aFileId); + root.append("sub-a", FileMode.TREE, inserter.insert(subA)); } { - final Tree subC = root.addTree("sub-c"); - subC.addFile("empty").setId(cFileId1); - subC.setId(inserter.insert(OBJ_TREE, subC.format())); + TreeFormatter subC = new TreeFormatter(); + subC.append("empty", FileMode.REGULAR_FILE, cFileId1); + root.append("sub-c", FileMode.TREE, inserter.insert(subC)); } - oldTree = inserter.insert(OBJ_TREE, root.format()); + oldTree = inserter.insert(root); } // Create sub-a/empty, sub-b/empty, sub-c/empty. { - final Tree root = new Tree(db); + TreeFormatter root = new TreeFormatter(); { - final Tree subA = root.addTree("sub-a"); - subA.addFile("empty").setId(aFileId); - subA.setId(inserter.insert(OBJ_TREE, subA.format())); + TreeFormatter subA = new TreeFormatter(); + subA.append("empty", FileMode.REGULAR_FILE, aFileId); + root.append("sub-a", FileMode.TREE, inserter.insert(subA)); } { - final Tree subB = root.addTree("sub-b"); - subB.addFile("empty").setId(bFileId); - subB.setId(inserter.insert(OBJ_TREE, subB.format())); + TreeFormatter subB = new TreeFormatter(); + subB.append("empty", FileMode.REGULAR_FILE, bFileId); + root.append("sub-b", FileMode.TREE, inserter.insert(subB)); } { - final Tree subC = root.addTree("sub-c"); - subC.addFile("empty").setId(cFileId2); - subC.setId(inserter.insert(OBJ_TREE, subC.format())); + TreeFormatter subC = new TreeFormatter(); + subC.append("empty", FileMode.REGULAR_FILE, cFileId2); + root.append("sub-c", FileMode.TREE, inserter.insert(subC)); } - newTree = inserter.insert(OBJ_TREE, root.format()); + newTree = inserter.insert(root); } inserter.flush(); } - final TreeWalk tw = new TreeWalk(db); - tw.reset(oldTree, newTree); - tw.setRecursive(true); - tw.setFilter(TreeFilter.ANY_DIFF); + try (TreeWalk tw = new TreeWalk(db)) { + tw.reset(oldTree, newTree); + tw.setRecursive(true); + tw.setFilter(TreeFilter.ANY_DIFF); - assertTrue(tw.next()); - assertEquals("sub-b/empty", tw.getPathString()); - assertEquals(FileMode.MISSING, tw.getFileMode(0)); - assertEquals(FileMode.REGULAR_FILE, tw.getFileMode(1)); - assertEquals(ObjectId.zeroId(), tw.getObjectId(0)); - assertEquals(bFileId, tw.getObjectId(1)); + assertTrue(tw.next()); + assertEquals("sub-b/empty", tw.getPathString()); + assertEquals(FileMode.MISSING, tw.getFileMode(0)); + assertEquals(FileMode.REGULAR_FILE, tw.getFileMode(1)); + assertEquals(ObjectId.zeroId(), tw.getObjectId(0)); + assertEquals(bFileId, tw.getObjectId(1)); - assertTrue(tw.next()); - assertEquals("sub-c/empty", tw.getPathString()); - assertEquals(FileMode.REGULAR_FILE, tw.getFileMode(0)); - assertEquals(FileMode.REGULAR_FILE, tw.getFileMode(1)); - assertEquals(cFileId1, tw.getObjectId(0)); - assertEquals(cFileId2, tw.getObjectId(1)); + assertTrue(tw.next()); + assertEquals("sub-c/empty", tw.getPathString()); + assertEquals(FileMode.REGULAR_FILE, tw.getFileMode(0)); + assertEquals(FileMode.REGULAR_FILE, tw.getFileMode(1)); + assertEquals(cFileId1, tw.getObjectId(0)); + assertEquals(cFileId2, tw.getObjectId(1)); - assertFalse(tw.next()); + assertFalse(tw.next()); + } } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/PathFilterGroupTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/PathFilterGroupTest.java index d0062e199..5edc1924f 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/PathFilterGroupTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/PathFilterGroupTest.java @@ -43,11 +43,18 @@ package org.eclipse.jgit.treewalk.filter; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheEditor; @@ -58,6 +65,7 @@ import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.errors.StopWalkException; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.Sets; import org.eclipse.jgit.treewalk.TreeWalk; import org.junit.Before; import org.junit.Test; @@ -66,6 +74,8 @@ public class PathFilterGroupTest { private TreeFilter filter; + private Map singles; + @Before public void setup() { // @formatter:off @@ -81,64 +91,75 @@ public class PathFilterGroupTest { }; // @formatter:on filter = PathFilterGroup.createFromStrings(paths); + singles = new HashMap<>(); + for (String path : paths) { + singles.put(path, PathFilterGroup.createFromStrings(path)); + } } @Test public void testExact() throws MissingObjectException, IncorrectObjectTypeException, IOException { - assertTrue(filter.include(fakeWalk("a"))); - assertTrue(filter.include(fakeWalk("b/c"))); - assertTrue(filter.include(fakeWalk("c/d/e"))); - assertTrue(filter.include(fakeWalk("c/d/f"))); - assertTrue(filter.include(fakeWalk("d/e/f/g"))); - assertTrue(filter.include(fakeWalk("d/e/f/g.x"))); + assertMatches(Sets.of("a"), fakeWalk("a")); + assertMatches(Sets.of("b/c"), fakeWalk("b/c")); + assertMatches(Sets.of("c/d/e"), fakeWalk("c/d/e")); + assertMatches(Sets.of("c/d/f"), fakeWalk("c/d/f")); + assertMatches(Sets.of("d/e/f/g"), fakeWalk("d/e/f/g")); + assertMatches(Sets.of("d/e/f/g.x"), fakeWalk("d/e/f/g.x")); } @Test public void testNoMatchButClose() throws MissingObjectException, IncorrectObjectTypeException, IOException { - assertFalse(filter.include(fakeWalk("a+"))); - assertFalse(filter.include(fakeWalk("b+/c"))); - assertFalse(filter.include(fakeWalk("c+/d/e"))); - assertFalse(filter.include(fakeWalk("c+/d/f"))); - assertFalse(filter.include(fakeWalk("c/d.a"))); - assertFalse(filter.include(fakeWalk("d+/e/f/g"))); + assertNoMatches(fakeWalk("a+")); + assertNoMatches(fakeWalk("b+/c")); + assertNoMatches(fakeWalk("c+/d/e")); + assertNoMatches(fakeWalk("c+/d/f")); + assertNoMatches(fakeWalk("c/d.a")); + assertNoMatches(fakeWalk("d+/e/f/g")); } @Test public void testJustCommonPrefixIsNotMatch() throws MissingObjectException, IncorrectObjectTypeException, IOException { - assertFalse(filter.include(fakeWalk("b/a"))); - assertFalse(filter.include(fakeWalk("b/d"))); - assertFalse(filter.include(fakeWalk("c/d/a"))); - assertFalse(filter.include(fakeWalk("d/e/e"))); + assertNoMatches(fakeWalk("b/a")); + assertNoMatches(fakeWalk("b/d")); + assertNoMatches(fakeWalk("c/d/a")); + assertNoMatches(fakeWalk("d/e/e")); + assertNoMatches(fakeWalk("d/e/f/g.y")); } @Test public void testKeyIsPrefixOfFilter() throws MissingObjectException, IncorrectObjectTypeException, IOException { - assertTrue(filter.include(fakeWalk("b"))); - assertTrue(filter.include(fakeWalk("c/d"))); - assertTrue(filter.include(fakeWalk("c/d"))); - assertTrue(filter.include(fakeWalk("c"))); - assertTrue(filter.include(fakeWalk("d/e/f"))); - assertTrue(filter.include(fakeWalk("d/e"))); - assertTrue(filter.include(fakeWalk("d"))); + assertMatches(Sets.of("b/c"), fakeWalkAtSubtree("b")); + assertMatches(Sets.of("c/d/e", "c/d/f"), fakeWalkAtSubtree("c/d")); + assertMatches(Sets.of("c/d/e", "c/d/f"), fakeWalkAtSubtree("c")); + assertMatches(Sets.of("d/e/f/g", "d/e/f/g.x"), + fakeWalkAtSubtree("d/e/f")); + assertMatches(Sets.of("d/e/f/g", "d/e/f/g.x"), + fakeWalkAtSubtree("d/e")); + assertMatches(Sets.of("d/e/f/g", "d/e/f/g.x"), fakeWalkAtSubtree("d")); + + assertNoMatches(fakeWalk("b")); + assertNoMatches(fakeWalk("c/d")); + assertNoMatches(fakeWalk("c")); + assertNoMatches(fakeWalk("d/e/f")); + assertNoMatches(fakeWalk("d/e")); + assertNoMatches(fakeWalk("d")); + } @Test public void testFilterIsPrefixOfKey() throws MissingObjectException, IncorrectObjectTypeException, IOException { - assertTrue(filter.include(fakeWalk("a/b"))); - assertTrue(filter.include(fakeWalk("b/c/d"))); - assertTrue(filter.include(fakeWalk("c/d/e/f"))); - assertTrue(filter.include(fakeWalk("c/d/f/g"))); - assertTrue(filter.include(fakeWalk("d/e/f/g/h"))); - assertTrue(filter.include(fakeWalk("d/e/f/g/y"))); - assertTrue(filter.include(fakeWalk("d/e/f/g.x/h"))); - // listed before g/y, so can't StopWalk here, but it's not included - // either - assertFalse(filter.include(fakeWalk("d/e/f/g.y"))); + assertMatches(Sets.of("a"), fakeWalk("a/b")); + assertMatches(Sets.of("b/c"), fakeWalk("b/c/d")); + assertMatches(Sets.of("c/d/e"), fakeWalk("c/d/e/f")); + assertMatches(Sets.of("c/d/f"), fakeWalk("c/d/f/g")); + assertMatches(Sets.of("d/e/f/g"), fakeWalk("d/e/f/g/h")); + assertMatches(Sets.of("d/e/f/g"), fakeWalk("d/e/f/g/y")); + assertMatches(Sets.of("d/e/f/g.x"), fakeWalk("d/e/f/g.x/h")); } @Test @@ -182,6 +203,10 @@ public class PathFilterGroupTest { // less obvious #2 due to git sorting order filter.include(fakeWalk("d/e/f/g/h.txt")); + // listed before g/y, so can't StopWalk here + filter.include(fakeWalk("d/e/f/g.y")); + singles.get("d/e/f/g").include(fakeWalk("d/e/f/g.y")); + // non-ascii try { filter.include(fakeWalk("\u00C0")); @@ -191,6 +216,44 @@ public class PathFilterGroupTest { } } + private void assertNoMatches(TreeWalk tw) throws MissingObjectException, + IncorrectObjectTypeException, IOException { + assertMatches(Sets. of(), tw); + } + + private void assertMatches(Set expect, TreeWalk tw) + throws MissingObjectException, IncorrectObjectTypeException, + IOException { + List actual = new ArrayList<>(); + for (String path : singles.keySet()) { + if (includes(singles.get(path), tw)) { + actual.add(path); + } + } + + String[] e = expect.toArray(new String[expect.size()]); + String[] a = actual.toArray(new String[actual.size()]); + Arrays.sort(e); + Arrays.sort(a); + assertArrayEquals(e, a); + + if (expect.isEmpty()) { + assertFalse(includes(filter, tw)); + } else { + assertTrue(includes(filter, tw)); + } + } + + private static boolean includes(TreeFilter f, TreeWalk tw) + throws MissingObjectException, IncorrectObjectTypeException, + IOException { + try { + return f.include(tw); + } catch (StopWalkException e) { + return false; + } + } + TreeWalk fakeWalk(final String path) throws IOException { DirCache dc = DirCache.newInCore(); DirCacheEditor dce = dc.editor(); @@ -210,4 +273,25 @@ public class PathFilterGroupTest { return ret; } + TreeWalk fakeWalkAtSubtree(final String path) throws IOException { + DirCache dc = DirCache.newInCore(); + DirCacheEditor dce = dc.editor(); + dce.add(new DirCacheEditor.PathEdit(path + "/README") { + public void apply(DirCacheEntry ent) { + ent.setFileMode(FileMode.REGULAR_FILE); + } + }); + dce.finish(); + + TreeWalk ret = new TreeWalk((ObjectReader) null); + ret.addTree(new DirCacheIterator(dc)); + ret.next(); + while (!path.equals(ret.getPathString())) { + if (ret.isSubtree()) { + ret.enterSubtree(); + } + ret.next(); + } + return ret; + } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/ChangeIdUtilTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/ChangeIdUtilTest.java index 7273cdbab..aaeb79c64 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/ChangeIdUtilTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/ChangeIdUtilTest.java @@ -45,7 +45,6 @@ package org.eclipse.jgit.util; import static org.junit.Assert.assertEquals; -import java.io.IOException; import java.util.concurrent.TimeUnit; import org.eclipse.jgit.junit.MockSystemReader; @@ -113,7 +112,7 @@ public class ChangeIdUtilTest { } @Test - public void testId() throws IOException { + public void testId() { String msg = "A\nMessage\n"; ObjectId id = ChangeIdUtil.computeChangeId(treeId, parentId, p, q, msg); assertEquals("73f3751208ac92cbb76f9a26ac4a0d9d472e381b", ObjectId diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FileUtilTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FileUtilTest.java index 0d7d31b3a..1f78e0208 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FileUtilTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FileUtilTest.java @@ -54,6 +54,7 @@ import java.util.regex.Matcher; import org.eclipse.jgit.junit.JGitTestUtil; import org.junit.After; +import org.junit.Assume; import org.junit.Before; import org.junit.Test; @@ -424,18 +425,27 @@ public class FileUtilTest { @Test public void testCreateSymlink() throws IOException { FS fs = FS.DETECTED; - try { - fs.createSymLink(new File(trash, "x"), "y"); - } catch (IOException e) { - if (fs.supportsSymlinks()) - fail("FS claims to support symlinks but attempt to create symlink failed"); - return; - } - assertTrue(fs.supportsSymlinks()); + // show test as ignored if the FS doesn't support symlinks + Assume.assumeTrue(fs.supportsSymlinks()); + fs.createSymLink(new File(trash, "x"), "y"); String target = fs.readSymLink(new File(trash, "x")); assertEquals("y", target); } + @Test + public void testCreateSymlinkOverrideExisting() throws IOException { + FS fs = FS.DETECTED; + // show test as ignored if the FS doesn't support symlinks + Assume.assumeTrue(fs.supportsSymlinks()); + File file = new File(trash, "x"); + fs.createSymLink(file, "y"); + String target = fs.readSymLink(file); + assertEquals("y", target); + fs.createSymLink(file, "z"); + target = fs.readSymLink(file); + assertEquals("z", target); + } + @Test public void testRelativize_doc() { // This is the javadoc example diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/PathsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/PathsTest.java new file mode 100644 index 000000000..7542ec891 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/PathsTest.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2016, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.util; + +import static org.eclipse.jgit.util.Paths.compare; +import static org.eclipse.jgit.util.Paths.compareSameName; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.FileMode; +import org.junit.Test; + +public class PathsTest { + @Test + public void testStripTrailingSeparator() { + assertNull(Paths.stripTrailingSeparator(null)); + assertEquals("", Paths.stripTrailingSeparator("")); + assertEquals("a", Paths.stripTrailingSeparator("a")); + assertEquals("a/boo", Paths.stripTrailingSeparator("a/boo")); + assertEquals("a/boo", Paths.stripTrailingSeparator("a/boo/")); + assertEquals("a/boo", Paths.stripTrailingSeparator("a/boo//")); + assertEquals("a/boo", Paths.stripTrailingSeparator("a/boo///")); + } + + @Test + public void testPathCompare() { + byte[] a = Constants.encode("afoo/bar.c"); + byte[] b = Constants.encode("bfoo/bar.c"); + + assertEquals(0, compare(a, 1, a.length, 0, b, 1, b.length, 0)); + assertEquals(-1, compare(a, 0, a.length, 0, b, 0, b.length, 0)); + assertEquals(1, compare(b, 0, b.length, 0, a, 0, a.length, 0)); + + a = Constants.encode("a"); + b = Constants.encode("aa"); + assertEquals(-97, compare(a, 0, a.length, 0, b, 0, b.length, 0)); + assertEquals(0, compare(a, 0, a.length, 0, b, 0, 1, 0)); + assertEquals(0, compare(a, 0, a.length, 0, b, 1, 2, 0)); + assertEquals(0, compareSameName(a, 0, a.length, b, 1, b.length, 0)); + assertEquals(0, compareSameName(a, 0, a.length, b, 0, 1, 0)); + assertEquals(-50, compareSameName(a, 0, a.length, b, 0, b.length, 0)); + assertEquals(97, compareSameName(b, 0, b.length, a, 0, a.length, 0)); + + a = Constants.encode("a"); + b = Constants.encode("a"); + assertEquals(0, compare( + a, 0, a.length, FileMode.TREE.getBits(), + b, 0, b.length, FileMode.TREE.getBits())); + assertEquals(0, compare( + a, 0, a.length, FileMode.REGULAR_FILE.getBits(), + b, 0, b.length, FileMode.REGULAR_FILE.getBits())); + assertEquals(-47, compare( + a, 0, a.length, FileMode.REGULAR_FILE.getBits(), + b, 0, b.length, FileMode.TREE.getBits())); + assertEquals(47, compare( + a, 0, a.length, FileMode.TREE.getBits(), + b, 0, b.length, FileMode.REGULAR_FILE.getBits())); + + assertEquals(0, compareSameName( + a, 0, a.length, + b, 0, b.length, FileMode.TREE.getBits())); + assertEquals(0, compareSameName( + a, 0, a.length, + b, 0, b.length, FileMode.REGULAR_FILE.getBits())); + + a = Constants.encode("a.c"); + b = Constants.encode("a"); + byte[] c = Constants.encode("a0c"); + assertEquals(-1, compare( + a, 0, a.length, FileMode.REGULAR_FILE.getBits(), + b, 0, b.length, FileMode.TREE.getBits())); + assertEquals(-1, compare( + b, 0, b.length, FileMode.TREE.getBits(), + c, 0, c.length, FileMode.REGULAR_FILE.getBits())); + } +} diff --git a/org.eclipse.jgit.ui/BUCK b/org.eclipse.jgit.ui/BUCK new file mode 100644 index 000000000..fcd87cf9a --- /dev/null +++ b/org.eclipse.jgit.ui/BUCK @@ -0,0 +1,7 @@ +java_library( + name = 'ui', + srcs = glob(['src/**']), + resources = glob(['resources/**']), + deps = ['//org.eclipse.jgit:jgit'], + visibility = ['PUBLIC'], +) diff --git a/org.eclipse.jgit.ui/src/org/eclipse/jgit/awtui/AwtCredentialsProvider.java b/org.eclipse.jgit.ui/src/org/eclipse/jgit/awtui/AwtCredentialsProvider.java index fd26bfa7f..a9967ae49 100644 --- a/org.eclipse.jgit.ui/src/org/eclipse/jgit/awtui/AwtCredentialsProvider.java +++ b/org.eclipse.jgit.ui/src/org/eclipse/jgit/awtui/AwtCredentialsProvider.java @@ -56,15 +56,20 @@ import javax.swing.JPasswordField; import javax.swing.JTextField; import org.eclipse.jgit.errors.UnsupportedCredentialItem; +import org.eclipse.jgit.transport.ChainingCredentialsProvider; import org.eclipse.jgit.transport.CredentialItem; import org.eclipse.jgit.transport.CredentialsProvider; +import org.eclipse.jgit.transport.NetRCCredentialsProvider; import org.eclipse.jgit.transport.URIish; /** Interacts with the user during authentication by using AWT/Swing dialogs. */ public class AwtCredentialsProvider extends CredentialsProvider { /** Install this implementation as the default. */ public static void install() { - CredentialsProvider.setDefault(new AwtCredentialsProvider()); + final AwtCredentialsProvider c = new AwtCredentialsProvider(); + CredentialsProvider cp = new ChainingCredentialsProvider( + new NetRCCredentialsProvider(), c); + CredentialsProvider.setDefault(cp); } @Override diff --git a/org.eclipse.jgit/.settings/.api_filters b/org.eclipse.jgit/.settings/.api_filters index b2a8f677f..36041f814 100644 --- a/org.eclipse.jgit/.settings/.api_filters +++ b/org.eclipse.jgit/.settings/.api_filters @@ -1,5 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/org.eclipse.jgit/BUCK b/org.eclipse.jgit/BUCK new file mode 100644 index 000000000..73e208057 --- /dev/null +++ b/org.eclipse.jgit/BUCK @@ -0,0 +1,20 @@ +SRCS = glob(['src/**']) +RESOURCES = glob(['resources/**']) + +java_library( + name = 'jgit', + srcs = SRCS, + resources = RESOURCES, + deps = [ + '//lib:javaewah', + '//lib:jsch', + '//lib:httpcomponents', + '//lib:slf4j-api', + ], + visibility = ['PUBLIC'], +) + +java_sources( + name = 'jgit_src', + srcs = SRCS + RESOURCES, +) diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF index 2a953b559..25d0be6ec 100644 --- a/org.eclipse.jgit/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit/META-INF/MANIFEST.MF @@ -65,9 +65,10 @@ Export-Package: org.eclipse.jgit.annotations;version="4.2.0", org.eclipse.jgit.junit, org.eclipse.jgit.junit.http, org.eclipse.jgit.http.server, - org.eclipse.jgit.java7.test, + org.eclipse.jgit.pgm.test, org.eclipse.jgit.pgm", org.eclipse.jgit.internal.storage.pack;version="4.2.0";x-friends:="org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm", + org.eclipse.jgit.internal.storage.reftree;version="4.2.0";x-friends:="org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm", org.eclipse.jgit.lib;version="4.2.0"; uses:="org.eclipse.jgit.revwalk, org.eclipse.jgit.treewalk.filter, diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties index 0e9b0b59e..992e10bad 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -99,6 +99,7 @@ cannotSquashFixupWithoutPreviousCommit=Cannot {0} without previous commit. cannotStoreObjects=cannot store objects cannotResolveUniquelyAbbrevObjectId=Could not resolve uniquely the abbreviated object ID cannotUnloadAModifiedTree=Cannot unload a modified tree. +cannotUpdateUnbornBranch=Cannot update unborn branch cannotWorkWithOtherStagesThanZeroRightNow=Cannot work with other stages than zero right now. Won't write corrupt index. cannotWriteObjectsPath=Cannot write {0}/{1}: {2} canOnlyCherryPickCommitsWithOneParent=Cannot cherry-pick commit ''{0}'' because it has {1} parents, only commits with exactly one parent are supported. @@ -125,14 +126,15 @@ connectionFailed=connection failed connectionTimeOut=Connection time out: {0} contextMustBeNonNegative=context must be >= 0 corruptionDetectedReReadingAt=Corruption detected re-reading at {0} +corruptObjectBadDate=bad date +corruptObjectBadEmail=bad email corruptObjectBadStream=bad stream corruptObjectBadStreamCorruptHeader=bad stream, corrupt header +corruptObjectBadTimezone=bad time zone corruptObjectDuplicateEntryNames=duplicate entry names corruptObjectGarbageAfterSize=garbage after size corruptObjectIncorrectLength=incorrect length corruptObjectIncorrectSorting=incorrectly sorted -corruptObjectInvalidAuthor=invalid author -corruptObjectInvalidCommitter=invalid committer corruptObjectInvalidEntryMode=invalid entry mode corruptObjectInvalidMode=invalid mode corruptObjectInvalidModeChar=invalid mode character @@ -151,11 +153,11 @@ corruptObjectInvalidNameNul=invalid name 'NUL' corruptObjectInvalidNamePrn=invalid name 'PRN' corruptObjectInvalidObject=invalid object corruptObjectInvalidParent=invalid parent -corruptObjectInvalidTagger=invalid tagger corruptObjectInvalidTree=invalid tree corruptObjectInvalidType=invalid type corruptObjectInvalidType2=invalid type {0} corruptObjectMalformedHeader=malformed header: {0} +corruptObjectMissingEmail=missing email corruptObjectNameContainsByte=name contains byte 0x%x corruptObjectNameContainsChar=name contains '%c' corruptObjectNameContainsNullByte=name contains byte 0x00 @@ -181,6 +183,7 @@ corruptObjectPackfileChecksumIncorrect=Packfile checksum incorrect. corruptObjectTruncatedInMode=truncated in mode corruptObjectTruncatedInName=truncated in name corruptObjectTruncatedInObjectId=truncated in object id +corruptObjectZeroId=entry points to null SHA-1 couldNotCheckOutBecauseOfConflicts=Could not check out because of conflicts couldNotDeleteLockFileShouldNotHappen=Could not delete lock file. Should not happen couldNotDeleteTemporaryIndexFileShouldNotHappen=Could not delete temporary index file. Should not happen @@ -432,6 +435,7 @@ noXMLParserAvailable=No XML parser available. objectAtHasBadZlibStream=Object at {0} in {1} has bad zlib stream objectAtPathDoesNotHaveId=Object at path "{0}" does not have an id assigned. All object ids must be assigned prior to writing a tree. objectIsCorrupt=Object {0} is corrupt: {1} +objectIsCorrupt3={0}: object {1}: {2} objectIsNotA=Object {0} is not a {1}. objectNotFound=Object {0} not found. objectNotFoundIn=Object {0} not found in {1}. @@ -595,6 +599,7 @@ transportExceptionInvalid=Invalid {0} {1}:{2} transportExceptionMissingAssumed=Missing assumed {0} transportExceptionReadRef=read {0} transportNeedsRepository=Transport needs repository +transportProvidedRefWithNoObjectId=Transport provided ref {0} with no object id transportProtoAmazonS3=Amazon S3 transportProtoBundleFile=Git Bundle File transportProtoFTP=FTP diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java index 67fb342fe..3b94f16f1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java @@ -43,6 +43,10 @@ */ package org.eclipse.jgit.api; +import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; +import static org.eclipse.jgit.lib.FileMode.GITLINK; +import static org.eclipse.jgit.lib.FileMode.TYPE_TREE; + import java.io.IOException; import java.io.InputStream; import java.util.Collection; @@ -58,12 +62,12 @@ import org.eclipse.jgit.dircache.DirCacheBuilder; import org.eclipse.jgit.dircache.DirCacheEntry; import org.eclipse.jgit.dircache.DirCacheIterator; import org.eclipse.jgit.internal.JGitText; -import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.treewalk.FileTreeIterator; -import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.NameConflictTreeWalk; import org.eclipse.jgit.treewalk.TreeWalk.OperationType; import org.eclipse.jgit.treewalk.WorkingTreeIterator; import org.eclipse.jgit.treewalk.filter.PathFilterGroup; @@ -135,15 +139,12 @@ public class AddCommand extends GitCommand { throw new NoFilepatternException(JGitText.get().atLeastOnePatternIsRequired); checkCallable(); DirCache dc = null; - boolean addAll = false; - if (filepatterns.contains(".")) //$NON-NLS-1$ - addAll = true; + boolean addAll = filepatterns.contains("."); //$NON-NLS-1$ try (ObjectInserter inserter = repo.newObjectInserter(); - final TreeWalk tw = new TreeWalk(repo)) { + NameConflictTreeWalk tw = new NameConflictTreeWalk(repo)) { tw.setOperationType(OperationType.CHECKIN_OP); dc = repo.lockDirCache(); - DirCacheIterator c; DirCacheBuilder builder = dc.builder(); tw.addTree(new DirCacheBuildIterator(builder)); @@ -151,62 +152,85 @@ public class AddCommand extends GitCommand { workingTreeIterator = new FileTreeIterator(repo); workingTreeIterator.setDirCacheIterator(tw, 0); tw.addTree(workingTreeIterator); - tw.setRecursive(true); if (!addAll) tw.setFilter(PathFilterGroup.createFromStrings(filepatterns)); - String lastAddedFile = null; + byte[] lastAdded = null; while (tw.next()) { - String path = tw.getPathString(); - + DirCacheIterator c = tw.getTree(0, DirCacheIterator.class); WorkingTreeIterator f = tw.getTree(1, WorkingTreeIterator.class); - if (tw.getTree(0, DirCacheIterator.class) == null && - f != null && f.isEntryIgnored()) { + if (c == null && f != null && f.isEntryIgnored()) { // file is not in index but is ignored, do nothing + continue; + } else if (c == null && update) { + // Only update of existing entries was requested. + continue; + } + + DirCacheEntry entry = c != null ? c.getDirCacheEntry() : null; + if (entry != null && entry.getStage() > 0 + && lastAdded != null + && lastAdded.length == tw.getPathLength() + && tw.isPathPrefix(lastAdded, lastAdded.length) == 0) { + // In case of an existing merge conflict the + // DirCacheBuildIterator iterates over all stages of + // this path, we however want to add only one + // new DirCacheEntry per path. + continue; + } + + if (tw.isSubtree() && !tw.isDirectoryFileConflict()) { + tw.enterSubtree(); + continue; + } + + if (f == null) { // working tree file does not exist + if (entry != null + && (!update || GITLINK == entry.getFileMode())) { + builder.add(entry); + } + continue; + } + + if (entry != null && entry.isAssumeValid()) { + // Index entry is marked assume valid. Even though + // the user specified the file to be added JGit does + // not consider the file for addition. + builder.add(entry); + continue; + } + + if (f.getEntryRawMode() == TYPE_TREE) { + // Index entry exists and is symlink, gitlink or file, + // otherwise the tree would have been entered above. + // Replace the index entry by diving into tree of files. + tw.enterSubtree(); + continue; + } + + byte[] path = tw.getRawPath(); + if (entry == null || entry.getStage() > 0) { + entry = new DirCacheEntry(path); } - // In case of an existing merge conflict the - // DirCacheBuildIterator iterates over all stages of - // this path, we however want to add only one - // new DirCacheEntry per path. - else if (!(path.equals(lastAddedFile))) { - if (!(update && tw.getTree(0, DirCacheIterator.class) == null)) { - c = tw.getTree(0, DirCacheIterator.class); - if (f != null) { // the file exists - long sz = f.getEntryLength(); - DirCacheEntry entry = new DirCacheEntry(path); - if (c == null || c.getDirCacheEntry() == null - || !c.getDirCacheEntry().isAssumeValid()) { - FileMode mode = f.getIndexFileMode(c); - entry.setFileMode(mode); - - if (FileMode.GITLINK != mode) { - entry.setLength(sz); - entry.setLastModified(f - .getEntryLastModified()); - long contentSize = f - .getEntryContentLength(); - InputStream in = f.openEntryStream(); - try { - entry.setObjectId(inserter.insert( - Constants.OBJ_BLOB, contentSize, in)); - } finally { - in.close(); - } - } else - entry.setObjectId(f.getEntryObjectId()); - builder.add(entry); - lastAddedFile = path; - } else { - builder.add(c.getDirCacheEntry()); - } - - } else if (c != null - && (!update || FileMode.GITLINK == c - .getEntryFileMode())) - builder.add(c.getDirCacheEntry()); + FileMode mode = f.getIndexFileMode(c); + entry.setFileMode(mode); + + if (GITLINK != mode) { + entry.setLength(f.getEntryLength()); + entry.setLastModified(f.getEntryLastModified()); + long len = f.getEntryContentLength(); + try (InputStream in = f.openEntryStream()) { + ObjectId id = inserter.insert(OBJ_BLOB, len, in); + entry.setObjectId(id); } + } else { + entry.setLength(0); + entry.setLastModified(0); + entry.setObjectId(f.getEntryObjectId()); } + builder.add(entry); + lastAdded = path; } inserter.flush(); builder.commit(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java index 6a945e4d3..676ae0300 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java @@ -47,6 +47,7 @@ import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; +import java.nio.file.StandardCopyOption; import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; @@ -141,9 +142,13 @@ public class ApplyCommand extends GitCommand { case RENAME: f = getFile(fh.getOldPath(), false); File dest = getFile(fh.getNewPath(), false); - if (!f.renameTo(dest)) + try { + FileUtils.rename(f, dest, + StandardCopyOption.ATOMIC_MOVE); + } catch (IOException e) { throw new PatchApplyException(MessageFormat.format( - JGitText.get().renameFileFailed, f, dest)); + JGitText.get().renameFileFailed, f, dest), e); + } break; case COPY: f = getFile(fh.getOldPath(), false); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java index 8743ea9ac..4f918fa35 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java @@ -331,9 +331,16 @@ public class CheckoutCommand extends GitCommand { } private String getShortBranchName(Ref headRef) { - if (headRef.getTarget().getName().equals(headRef.getName())) - return headRef.getTarget().getObjectId().getName(); - return Repository.shortenRefName(headRef.getTarget().getName()); + if (headRef.isSymbolic()) { + return Repository.shortenRefName(headRef.getTarget().getName()); + } + // Detached HEAD. Every non-symbolic ref in the ref database has an + // object id, so this cannot be null. + ObjectId id = headRef.getObjectId(); + if (id == null) { + throw new NullPointerException(); + } + return id.getName(); } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java index b3bc319ae..2ac872950 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java @@ -61,6 +61,7 @@ import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.NullProgressMonitor; +import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefUpdate; @@ -235,7 +236,7 @@ public class CloneCommand extends TransportCommand { } if (head == null || head.getObjectId() == null) - return; // throw exception? + return; // TODO throw exception? if (head.getName().startsWith(Constants.R_HEADS)) { final RefUpdate newHead = clonedRepo.updateRef(Constants.HEAD); @@ -287,20 +288,24 @@ public class CloneCommand extends TransportCommand { private Ref findBranchToCheckout(FetchResult result) { final Ref idHEAD = result.getAdvertisedRef(Constants.HEAD); - if (idHEAD == null) + ObjectId headId = idHEAD != null ? idHEAD.getObjectId() : null; + if (headId == null) { return null; + } Ref master = result.getAdvertisedRef(Constants.R_HEADS + Constants.MASTER); - if (master != null && master.getObjectId().equals(idHEAD.getObjectId())) + ObjectId objectId = master != null ? master.getObjectId() : null; + if (headId.equals(objectId)) { return master; + } Ref foundBranch = null; for (final Ref r : result.getAdvertisedRefs()) { final String n = r.getName(); if (!n.startsWith(Constants.R_HEADS)) continue; - if (r.getObjectId().equals(idHEAD.getObjectId())) { + if (headId.equals(r.getObjectId())) { foundBranch = r; break; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java index 6828ed338..b5057ad28 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java @@ -53,6 +53,7 @@ import java.util.List; import org.eclipse.jgit.api.errors.AbortedByHookException; import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; +import org.eclipse.jgit.api.errors.EmtpyCommitException; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.api.errors.NoFilepatternException; @@ -130,6 +131,8 @@ public class CommitCommand extends GitCommand { private PrintStream hookOutRedirect; + private Boolean allowEmpty; + /** * @param repo */ @@ -231,6 +234,16 @@ public class CommitCommand extends GitCommand { if (insertChangeId) insertChangeId(indexTreeId); + // Check for empty commits + if (headId != null && !allowEmpty.booleanValue()) { + RevCommit headCommit = rw.parseCommit(headId); + headCommit.getTree(); + if (indexTreeId.equals(headCommit.getTree())) { + throw new EmtpyCommitException( + JGitText.get().emptyCommit); + } + } + // Create a Commit object, populate it and write it CommitBuilder commit = new CommitBuilder(); commit.setCommitter(committer); @@ -457,6 +470,8 @@ public class CommitCommand extends GitCommand { // there must be at least one change if (emptyCommit) + // Would like to throw a EmptyCommitException. But this would break the API + // TODO(ch): Change this in the next release throw new JGitInternalException(JGitText.get().emptyCommit); // update index @@ -510,6 +525,12 @@ public class CommitCommand extends GitCommand { committer = new PersonIdent(repo); if (author == null && !amend) author = committer; + if (allowEmpty == null) + // JGit allows empty commits by default. Only when pathes are + // specified the commit should not be empty. This behaviour differs + // from native git but can only be adapted in the next release. + // TODO(ch) align the defaults with native git + allowEmpty = (only.isEmpty()) ? Boolean.TRUE : Boolean.FALSE; // when doing a merge commit parse MERGE_HEAD and MERGE_MSG files if (state == RepositoryState.MERGING_RESOLVED @@ -578,6 +599,27 @@ public class CommitCommand extends GitCommand { return this; } + /** + * @param allowEmpty + * whether it should be allowed to create a commit which has the + * same tree as it's sole predecessor (a commit which doesn't + * change anything). By default when creating standard commits + * (without specifying paths) JGit allows to create such commits. + * When this flag is set to false an attempt to create an "empty" + * standard commit will lead to an EmptyCommitException. + *

+ * By default when creating a commit containing only specified + * paths an attempt to create an empty commit leads to a + * {@link JGitInternalException}. By setting this flag to + * true this exception will not be thrown. + * @return {@code this} + * @since 4.2 + */ + public CommitCommand setAllowEmpty(boolean allowEmpty) { + this.allowEmpty = Boolean.valueOf(allowEmpty); + return this; + } + /** * @return the commit message used for the commit */ @@ -681,7 +723,7 @@ public class CommitCommand extends GitCommand { */ public CommitCommand setAll(boolean all) { checkCallable(); - if (!only.isEmpty()) + if (all && !only.isEmpty()) throw new JGitInternalException(MessageFormat.format( JGitText.get().illegalCombinationOfArguments, "--all", //$NON-NLS-1$ "--only")); //$NON-NLS-1$ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java index 9620089b0..de512761a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java @@ -116,22 +116,17 @@ public class FetchCommand extends TransportCommand { org.eclipse.jgit.api.errors.TransportException { checkCallable(); - try { - Transport transport = Transport.open(repo, remote); - try { - transport.setCheckFetchedObjects(checkFetchedObjects); - transport.setRemoveDeletedRefs(isRemoveDeletedRefs()); - transport.setDryRun(dryRun); - if (tagOption != null) - transport.setTagOpt(tagOption); - transport.setFetchThin(thin); - configure(transport); - - FetchResult result = transport.fetch(monitor, refSpecs); - return result; - } finally { - transport.close(); - } + try (Transport transport = Transport.open(repo, remote)) { + transport.setCheckFetchedObjects(checkFetchedObjects); + transport.setRemoveDeletedRefs(isRemoveDeletedRefs()); + transport.setDryRun(dryRun); + if (tagOption != null) + transport.setTagOpt(tagOption); + transport.setFetchThin(thin); + configure(transport); + + FetchResult result = transport.fetch(monitor, refSpecs); + return result; } catch (NoRemoteRepositoryException e) { throw new InvalidRemoteException(MessageFormat.format( JGitText.get().invalidRemote, remote), e); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/LsRemoteCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/LsRemoteCommand.java index 3363a0fc8..f3527fd80 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/LsRemoteCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/LsRemoteCommand.java @@ -182,13 +182,9 @@ public class LsRemoteCommand extends org.eclipse.jgit.api.errors.TransportException { checkCallable(); - Transport transport = null; - FetchConnection fc = null; - try { - if (repo != null) - transport = Transport.open(repo, remote); - else - transport = Transport.open(new URIish(remote)); + try (Transport transport = repo != null + ? Transport.open(repo, remote) + : Transport.open(new URIish(remote))) { transport.setOptionUploadPack(uploadPack); configure(transport); Collection refSpecs = new ArrayList(1); @@ -199,19 +195,20 @@ public class LsRemoteCommand extends refSpecs.add(new RefSpec("refs/heads/*:refs/remotes/origin/*")); //$NON-NLS-1$ Collection refs; Map refmap = new HashMap(); - fc = transport.openFetch(); - refs = fc.getRefs(); - if (refSpecs.isEmpty()) - for (Ref r : refs) - refmap.put(r.getName(), r); - else - for (Ref r : refs) - for (RefSpec rs : refSpecs) - if (rs.matchSource(r)) { - refmap.put(r.getName(), r); - break; - } - return refmap; + try (FetchConnection fc = transport.openFetch()) { + refs = fc.getRefs(); + if (refSpecs.isEmpty()) + for (Ref r : refs) + refmap.put(r.getName(), r); + else + for (Ref r : refs) + for (RefSpec rs : refSpecs) + if (rs.matchSource(r)) { + refmap.put(r.getName(), r); + break; + } + return refmap; + } } catch (URISyntaxException e) { throw new InvalidRemoteException(MessageFormat.format( JGitText.get().invalidRemote, remote)); @@ -223,11 +220,6 @@ public class LsRemoteCommand extends throw new org.eclipse.jgit.api.errors.TransportException( e.getMessage(), e); - } finally { - if (fc != null) - fc.close(); - if (transport != null) - transport.close(); } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java index 8582bbb0d..e3e76c95f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java @@ -560,6 +560,8 @@ public class RebaseCommand extends GitCommand { lastStepWasForward = newHead != null; if (!lastStepWasForward) { ObjectId headId = getHead().getObjectId(); + // getHead() checks for null + assert headId != null; if (!AnyObjectId.equals(headId, newParents.get(0))) checkoutCommit(headId.getName(), newParents.get(0)); @@ -674,6 +676,8 @@ public class RebaseCommand extends GitCommand { return; ObjectId headId = getHead().getObjectId(); + // getHead() checks for null + assert headId != null; String head = headId.getName(); String currentCommits = rebaseState.readFile(CURRENT_COMMIT); for (String current : currentCommits.split("\n")) //$NON-NLS-1$ @@ -1073,11 +1077,12 @@ public class RebaseCommand extends GitCommand { Ref head = getHead(); - String headName = getHeadName(head); ObjectId headId = head.getObjectId(); - if (headId == null) + if (headId == null) { throw new RefNotFoundException(MessageFormat.format( JGitText.get().refNotResolved, Constants.HEAD)); + } + String headName = getHeadName(head); RevCommit headCommit = walk.lookupCommit(headId); RevCommit upstream = walk.lookupCommit(upstreamCommit.getId()); @@ -1188,10 +1193,14 @@ public class RebaseCommand extends GitCommand { private static String getHeadName(Ref head) { String headName; - if (head.isSymbolic()) + if (head.isSymbolic()) { headName = head.getTarget().getName(); - else - headName = head.getObjectId().getName(); + } else { + ObjectId headId = head.getObjectId(); + // the callers are checking this already + assert headId != null; + headName = headId.getName(); + } return headName; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java index 8f4bc4f26..4c91e6c17 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java @@ -190,10 +190,8 @@ public class ResetCommand extends GitCommand { ObjectId origHead = ru.getOldObjectId(); if (origHead != null) repo.writeOrigHead(origHead); - result = ru.getRef(); - } else { - result = repo.getRef(Constants.HEAD); } + result = repo.exactRef(Constants.HEAD); if (mode == null) mode = ResetType.MIXED; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashDropCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashDropCommand.java index 7923fd49b..f6903be05 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashDropCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashDropCommand.java @@ -46,6 +46,7 @@ import static org.eclipse.jgit.lib.Constants.R_STASH; import java.io.File; import java.io.IOException; +import java.nio.file.StandardCopyOption; import java.text.MessageFormat; import java.util.List; @@ -220,12 +221,14 @@ public class StashDropCommand extends GitCommand { entry.getWho(), entry.getComment()); entryId = entry.getNewId(); } - if (!stashLockFile.renameTo(stashFile)) { - FileUtils.delete(stashFile); - if (!stashLockFile.renameTo(stashFile)) + try { + FileUtils.rename(stashLockFile, stashFile, + StandardCopyOption.ATOMIC_MOVE); + } catch (IOException e) { throw new JGitInternalException(MessageFormat.format( JGitText.get().renameFileFailed, - stashLockFile.getPath(), stashFile.getPath())); + stashLockFile.getPath(), stashFile.getPath()), + e); } } catch (IOException e) { throw new JGitInternalException(JGitText.get().stashDropFailed, e); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/TransportCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/TransportCommand.java index 1aeb6109e..3d2e46b26 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/TransportCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/TransportCommand.java @@ -79,6 +79,7 @@ public abstract class TransportCommand extends */ protected TransportCommand(final Repository repo) { super(repo); + setCredentialsProvider(CredentialsProvider.getDefault()); } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/EmtpyCommitException.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/EmtpyCommitException.java new file mode 100644 index 000000000..b3cc1bfcf --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/EmtpyCommitException.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2015, Christian Halstrick + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v1.0 which accompanies this + * distribution, is reproduced below, and is available at + * http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.api.errors; + +/** + * Exception thrown when a newly created commit does not contain any changes + * + * @since 4.2 + */ +public class EmtpyCommitException extends GitAPIException { + private static final long serialVersionUID = 1L; + + /** + * @param message + * @param cause + */ + public EmtpyCommitException(String message, Throwable cause) { + super(message, cause); + } + + /** + * @param message + */ + public EmtpyCommitException(String message) { + super(message); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/BaseDirCacheEditor.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/BaseDirCacheEditor.java index 70f80aeb7..0fbc1f8ac 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/BaseDirCacheEditor.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/BaseDirCacheEditor.java @@ -44,8 +44,13 @@ package org.eclipse.jgit.dircache; +import static org.eclipse.jgit.lib.FileMode.TYPE_TREE; +import static org.eclipse.jgit.util.Paths.compareSameName; + import java.io.IOException; +import org.eclipse.jgit.errors.DirCacheNameConflictException; + /** * Generic update/editing support for {@link DirCache}. *

@@ -168,6 +173,7 @@ abstract class BaseDirCacheEditor { * {@link #finish()}, and only after {@link #entries} is sorted. */ protected void replace() { + checkNameConflicts(); if (entryCnt < entries.length / 2) { final DirCacheEntry[] n = new DirCacheEntry[entryCnt]; System.arraycopy(entries, 0, n, 0, entryCnt); @@ -176,6 +182,76 @@ abstract class BaseDirCacheEditor { cache.replace(entries, entryCnt); } + private void checkNameConflicts() { + int end = entryCnt - 1; + for (int eIdx = 0; eIdx < end; eIdx++) { + DirCacheEntry e = entries[eIdx]; + if (e.getStage() != 0) { + continue; + } + + byte[] ePath = e.path; + int prefixLen = lastSlash(ePath) + 1; + + for (int nIdx = eIdx + 1; nIdx < entryCnt; nIdx++) { + DirCacheEntry n = entries[nIdx]; + if (n.getStage() != 0) { + continue; + } + + byte[] nPath = n.path; + if (!startsWith(ePath, nPath, prefixLen)) { + // Different prefix; this entry is in another directory. + break; + } + + int s = nextSlash(nPath, prefixLen); + int m = s < nPath.length ? TYPE_TREE : n.getRawMode(); + int cmp = compareSameName( + ePath, prefixLen, ePath.length, + nPath, prefixLen, s, m); + if (cmp < 0) { + break; + } else if (cmp == 0) { + throw new DirCacheNameConflictException( + e.getPathString(), + n.getPathString()); + } + } + } + } + + private static int lastSlash(byte[] path) { + for (int i = path.length - 1; i >= 0; i--) { + if (path[i] == '/') { + return i; + } + } + return -1; + } + + private static int nextSlash(byte[] b, int p) { + final int n = b.length; + for (; p < n; p++) { + if (b[p] == '/') { + return p; + } + } + return n; + } + + private static boolean startsWith(byte[] a, byte[] b, int n) { + if (b.length < n) { + return false; + } + for (n--; n >= 0; n--) { + if (a[n] != b[n]) { + return false; + } + } + return true; + } + /** * Finish, write, commit this change, and release the index lock. *

diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java index fa0339544..ecdfe823a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java @@ -800,8 +800,11 @@ public class DirCache { * information. If < 0 the entry does not exist in the index. * @since 3.4 */ - public int findEntry(final byte[] p, final int pLen) { - int low = 0; + public int findEntry(byte[] p, int pLen) { + return findEntry(0, p, pLen); + } + + int findEntry(int low, byte[] p, int pLen) { int high = entryCnt; while (low < high) { int mid = (low + high) >>> 1; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheBuildIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheBuildIterator.java index da5530666..c10e41608 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheBuildIterator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheBuildIterator.java @@ -130,4 +130,9 @@ public class DirCacheBuildIterator extends DirCacheIterator { if (cur < cnt) builder.keep(cur, cnt - cur); } + + @Override + protected boolean needsStopWalk() { + return ptr < cache.getEntryCount(); + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java index 4eb688170..a1e1d15ac 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java @@ -46,6 +46,7 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.nio.file.StandardCopyOption; import java.text.MessageFormat; import java.util.ArrayList; import java.util.HashMap; @@ -1319,11 +1320,12 @@ public class DirCacheCheckout { if (deleteRecursive && f.isDirectory()) { FileUtils.delete(f, FileUtils.RECURSIVE); } - FileUtils.rename(tmpFile, f); + FileUtils.rename(tmpFile, f, StandardCopyOption.ATOMIC_MOVE); } catch (IOException e) { - throw new IOException(MessageFormat.format( - JGitText.get().renameFileFailed, tmpFile.getPath(), - f.getPath())); + throw new IOException( + MessageFormat.format(JGitText.get().renameFileFailed, + tmpFile.getPath(), f.getPath()), + e); } finally { if (tmpFile.exists()) { FileUtils.delete(tmpFile); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEditor.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEditor.java index 13885d370..c987c964c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEditor.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEditor.java @@ -44,6 +44,10 @@ package org.eclipse.jgit.dircache; +import static org.eclipse.jgit.dircache.DirCache.cmp; +import static org.eclipse.jgit.dircache.DirCacheTree.peq; +import static org.eclipse.jgit.lib.FileMode.TYPE_TREE; + import java.io.IOException; import java.text.MessageFormat; import java.util.ArrayList; @@ -53,6 +57,7 @@ import java.util.List; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.util.Paths; /** * Updates a {@link DirCache} by supplying discrete edit commands. @@ -72,11 +77,12 @@ public class DirCacheEditor extends BaseDirCacheEditor { public int compare(final PathEdit o1, final PathEdit o2) { final byte[] a = o1.path; final byte[] b = o2.path; - return DirCache.cmp(a, a.length, b, b.length); + return cmp(a, a.length, b, b.length); } }; private final List edits; + private int editIdx; /** * Construct a new editor. @@ -126,35 +132,44 @@ public class DirCacheEditor extends BaseDirCacheEditor { private void applyEdits() { Collections.sort(edits, EDIT_CMP); + editIdx = 0; final int maxIdx = cache.getEntryCount(); int lastIdx = 0; - for (final PathEdit e : edits) { - int eIdx = cache.findEntry(e.path, e.path.length); + while (editIdx < edits.size()) { + PathEdit e = edits.get(editIdx++); + int eIdx = cache.findEntry(lastIdx, e.path, e.path.length); final boolean missing = eIdx < 0; if (eIdx < 0) eIdx = -(eIdx + 1); final int cnt = Math.min(eIdx, maxIdx) - lastIdx; if (cnt > 0) fastKeep(lastIdx, cnt); - lastIdx = missing ? eIdx : cache.nextEntry(eIdx); - if (e instanceof DeletePath) + if (e instanceof DeletePath) { + lastIdx = missing ? eIdx : cache.nextEntry(eIdx); continue; + } if (e instanceof DeleteTree) { lastIdx = cache.nextEntry(e.path, e.path.length, eIdx); continue; } if (missing) { - final DirCacheEntry ent = new DirCacheEntry(e.path); + DirCacheEntry ent = new DirCacheEntry(e.path); e.apply(ent); - if (ent.getRawMode() == 0) - throw new IllegalArgumentException(MessageFormat.format(JGitText.get().fileModeNotSetForPath - , ent.getPathString())); + if (ent.getRawMode() == 0) { + throw new IllegalArgumentException(MessageFormat.format( + JGitText.get().fileModeNotSetForPath, + ent.getPathString())); + } + lastIdx = e.replace + ? deleteOverlappingSubtree(ent, eIdx) + : eIdx; fastAdd(ent); } else { // Apply to all entries of the current path (different stages) + lastIdx = cache.nextEntry(eIdx); for (int i = eIdx; i < lastIdx; i++) { final DirCacheEntry ent = cache.getEntry(i); e.apply(ent); @@ -168,6 +183,102 @@ public class DirCacheEditor extends BaseDirCacheEditor { fastKeep(lastIdx, cnt); } + private int deleteOverlappingSubtree(DirCacheEntry ent, int eIdx) { + byte[] entPath = ent.path; + int entLen = entPath.length; + + // Delete any file that was previously processed and overlaps + // the parent directory for the new entry. Since the editor + // always processes entries in path order, binary search back + // for the overlap for each parent directory. + for (int p = pdir(entPath, entLen); p > 0; p = pdir(entPath, p)) { + int i = findEntry(entPath, p); + if (i >= 0) { + // A file does overlap, delete the file from the array. + // No other parents can have overlaps as the file should + // have taken care of that itself. + int n = --entryCnt - i; + System.arraycopy(entries, i + 1, entries, i, n); + break; + } + + // If at least one other entry already exists in this parent + // directory there is no need to continue searching up the tree. + i = -(i + 1); + if (i < entryCnt && inDir(entries[i], entPath, p)) { + break; + } + } + + int maxEnt = cache.getEntryCount(); + if (eIdx >= maxEnt) { + return maxEnt; + } + + DirCacheEntry next = cache.getEntry(eIdx); + if (Paths.compare(next.path, 0, next.path.length, 0, + entPath, 0, entLen, TYPE_TREE) < 0) { + // Next DirCacheEntry sorts before new entry as tree. Defer a + // DeleteTree command to delete any entries if they exist. This + // case only happens for A, A.c, A/c type of conflicts (rare). + insertEdit(new DeleteTree(entPath)); + return eIdx; + } + + // Next entry may be contained by the entry-as-tree, skip if so. + while (eIdx < maxEnt && inDir(cache.getEntry(eIdx), entPath, entLen)) { + eIdx++; + } + return eIdx; + } + + private int findEntry(byte[] p, int pLen) { + int low = 0; + int high = entryCnt; + while (low < high) { + int mid = (low + high) >>> 1; + int cmp = cmp(p, pLen, entries[mid]); + if (cmp < 0) { + high = mid; + } else if (cmp == 0) { + while (mid > 0 && cmp(p, pLen, entries[mid - 1]) == 0) { + mid--; + } + return mid; + } else { + low = mid + 1; + } + } + return -(low + 1); + } + + private void insertEdit(DeleteTree d) { + for (int i = editIdx; i < edits.size(); i++) { + int cmp = EDIT_CMP.compare(d, edits.get(i)); + if (cmp < 0) { + edits.add(i, d); + return; + } else if (cmp == 0) { + return; + } + } + edits.add(d); + } + + private static boolean inDir(DirCacheEntry e, byte[] path, int pLen) { + return e.path.length > pLen && e.path[pLen] == '/' + && peq(path, e.path, pLen); + } + + private static int pdir(byte[] path, int e) { + for (e--; e > 0; e--) { + if (path[e] == '/') { + return e; + } + } + return 0; + } + /** * Any index record update. *

@@ -179,6 +290,7 @@ public class DirCacheEditor extends BaseDirCacheEditor { */ public abstract static class PathEdit { final byte[] path; + boolean replace = true; /** * Create a new update command by path name. @@ -190,6 +302,10 @@ public class DirCacheEditor extends BaseDirCacheEditor { path = Constants.encode(entryPath); } + PathEdit(byte[] path) { + this.path = path; + } + /** * Create a new update command for an existing entry instance. * @@ -201,6 +317,22 @@ public class DirCacheEditor extends BaseDirCacheEditor { path = ent.path; } + /** + * Configure if a file can replace a directory (or vice versa). + *

+ * Default is {@code true} as this is usually the desired behavior. + * + * @param ok + * if true a file can replace a directory, or a directory can + * replace a file. + * @return {@code this} + * @since 4.2 + */ + public PathEdit setReplace(boolean ok) { + replace = ok; + return this; + } + /** * Apply the update to a single cache entry matching the path. *

@@ -212,6 +344,12 @@ public class DirCacheEditor extends BaseDirCacheEditor { * the path is a new path in the index. */ public abstract void apply(DirCacheEntry ent); + + @Override + public String toString() { + String p = DirCacheEntry.toString(path); + return getClass().getSimpleName() + '[' + p + ']'; + } } /** @@ -272,10 +410,26 @@ public class DirCacheEditor extends BaseDirCacheEditor { * only the subtree's contents are matched by the command. * The special case "" (not "/"!) deletes all entries. */ - public DeleteTree(final String entryPath) { - super( - (entryPath.endsWith("/") || entryPath.length() == 0) ? entryPath //$NON-NLS-1$ - : entryPath + "/"); //$NON-NLS-1$ + public DeleteTree(String entryPath) { + super(entryPath.isEmpty() + || entryPath.charAt(entryPath.length() - 1) == '/' + ? entryPath + : entryPath + '/'); + } + + DeleteTree(byte[] path) { + super(appendSlash(path)); + } + + private static byte[] appendSlash(byte[] path) { + int n = path.length; + if (n > 0 && path[n - 1] != '/') { + byte[] r = new byte[n + 1]; + System.arraycopy(path, 0, r, 0, n); + r[n] = '/'; + return r; + } + return path; } public void apply(final DirCacheEntry ent) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java index c8bc0960f..4ebf2e0d7 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java @@ -294,6 +294,23 @@ public class DirCacheEntry { NB.encodeInt16(info, infoOffset + P_FLAGS, flags); } + /** + * Duplicate DirCacheEntry with same path and copied info. + *

+ * The same path buffer is reused (avoiding copying), however a new info + * buffer is created and its contents are copied. + * + * @param src + * entry to clone. + * @since 4.2 + */ + public DirCacheEntry(DirCacheEntry src) { + path = src.path; + info = new byte[INFO_LEN]; + infoOffset = 0; + System.arraycopy(src.info, src.infoOffset, info, 0, INFO_LEN); + } + void write(final OutputStream os) throws IOException { final int len = isExtended() ? INFO_LEN_EXTENDED : INFO_LEN; final int pathLen = path.length; @@ -745,7 +762,7 @@ public class DirCacheEntry { } } - private static String toString(final byte[] path) { + static String toString(final byte[] path) { return Constants.CHARSET.decode(ByteBuffer.wrap(path)).toString(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/CorruptObjectException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/CorruptObjectException.java index c6ea09375..e4db40b88 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/errors/CorruptObjectException.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/CorruptObjectException.java @@ -49,8 +49,10 @@ package org.eclipse.jgit.errors; import java.io.IOException; import java.text.MessageFormat; +import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.ObjectChecker; import org.eclipse.jgit.lib.ObjectId; /** @@ -59,6 +61,26 @@ import org.eclipse.jgit.lib.ObjectId; public class CorruptObjectException extends IOException { private static final long serialVersionUID = 1L; + private ObjectChecker.ErrorType errorType; + + /** + * Report a specific error condition discovered in an object. + * + * @param type + * type of error + * @param id + * identity of the bad object + * @param why + * description of the error. + * @since 4.2 + */ + public CorruptObjectException(ObjectChecker.ErrorType type, AnyObjectId id, + String why) { + super(MessageFormat.format(JGitText.get().objectIsCorrupt3, + type.getMessageId(), id.name(), why)); + this.errorType = type; + } + /** * Construct a CorruptObjectException for reporting a problem specified * object id @@ -66,8 +88,8 @@ public class CorruptObjectException extends IOException { * @param id * @param why */ - public CorruptObjectException(final AnyObjectId id, final String why) { - this(id.toObjectId(), why); + public CorruptObjectException(AnyObjectId id, String why) { + super(MessageFormat.format(JGitText.get().objectIsCorrupt, id.name(), why)); } /** @@ -77,7 +99,7 @@ public class CorruptObjectException extends IOException { * @param id * @param why */ - public CorruptObjectException(final ObjectId id, final String why) { + public CorruptObjectException(ObjectId id, String why) { super(MessageFormat.format(JGitText.get().objectIsCorrupt, id.name(), why)); } @@ -87,7 +109,7 @@ public class CorruptObjectException extends IOException { * * @param why */ - public CorruptObjectException(final String why) { + public CorruptObjectException(String why) { super(why); } @@ -105,4 +127,15 @@ public class CorruptObjectException extends IOException { super(why); initCause(cause); } + + /** + * Specific error condition identified by {@link ObjectChecker}. + * + * @return error condition or null. + * @since 4.2 + */ + @Nullable + public ObjectChecker.ErrorType getErrorType() { + return errorType; + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GitlinkTreeEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/DirCacheNameConflictException.java similarity index 59% rename from org.eclipse.jgit/src/org/eclipse/jgit/lib/GitlinkTreeEntry.java rename to org.eclipse.jgit/src/org/eclipse/jgit/errors/DirCacheNameConflictException.java index 936fd82bf..5f67e3439 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GitlinkTreeEntry.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/DirCacheNameConflictException.java @@ -1,7 +1,5 @@ /* - * Copyright (C) 2009, Jonas Fonseca - * Copyright (C) 2007, Robin Rosenberg - * Copyright (C) 2007, Shawn O. Pearce + * Copyright (C) 2015, Google Inc. * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -43,45 +41,40 @@ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package org.eclipse.jgit.lib; +package org.eclipse.jgit.errors; /** - * A tree entry representing a gitlink entry used for submodules. + * Thrown by DirCache code when entries overlap in impossible way. * - * Note. Java cannot really handle these as file system objects. - * - * @deprecated To look up information about a single path, use - * {@link org.eclipse.jgit.treewalk.TreeWalk#forPath(Repository, String, org.eclipse.jgit.revwalk.RevTree)}. - * To lookup information about multiple paths at once, use a - * {@link org.eclipse.jgit.treewalk.TreeWalk} and obtain the current entry's - * information from its getter methods. + * @since 4.2 */ -@Deprecated -public class GitlinkTreeEntry extends TreeEntry { +public class DirCacheNameConflictException extends IllegalStateException { + private static final long serialVersionUID = 1L; + + private final String path1; + private final String path2; /** - * Construct a {@link GitlinkTreeEntry} with the specified name and SHA-1 in - * the specified parent + * Construct an exception for a specific path. * - * @param parent - * @param id - * @param nameUTF8 + * @param path1 + * one path that conflicts. + * @param path2 + * another path that conflicts. */ - public GitlinkTreeEntry(final Tree parent, final ObjectId id, - final byte[] nameUTF8) { - super(parent, id, nameUTF8); + public DirCacheNameConflictException(String path1, String path2) { + super(path1 + ' ' + path2); + this.path1 = path1; + this.path2 = path2; } - public FileMode getMode() { - return FileMode.GITLINK; + /** @return one of the paths that has a conflict. */ + public String getPath1() { + return path1; } - @Override - public String toString() { - final StringBuilder r = new StringBuilder(); - r.append(ObjectId.toString(getId())); - r.append(" G "); //$NON-NLS-1$ - r.append(getFullName()); - return r.toString(); + /** @return another path that has a conflict. */ + public String getPath2() { + return path2; } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java index 891479d1f..7eb955006 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java @@ -338,6 +338,20 @@ public class ManifestParser extends DefaultHandler { else last = p; } + removeNestedCopyfiles(); + } + + /** Remove copyfiles that sit in a subdirectory of any other project. */ + void removeNestedCopyfiles() { + for (RepoProject proj : filteredProjects) { + List copyfiles = new ArrayList<>(proj.getCopyFiles()); + proj.clearCopyFiles(); + for (CopyFile copyfile : copyfiles) { + if (!isNestedCopyfile(copyfile)) { + proj.addCopyFile(copyfile); + } + } + } } boolean inGroups(RepoProject proj) { @@ -357,4 +371,22 @@ public class ManifestParser extends DefaultHandler { } return false; } + + private boolean isNestedCopyfile(CopyFile copyfile) { + if (copyfile.dest.indexOf('/') == -1) { + // If the copyfile is at root level then it won't be nested. + return false; + } + for (RepoProject proj : filteredProjects) { + if (proj.getPath().compareTo(copyfile.dest) > 0) { + // Early return as remaining projects can't be ancestor of this + // copyfile config (filteredProjects is sorted). + return false; + } + if (proj.isAncestorOf(copyfile.dest)) { + return true; + } + } + return false; + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java index 9a072114a..915066d58 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java @@ -258,6 +258,15 @@ public class RepoProject implements Comparable { this.copyfiles.addAll(copyfiles); } + /** + * Clear all the copyfiles. + * + * @since 4.2 + */ + public void clearCopyFiles() { + this.copyfiles.clear(); + } + private String getPathWithSlash() { if (path.endsWith("/")) //$NON-NLS-1$ return path; @@ -273,7 +282,19 @@ public class RepoProject implements Comparable { * @return true if this sub repo is the ancestor of given sub repo. */ public boolean isAncestorOf(RepoProject that) { - return that.getPathWithSlash().startsWith(this.getPathWithSlash()); + return isAncestorOf(that.getPathWithSlash()); + } + + /** + * Check if this sub repo is an ancestor of the given path. + * + * @param path + * path to be checked to see if it is within this repository + * @return true if this sub repo is an ancestor of the given path. + * @since 4.2 + */ + public boolean isAncestorOf(String path) { + return path.startsWith(getPathWithSlash()); } @Override diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java index 796eaaebf..7740a2bb8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -158,6 +158,7 @@ public class JGitText extends TranslationBundle { /***/ public String cannotStoreObjects; /***/ public String cannotResolveUniquelyAbbrevObjectId; /***/ public String cannotUnloadAModifiedTree; + /***/ public String cannotUpdateUnbornBranch; /***/ public String cannotWorkWithOtherStagesThanZeroRightNow; /***/ public String cannotWriteObjectsPath; /***/ public String canOnlyCherryPickCommitsWithOneParent; @@ -184,14 +185,15 @@ public class JGitText extends TranslationBundle { /***/ public String connectionTimeOut; /***/ public String contextMustBeNonNegative; /***/ public String corruptionDetectedReReadingAt; + /***/ public String corruptObjectBadDate; + /***/ public String corruptObjectBadEmail; /***/ public String corruptObjectBadStream; /***/ public String corruptObjectBadStreamCorruptHeader; + /***/ public String corruptObjectBadTimezone; /***/ public String corruptObjectDuplicateEntryNames; /***/ public String corruptObjectGarbageAfterSize; /***/ public String corruptObjectIncorrectLength; /***/ public String corruptObjectIncorrectSorting; - /***/ public String corruptObjectInvalidAuthor; - /***/ public String corruptObjectInvalidCommitter; /***/ public String corruptObjectInvalidEntryMode; /***/ public String corruptObjectInvalidMode; /***/ public String corruptObjectInvalidModeChar; @@ -210,11 +212,11 @@ public class JGitText extends TranslationBundle { /***/ public String corruptObjectInvalidNamePrn; /***/ public String corruptObjectInvalidObject; /***/ public String corruptObjectInvalidParent; - /***/ public String corruptObjectInvalidTagger; /***/ public String corruptObjectInvalidTree; /***/ public String corruptObjectInvalidType; /***/ public String corruptObjectInvalidType2; /***/ public String corruptObjectMalformedHeader; + /***/ public String corruptObjectMissingEmail; /***/ public String corruptObjectNameContainsByte; /***/ public String corruptObjectNameContainsChar; /***/ public String corruptObjectNameContainsNullByte; @@ -240,6 +242,7 @@ public class JGitText extends TranslationBundle { /***/ public String corruptObjectTruncatedInMode; /***/ public String corruptObjectTruncatedInName; /***/ public String corruptObjectTruncatedInObjectId; + /***/ public String corruptObjectZeroId; /***/ public String corruptPack; /***/ public String couldNotCheckOutBecauseOfConflicts; /***/ public String couldNotDeleteLockFileShouldNotHappen; @@ -491,6 +494,7 @@ public class JGitText extends TranslationBundle { /***/ public String objectAtHasBadZlibStream; /***/ public String objectAtPathDoesNotHaveId; /***/ public String objectIsCorrupt; + /***/ public String objectIsCorrupt3; /***/ public String objectIsNotA; /***/ public String objectNotFound; /***/ public String objectNotFoundIn; @@ -663,6 +667,7 @@ public class JGitText extends TranslationBundle { /***/ public String transportProtoSFTP; /***/ public String transportProtoSSH; /***/ public String transportProtoTest; + /***/ public String transportProvidedRefWithNoObjectId; /***/ public String transportSSHRetryInterrupt; /***/ public String treeEntryAlreadyExists; /***/ public String treeFilterMarkerTooManyFilters; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollector.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollector.java index faf27e32b..784507d88 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollector.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollector.java @@ -44,18 +44,17 @@ package org.eclipse.jgit.internal.storage.dfs; import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.GC; +import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.GC_TXN; import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.UNREACHABLE_GARBAGE; import static org.eclipse.jgit.internal.storage.pack.PackExt.BITMAP_INDEX; import static org.eclipse.jgit.internal.storage.pack.PackExt.INDEX; import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK; -import static org.eclipse.jgit.lib.RefDatabase.ALL; import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; +import java.util.Collection; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; import org.eclipse.jgit.internal.JGitText; @@ -63,13 +62,15 @@ import org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource; import org.eclipse.jgit.internal.storage.file.PackIndex; import org.eclipse.jgit.internal.storage.pack.PackExt; import org.eclipse.jgit.internal.storage.pack.PackWriter; +import org.eclipse.jgit.internal.storage.reftree.RefTreeNames; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.lib.ObjectIdOwnerMap; +import org.eclipse.jgit.lib.ObjectIdSet; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefDatabase; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.storage.pack.PackConfig; import org.eclipse.jgit.storage.pack.PackStatistics; @@ -78,16 +79,14 @@ import org.eclipse.jgit.util.io.CountingOutputStream; /** Repack and garbage collect a repository. */ public class DfsGarbageCollector { private final DfsRepository repo; - - private final DfsRefDatabase refdb; - + private final RefDatabase refdb; private final DfsObjDatabase objdb; private final List newPackDesc; private final List newPackStats; - private final List newPackObj; + private final List newPackObj; private DfsReader ctx; @@ -95,14 +94,11 @@ public class DfsGarbageCollector { private long coalesceGarbageLimit = 50 << 20; - private Map refsBefore; - private List packsBefore; private Set allHeads; - private Set nonHeads; - + private Set txnHeads; private Set tagTargets; /** @@ -117,7 +113,7 @@ public class DfsGarbageCollector { objdb = repo.getObjectDatabase(); newPackDesc = new ArrayList(4); newPackStats = new ArrayList(4); - newPackObj = new ArrayList(4); + newPackObj = new ArrayList(4); packConfig = new PackConfig(repo); packConfig.setIndexVersion(2); @@ -195,22 +191,25 @@ public class DfsGarbageCollector { ctx = (DfsReader) objdb.newReader(); try { - refdb.clearCache(); + refdb.refresh(); objdb.clearCache(); - refsBefore = refdb.getRefs(ALL); + Collection refsBefore = RefTreeNames.allRefs(refdb); packsBefore = packsToRebuild(); if (packsBefore.isEmpty()) return true; allHeads = new HashSet(); nonHeads = new HashSet(); + txnHeads = new HashSet(); tagTargets = new HashSet(); - for (Ref ref : refsBefore.values()) { + for (Ref ref : refsBefore) { if (ref.isSymbolic() || ref.getObjectId() == null) continue; if (isHead(ref)) allHeads.add(ref.getObjectId()); + else if (RefTreeNames.isRefTree(refdb, ref.getName())) + txnHeads.add(ref.getObjectId()); else nonHeads.add(ref.getObjectId()); if (ref.getPeeledObjectId() != null) @@ -222,6 +221,7 @@ public class DfsGarbageCollector { try { packHeads(pm); packRest(pm); + packRefTreeGraph(pm); packGarbage(pm); objdb.commitPack(newPackDesc, toPrune()); rollback = false; @@ -277,18 +277,17 @@ public class DfsGarbageCollector { try (PackWriter pw = newPackWriter()) { pw.setTagTargets(tagTargets); - pw.preparePack(pm, allHeads, Collections. emptySet()); + pw.preparePack(pm, allHeads, PackWriter.NONE); if (0 < pw.getObjectCount()) writePack(GC, pw, pm); } } - private void packRest(ProgressMonitor pm) throws IOException { if (nonHeads.isEmpty()) return; try (PackWriter pw = newPackWriter()) { - for (PackWriter.ObjectIdSet packedObjs : newPackObj) + for (ObjectIdSet packedObjs : newPackObj) pw.excludeObjects(packedObjs); pw.preparePack(pm, nonHeads, allHeads); if (0 < pw.getObjectCount()) @@ -296,6 +295,19 @@ public class DfsGarbageCollector { } } + private void packRefTreeGraph(ProgressMonitor pm) throws IOException { + if (txnHeads.isEmpty()) + return; + + try (PackWriter pw = newPackWriter()) { + for (ObjectIdSet packedObjs : newPackObj) + pw.excludeObjects(packedObjs); + pw.preparePack(pm, txnHeads, PackWriter.NONE); + if (0 < pw.getObjectCount()) + writePack(GC_TXN, pw, pm); + } + } + private void packGarbage(ProgressMonitor pm) throws IOException { // TODO(sop) This is ugly. The garbage pack needs to be deleted. PackConfig cfg = new PackConfig(packConfig); @@ -328,7 +340,7 @@ public class DfsGarbageCollector { } private boolean anyPackHas(AnyObjectId id) { - for (PackWriter.ObjectIdSet packedObjs : newPackObj) + for (ObjectIdSet packedObjs : newPackObj) if (packedObjs.contains(id)) return true; return false; @@ -389,17 +401,10 @@ public class DfsGarbageCollector { } } - final ObjectIdOwnerMap packedObjs = pw - .getObjectSet(); - newPackObj.add(new PackWriter.ObjectIdSet() { - public boolean contains(AnyObjectId objectId) { - return packedObjs.contains(objectId); - } - }); - PackStatistics stats = pw.getStatistics(); pack.setPackStats(stats); newPackStats.add(stats); + newPackObj.add(pw.getObjectSet()); DfsBlockCache.getInstance().getOrCreate(pack, null); return pack; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjDatabase.java index 5f491ff2f..3641560ee 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjDatabase.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjDatabase.java @@ -90,6 +90,13 @@ public abstract class DfsObjDatabase extends ObjectDatabase { */ GC(1), + /** + * RefTreeGraph pack was created by Git garbage collection. + * + * @see DfsGarbageCollector + */ + GC_TXN(1), + /** * The pack was created by compacting multiple packs together. *

diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackCompactor.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackCompactor.java index 7073763a7..11aef7fea 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackCompactor.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackCompactor.java @@ -62,6 +62,7 @@ import org.eclipse.jgit.internal.storage.pack.PackWriter; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdSet; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.revwalk.RevFlag; import org.eclipse.jgit.revwalk.RevObject; @@ -91,7 +92,7 @@ public class DfsPackCompactor { private final List srcPacks; - private final List exclude; + private final List exclude; private final List newPacks; @@ -113,7 +114,7 @@ public class DfsPackCompactor { repo = repository; autoAddSize = 5 * 1024 * 1024; // 5 MiB srcPacks = new ArrayList(); - exclude = new ArrayList(4); + exclude = new ArrayList(4); newPacks = new ArrayList(1); newStats = new ArrayList(1); } @@ -164,7 +165,7 @@ public class DfsPackCompactor { * objects to not include. * @return {@code this}. */ - public DfsPackCompactor exclude(PackWriter.ObjectIdSet set) { + public DfsPackCompactor exclude(ObjectIdSet set) { exclude.add(set); return this; } @@ -183,11 +184,7 @@ public class DfsPackCompactor { try (DfsReader ctx = (DfsReader) repo.newObjectReader()) { idx = pack.getPackIndex(ctx); } - return exclude(new PackWriter.ObjectIdSet() { - public boolean contains(AnyObjectId id) { - return idx.hasObject(id); - } - }); + return exclude(idx); } /** @@ -343,7 +340,7 @@ public class DfsPackCompactor { RevObject obj = rw.lookupOrNull(id); if (obj != null && (obj.has(added) || obj.has(isBase))) continue; - for (PackWriter.ObjectIdSet e : exclude) + for (ObjectIdSet e : exclude) if (e.contains(id)) continue SCAN; want.add(new ObjectIdWithOffset(id, ent.getOffset())); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRefDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRefDatabase.java index a1035a128..e5469f6b8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRefDatabase.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRefDatabase.java @@ -261,6 +261,11 @@ public abstract class DfsRefDatabase extends RefDatabase { // Nothing to do. } + @Override + public void refresh() { + clearCache(); + } + @Override public void close() { clearCache(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRepository.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRepository.java index 0d5fd0f85..ef8845084 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRepository.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRepository.java @@ -79,9 +79,6 @@ public abstract class DfsRepository extends Repository { @Override public abstract DfsObjDatabase getObjectDatabase(); - @Override - public abstract DfsRefDatabase getRefDatabase(); - /** @return a description of this repository. */ public DfsRepositoryDescription getDescription() { return description; @@ -95,7 +92,10 @@ public abstract class DfsRepository extends Repository { * the repository cannot be checked. */ public boolean exists() throws IOException { - return getRefDatabase().exists(); + if (getRefDatabase() instanceof DfsRefDatabase) { + return ((DfsRefDatabase) getRefDatabase()).exists(); + } + return true; } @Override @@ -117,7 +117,7 @@ public abstract class DfsRepository extends Repository { @Override public void scanForRepoChanges() throws IOException { - getRefDatabase().clearCache(); + getRefDatabase().refresh(); getObjectDatabase().clearCache(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/InMemoryRepository.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/InMemoryRepository.java index 1c664b409..5e246b47b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/InMemoryRepository.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/InMemoryRepository.java @@ -16,7 +16,6 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; -import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.internal.storage.pack.PackExt; import org.eclipse.jgit.lib.BatchRefUpdate; import org.eclipse.jgit.lib.ObjectId; @@ -24,6 +23,7 @@ import org.eclipse.jgit.lib.ObjectIdRef; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Ref.Storage; +import org.eclipse.jgit.lib.RefDatabase; import org.eclipse.jgit.lib.SymbolicRef; import org.eclipse.jgit.revwalk.RevObject; import org.eclipse.jgit.revwalk.RevTag; @@ -54,7 +54,7 @@ public class InMemoryRepository extends DfsRepository { static final AtomicInteger packId = new AtomicInteger(); private final DfsObjDatabase objdb; - private final DfsRefDatabase refdb; + private final RefDatabase refdb; private boolean performsAtomicTransactions = true; /** @@ -80,7 +80,7 @@ public class InMemoryRepository extends DfsRepository { } @Override - public DfsRefDatabase getRefDatabase() { + public RefDatabase getRefDatabase() { return refdb; } @@ -310,6 +310,11 @@ public class InMemoryRepository extends DfsRepository { Map peeled = new HashMap<>(); try (RevWalk rw = new RevWalk(getRepository())) { for (ReceiveCommand c : cmds) { + if (c.getResult() != ReceiveCommand.Result.NOT_ATTEMPTED) { + ReceiveCommand.abort(cmds); + return; + } + if (!ObjectId.zeroId().equals(c.getNewId())) { try { RevObject o = rw.parseAny(c.getNewId()); @@ -318,7 +323,7 @@ public class InMemoryRepository extends DfsRepository { } } catch (IOException e) { c.setResult(ReceiveCommand.Result.REJECTED_MISSING_OBJECT); - reject(cmds); + ReceiveCommand.abort(cmds); return; } } @@ -331,14 +336,17 @@ public class InMemoryRepository extends DfsRepository { if (r == null) { if (c.getType() != ReceiveCommand.Type.CREATE) { c.setResult(ReceiveCommand.Result.LOCK_FAILURE); - reject(cmds); + ReceiveCommand.abort(cmds); + return; + } + } else { + ObjectId objectId = r.getObjectId(); + if (r.isSymbolic() || objectId == null + || !objectId.equals(c.getOldId())) { + c.setResult(ReceiveCommand.Result.LOCK_FAILURE); + ReceiveCommand.abort(cmds); return; } - } else if (r.isSymbolic() || r.getObjectId() == null - || !r.getObjectId().equals(c.getOldId())) { - c.setResult(ReceiveCommand.Result.LOCK_FAILURE); - reject(cmds); - return; } } @@ -365,15 +373,6 @@ public class InMemoryRepository extends DfsRepository { clearCache(); } - private void reject(List cmds) { - for (ReceiveCommand c : cmds) { - if (c.getResult() == ReceiveCommand.Result.NOT_ATTEMPTED) { - c.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, - JGitText.get().transactionAborted); - } - } - } - @Override protected boolean compareAndPut(Ref oldRef, Ref newRef) throws IOException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java index 490cbcaa8..62d2d6969 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java @@ -63,6 +63,7 @@ import org.eclipse.jgit.events.ConfigChangedEvent; import org.eclipse.jgit.events.ConfigChangedListener; import org.eclipse.jgit.events.IndexChangedEvent; import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.internal.storage.reftree.RefTreeDatabase; import org.eclipse.jgit.internal.storage.file.ObjectDirectory.AlternateHandle; import org.eclipse.jgit.internal.storage.file.ObjectDirectory.AlternateRepository; import org.eclipse.jgit.lib.BaseRepositoryBuilder; @@ -201,7 +202,22 @@ public class FileRepository extends Repository { } }); - refs = new RefDirectory(this); + final long repositoryFormatVersion = getConfig().getLong( + ConfigConstants.CONFIG_CORE_SECTION, null, + ConfigConstants.CONFIG_KEY_REPO_FORMAT_VERSION, 0); + + String reftype = repoConfig.getString( + "extensions", null, "refsStorage"); //$NON-NLS-1$ //$NON-NLS-2$ + if (repositoryFormatVersion >= 1 && reftype != null) { + if (StringUtils.equalsIgnoreCase(reftype, "reftree")) { //$NON-NLS-1$ + refs = new RefTreeDatabase(this, new RefDirectory(this)); + } else { + throw new IOException(JGitText.get().unknownRepositoryFormat); + } + } else { + refs = new RefDirectory(this); + } + objectDatabase = new ObjectDirectory(repoConfig, // options.getObjectDirectory(), // options.getAlternateObjectDirectories(), // @@ -209,10 +225,7 @@ public class FileRepository extends Repository { new File(getDirectory(), Constants.SHALLOW)); if (objectDatabase.exists()) { - final long repositoryFormatVersion = getConfig().getLong( - ConfigConstants.CONFIG_CORE_SECTION, null, - ConfigConstants.CONFIG_KEY_REPO_FORMAT_VERSION, 0); - if (repositoryFormatVersion > 0) + if (repositoryFormatVersion > 1) throw new IOException(MessageFormat.format( JGitText.get().unknownRepositoryFormat2, Long.valueOf(repositoryFormatVersion))); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java index 4c40538b6..2ce0d4734 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java @@ -45,7 +45,6 @@ package org.eclipse.jgit.internal.storage.file; import static org.eclipse.jgit.internal.storage.pack.PackExt.BITMAP_INDEX; import static org.eclipse.jgit.internal.storage.pack.PackExt.INDEX; -import static org.eclipse.jgit.lib.RefDatabase.ALL; import java.io.File; import java.io.FileOutputStream; @@ -53,6 +52,7 @@ import java.io.IOException; import java.io.OutputStream; import java.nio.channels.Channels; import java.nio.channels.FileChannel; +import java.nio.file.StandardCopyOption; import java.text.MessageFormat; import java.text.ParseException; import java.util.ArrayList; @@ -62,14 +62,14 @@ import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Map.Entry; +import java.util.Objects; import java.util.Set; import java.util.TreeMap; +import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.dircache.DirCacheIterator; import org.eclipse.jgit.errors.CorruptObjectException; import org.eclipse.jgit.errors.IncorrectObjectTypeException; @@ -78,13 +78,13 @@ import org.eclipse.jgit.errors.NoWorkTreeException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.internal.storage.pack.PackExt; import org.eclipse.jgit.internal.storage.pack.PackWriter; -import org.eclipse.jgit.internal.storage.pack.PackWriter.ObjectIdSet; -import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.internal.storage.reftree.RefTreeNames; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdSet; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Ref.Storage; @@ -127,7 +127,7 @@ public class GC { * difference between the current refs and the refs which existed during * last {@link #repack()}. */ - private Map lastPackedRefs; + private Collection lastPackedRefs; /** * Holds the starting time of the last repack() execution. This is needed in @@ -361,17 +361,20 @@ public class GC { // during last repack(). Only those refs will survive which have been // added or modified since the last repack. Only these can save existing // loose refs from being pruned. - Map newRefs; + Collection newRefs; if (lastPackedRefs == null || lastPackedRefs.isEmpty()) newRefs = getAllRefs(); else { - newRefs = new HashMap(); - for (Iterator> i = getAllRefs().entrySet() - .iterator(); i.hasNext();) { - Entry newEntry = i.next(); - Ref old = lastPackedRefs.get(newEntry.getKey()); - if (!equals(newEntry.getValue(), old)) - newRefs.put(newEntry.getKey(), newEntry.getValue()); + Map last = new HashMap<>(); + for (Ref r : lastPackedRefs) { + last.put(r.getName(), r); + } + newRefs = new ArrayList<>(); + for (Ref r : getAllRefs()) { + Ref old = last.get(r.getName()); + if (!equals(r, old)) { + newRefs.add(r); + } } } @@ -383,10 +386,10 @@ public class GC { // leave this method. ObjectWalk w = new ObjectWalk(repo); try { - for (Ref cr : newRefs.values()) + for (Ref cr : newRefs) w.markStart(w.parseAny(cr.getObjectId())); if (lastPackedRefs != null) - for (Ref lpr : lastPackedRefs.values()) + for (Ref lpr : lastPackedRefs) w.markUninteresting(w.parseAny(lpr.getObjectId())); removeReferenced(deletionCandidates, w); } finally { @@ -404,11 +407,11 @@ public class GC { // additional reflog entries not handled during last repack() ObjectWalk w = new ObjectWalk(repo); try { - for (Ref ar : getAllRefs().values()) + for (Ref ar : getAllRefs()) for (ObjectId id : listRefLogObjects(ar, lastRepackTime)) w.markStart(w.parseAny(id)); if (lastPackedRefs != null) - for (Ref lpr : lastPackedRefs.values()) + for (Ref lpr : lastPackedRefs) w.markUninteresting(w.parseAny(lpr.getObjectId())); removeReferenced(deletionCandidates, w); } finally { @@ -483,9 +486,10 @@ public class GC { return false; return r1.getTarget().getName().equals(r2.getTarget().getName()); } else { - if (r2.isSymbolic()) + if (r2.isSymbolic()) { return false; - return r1.getObjectId().equals(r2.getObjectId()); + } + return Objects.equals(r1.getObjectId(), r2.getObjectId()); } } @@ -528,19 +532,23 @@ public class GC { Collection toBeDeleted = repo.getObjectDatabase().getPacks(); long time = System.currentTimeMillis(); - Map refsBefore = getAllRefs(); + Collection refsBefore = getAllRefs(); Set allHeads = new HashSet(); Set nonHeads = new HashSet(); + Set txnHeads = new HashSet(); Set tagTargets = new HashSet(); Set indexObjects = listNonHEADIndexObjects(); + RefDatabase refdb = repo.getRefDatabase(); - for (Ref ref : refsBefore.values()) { + for (Ref ref : refsBefore) { nonHeads.addAll(listRefLogObjects(ref, 0)); if (ref.isSymbolic() || ref.getObjectId() == null) continue; if (ref.getName().startsWith(Constants.R_HEADS)) allHeads.add(ref.getObjectId()); + else if (RefTreeNames.isRefTree(refdb, ref.getName())) + txnHeads.add(ref.getObjectId()); else nonHeads.add(ref.getObjectId()); if (ref.getPeeledObjectId() != null) @@ -550,7 +558,7 @@ public class GC { List excluded = new LinkedList(); for (final PackFile f : repo.getObjectDatabase().getPacks()) if (f.shouldBeKept()) - excluded.add(objectIdSet(f.getIndex())); + excluded.add(f.getIndex()); tagTargets.addAll(allHeads); nonHeads.addAll(indexObjects); @@ -562,7 +570,7 @@ public class GC { tagTargets, excluded); if (heads != null) { ret.add(heads); - excluded.add(0, objectIdSet(heads.getIndex())); + excluded.add(0, heads.getIndex()); } } if (!nonHeads.isEmpty()) { @@ -570,6 +578,11 @@ public class GC { if (rest != null) ret.add(rest); } + if (!txnHeads.isEmpty()) { + PackFile txn = writePack(txnHeads, PackWriter.NONE, null, excluded); + if (txn != null) + ret.add(txn); + } try { deleteOldPacks(toBeDeleted, ret); } catch (ParseException e) { @@ -622,11 +635,16 @@ public class GC { * @return a map where names of refs point to ref objects * @throws IOException */ - private Map getAllRefs() throws IOException { - Map ret = repo.getRefDatabase().getRefs(ALL); - for (Ref ref : repo.getRefDatabase().getAdditionalRefs()) - ret.put(ref.getName(), ref); - return ret; + private Collection getAllRefs() throws IOException { + Collection refs = RefTreeNames.allRefs(repo.getRefDatabase()); + List addl = repo.getRefDatabase().getAdditionalRefs(); + if (!addl.isEmpty()) { + List all = new ArrayList<>(refs.size() + addl.size()); + all.addAll(refs); + all.addAll(addl); + return all; + } + return refs; } /** @@ -681,8 +699,8 @@ public class GC { } } - private PackFile writePack(Set want, - Set have, Set tagTargets, + private PackFile writePack(@NonNull Set want, + @NonNull Set have, Set tagTargets, List excludeObjects) throws IOException { File tmpPack = null; Map tmpExts = new TreeMap( @@ -788,39 +806,33 @@ public class GC { break; } tmpPack.setReadOnly(); - boolean delete = true; - try { - FileUtils.rename(tmpPack, realPack); - delete = false; - for (Map.Entry tmpEntry : tmpExts.entrySet()) { - File tmpExt = tmpEntry.getValue(); - tmpExt.setReadOnly(); - - File realExt = nameFor( - id, "." + tmpEntry.getKey().getExtension()); //$NON-NLS-1$ - try { - FileUtils.rename(tmpExt, realExt); - } catch (IOException e) { - File newExt = new File(realExt.getParentFile(), - realExt.getName() + ".new"); //$NON-NLS-1$ - if (!tmpExt.renameTo(newExt)) - newExt = tmpExt; - throw new IOException(MessageFormat.format( - JGitText.get().panicCantRenameIndexFile, newExt, - realExt)); - } - } - } finally { - if (delete) { - if (tmpPack.exists()) - tmpPack.delete(); - for (File tmpExt : tmpExts.values()) { - if (tmpExt.exists()) - tmpExt.delete(); + FileUtils.rename(tmpPack, realPack, StandardCopyOption.ATOMIC_MOVE); + for (Map.Entry tmpEntry : tmpExts.entrySet()) { + File tmpExt = tmpEntry.getValue(); + tmpExt.setReadOnly(); + + File realExt = nameFor(id, + "." + tmpEntry.getKey().getExtension()); //$NON-NLS-1$ + try { + FileUtils.rename(tmpExt, realExt, + StandardCopyOption.ATOMIC_MOVE); + } catch (IOException e) { + File newExt = new File(realExt.getParentFile(), + realExt.getName() + ".new"); //$NON-NLS-1$ + try { + FileUtils.rename(tmpExt, newExt, + StandardCopyOption.ATOMIC_MOVE); + } catch (IOException e2) { + newExt = tmpExt; + e = e2; } + throw new IOException(MessageFormat.format( + JGitText.get().panicCantRenameIndexFile, newExt, + realExt), e); } } + return repo.getObjectDatabase().openPack(realPack); } finally { if (tmpPack != null && tmpPack.exists()) @@ -998,12 +1010,4 @@ public class GC { this.expire = expire; expireAgeMillis = -1; } - - private static ObjectIdSet objectIdSet(final PackIndex idx) { - return new ObjectIdSet() { - public boolean contains(AnyObjectId objectId) { - return idx.hasObject(objectId); - } - }; - } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LazyObjectIdSetFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LazyObjectIdSetFile.java new file mode 100644 index 000000000..1e2617c0e --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LazyObjectIdSetFile.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2015, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.internal.storage.file; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; + +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.MutableObjectId; +import org.eclipse.jgit.lib.ObjectIdOwnerMap; +import org.eclipse.jgit.lib.ObjectIdSet; + +/** Lazily loads a set of ObjectIds, one per line. */ +public class LazyObjectIdSetFile implements ObjectIdSet { + private final File src; + private ObjectIdOwnerMap set; + + /** + * Create a new lazy set from a file. + * + * @param src + * the source file. + */ + public LazyObjectIdSetFile(File src) { + this.src = src; + } + + @Override + public boolean contains(AnyObjectId objectId) { + if (set == null) { + set = load(); + } + return set.contains(objectId); + } + + private ObjectIdOwnerMap load() { + ObjectIdOwnerMap r = new ObjectIdOwnerMap<>(); + try (FileInputStream fin = new FileInputStream(src); + Reader rin = new InputStreamReader(fin, UTF_8); + BufferedReader br = new BufferedReader(rin)) { + MutableObjectId id = new MutableObjectId(); + for (String line; (line = br.readLine()) != null;) { + id.fromString(line); + if (!r.contains(id)) { + r.add(new Entry(id)); + } + } + } catch (IOException e) { + // Ignore IO errors accessing the lazy set. + } + return r; + } + + static class Entry extends ObjectIdOwnerMap.Entry { + Entry(AnyObjectId id) { + super(id); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java index e23ca741b..ce9677a62 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java @@ -54,6 +54,7 @@ import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.FileChannel; +import java.nio.file.StandardCopyOption; import java.text.MessageFormat; import org.eclipse.jgit.errors.LockFailedException; @@ -128,8 +129,6 @@ public class LockFile { private FileSnapshot commitSnapshot; - private final FS fs; - /** * Create a new lock for any file. * @@ -138,11 +137,24 @@ public class LockFile { * @param fs * the file system abstraction which will be necessary to perform * certain file system operations. + * @deprecated use {@link LockFile#LockFile(File)} instead */ + @Deprecated public LockFile(final File f, final FS fs) { ref = f; lck = getLockFile(ref); - this.fs = fs; + } + + /** + * Create a new lock for any file. + * + * @param f + * the file that will be locked. + * @since 4.2 + */ + public LockFile(final File f) { + ref = f; + lck = getLockFile(ref); } /** @@ -441,56 +453,14 @@ public class LockFile { } saveStatInformation(); - if (lck.renameTo(ref)) { + try { + FileUtils.rename(lck, ref, StandardCopyOption.ATOMIC_MOVE); haveLck = false; return true; + } catch (IOException e) { + unlock(); + return false; } - if (!ref.exists() || deleteRef()) { - if (renameLock()) { - haveLck = false; - return true; - } - } - unlock(); - return false; - } - - private boolean deleteRef() { - if (!fs.retryFailedLockFileCommit()) - return ref.delete(); - - // File deletion fails on windows if another thread is - // concurrently reading the same file. So try a few times. - // - for (int attempts = 0; attempts < 10; attempts++) { - if (ref.delete()) - return true; - try { - Thread.sleep(100); - } catch (InterruptedException e) { - return false; - } - } - return false; - } - - private boolean renameLock() { - if (!fs.retryFailedLockFileCommit()) - return lck.renameTo(ref); - - // File renaming fails on windows if another thread is - // concurrently reading the same file. So try a few times. - // - for (int attempts = 0; attempts < 10; attempts++) { - if (lck.renameTo(ref)) - return true; - try { - Thread.sleep(100); - } catch (InterruptedException e) { - return false; - } - } - return false; } private void saveStatInformation() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java index bd1d488d9..ea8052851 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java @@ -52,6 +52,9 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; @@ -608,10 +611,16 @@ public class ObjectDirectory extends FileObjectDatabase { FileUtils.delete(tmp, FileUtils.RETRY); return InsertLooseObjectResult.EXISTS_LOOSE; } - if (tmp.renameTo(dst)) { + try { + Files.move(tmp.toPath(), dst.toPath(), + StandardCopyOption.ATOMIC_MOVE); dst.setReadOnly(); unpackedObjectCache.add(id); return InsertLooseObjectResult.INSERTED; + } catch (AtomicMoveNotSupportedException e) { + LOG.error(e.getMessage(), e); + } catch (IOException e) { + // ignore } // Maybe the directory doesn't exist yet as the object @@ -619,10 +628,16 @@ public class ObjectDirectory extends FileObjectDatabase { // try the rename first as the directory likely does exist. // FileUtils.mkdir(dst.getParentFile(), true); - if (tmp.renameTo(dst)) { + try { + Files.move(tmp.toPath(), dst.toPath(), + StandardCopyOption.ATOMIC_MOVE); dst.setReadOnly(); unpackedObjectCache.add(id); return InsertLooseObjectResult.INSERTED; + } catch (AtomicMoveNotSupportedException e) { + LOG.error(e.getMessage(), e); + } catch (IOException e) { + LOG.debug(e.getMessage(), e); } if (!createDuplicate && has(id)) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryPackParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryPackParser.java index 1c076ee09..2e6c245ea 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryPackParser.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryPackParser.java @@ -50,6 +50,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; +import java.nio.file.StandardCopyOption; import java.security.MessageDigest; import java.text.MessageFormat; import java.util.Arrays; @@ -476,20 +477,25 @@ public class ObjectDirectoryPackParser extends PackParser { } } - if (!tmpPack.renameTo(finalPack)) { + try { + FileUtils.rename(tmpPack, finalPack, + StandardCopyOption.ATOMIC_MOVE); + } catch (IOException e) { cleanupTemporaryFiles(); keep.unlock(); throw new IOException(MessageFormat.format( - JGitText.get().cannotMovePackTo, finalPack)); + JGitText.get().cannotMovePackTo, finalPack), e); } - if (!tmpIdx.renameTo(finalIdx)) { + try { + FileUtils.rename(tmpIdx, finalIdx, StandardCopyOption.ATOMIC_MOVE); + } catch (IOException e) { cleanupTemporaryFiles(); keep.unlock(); if (!finalPack.delete()) finalPack.deleteOnExit(); throw new IOException(MessageFormat.format( - JGitText.get().cannotMoveIndexTo, finalIdx)); + JGitText.get().cannotMoveIndexTo, finalIdx), e); } try { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndex.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndex.java index 0040aea71..f36bd4d70 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndex.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndex.java @@ -60,6 +60,7 @@ import org.eclipse.jgit.lib.AbbreviatedObjectId; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.MutableObjectId; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdSet; import org.eclipse.jgit.util.IO; import org.eclipse.jgit.util.NB; @@ -72,7 +73,8 @@ import org.eclipse.jgit.util.NB; * by ObjectId. *

*/ -public abstract class PackIndex implements Iterable { +public abstract class PackIndex + implements Iterable, ObjectIdSet { /** * Open an existing pack .idx file for reading. *

@@ -166,6 +168,11 @@ public abstract class PackIndex implements Iterable { return findOffset(id) != -1; } + @Override + public boolean contains(AnyObjectId id) { + return findOffset(id) != -1; + } + /** * Provide iterator that gives access to index entries. Note, that iterator * returns reference to mutable object, the same reference in each call - diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java index 69f7e9707..2c8e5f9d1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java @@ -73,6 +73,7 @@ import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.errors.InvalidObjectIdException; import org.eclipse.jgit.errors.LockFailedException; import org.eclipse.jgit.errors.MissingObjectException; @@ -715,16 +716,20 @@ public class RefDirectory extends RefDatabase { */ private Ref peeledPackedRef(Ref f) throws MissingObjectException, IOException { - if (f.getStorage().isPacked() && f.isPeeled()) + if (f.getStorage().isPacked() && f.isPeeled()) { return f; - if (!f.isPeeled()) + } + if (!f.isPeeled()) { f = peel(f); - if (f.getPeeledObjectId() != null) + } + ObjectId peeledObjectId = f.getPeeledObjectId(); + if (peeledObjectId != null) { return new ObjectIdRef.PeeledTag(PACKED, f.getName(), - f.getObjectId(), f.getPeeledObjectId()); - else + f.getObjectId(), peeledObjectId); + } else { return new ObjectIdRef.PeeledNonTag(PACKED, f.getName(), f.getObjectId()); + } } void log(final RefUpdate update, final String msg, final boolean deref) @@ -985,7 +990,7 @@ public class RefDirectory extends RefDatabase { try { id = ObjectId.fromString(buf, 0); if (ref != null && !ref.isSymbolic() - && ref.getTarget().getObjectId().equals(id)) { + && id.equals(ref.getTarget().getObjectId())) { assert(currentSnapshot != null); currentSnapshot.setClean(otherSnapshot); return ref; @@ -1103,8 +1108,8 @@ public class RefDirectory extends RefDatabase { implements LooseRef { private final FileSnapshot snapShot; - LoosePeeledTag(FileSnapshot snapshot, String refName, ObjectId id, - ObjectId p) { + LoosePeeledTag(FileSnapshot snapshot, @NonNull String refName, + @NonNull ObjectId id, @NonNull ObjectId p) { super(LOOSE, refName, id, p); this.snapShot = snapshot; } @@ -1122,7 +1127,8 @@ public class RefDirectory extends RefDatabase { implements LooseRef { private final FileSnapshot snapShot; - LooseNonTag(FileSnapshot snapshot, String refName, ObjectId id) { + LooseNonTag(FileSnapshot snapshot, @NonNull String refName, + @NonNull ObjectId id) { super(LOOSE, refName, id); this.snapShot = snapshot; } @@ -1140,7 +1146,8 @@ public class RefDirectory extends RefDatabase { implements LooseRef { private FileSnapshot snapShot; - LooseUnpeeled(FileSnapshot snapShot, String refName, ObjectId id) { + LooseUnpeeled(FileSnapshot snapShot, @NonNull String refName, + @NonNull ObjectId id) { super(LOOSE, refName, id); this.snapShot = snapShot; } @@ -1149,13 +1156,24 @@ public class RefDirectory extends RefDatabase { return snapShot; } + @NonNull + @Override + public ObjectId getObjectId() { + ObjectId id = super.getObjectId(); + assert id != null; // checked in constructor + return id; + } + public LooseRef peel(ObjectIdRef newLeaf) { - if (newLeaf.getPeeledObjectId() != null) + ObjectId peeledObjectId = newLeaf.getPeeledObjectId(); + ObjectId objectId = getObjectId(); + if (peeledObjectId != null) { return new LoosePeeledTag(snapShot, getName(), - getObjectId(), newLeaf.getPeeledObjectId()); - else + objectId, peeledObjectId); + } else { return new LooseNonTag(snapShot, getName(), - getObjectId()); + objectId); + } } } @@ -1163,7 +1181,8 @@ public class RefDirectory extends RefDatabase { LooseRef { private final FileSnapshot snapShot; - LooseSymbolicRef(FileSnapshot snapshot, String refName, Ref target) { + LooseSymbolicRef(FileSnapshot snapshot, @NonNull String refName, + @NonNull Ref target) { super(refName, target); this.snapShot = snapshot; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectoryRename.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectoryRename.java index ba4a63d7f..4b803a514 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectoryRename.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectoryRename.java @@ -46,6 +46,8 @@ package org.eclipse.jgit.internal.storage.file; import java.io.File; import java.io.IOException; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.StandardCopyOption; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; @@ -54,6 +56,8 @@ import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.RefUpdate.Result; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.util.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Rename any reference stored by {@link RefDirectory}. @@ -66,6 +70,9 @@ import org.eclipse.jgit.util.FileUtils; * directory that happens to match the source name. */ class RefDirectoryRename extends RefRename { + private static final Logger LOG = LoggerFactory + .getLogger(RefDirectoryRename.class); + private final RefDirectory refdb; /** @@ -201,13 +208,25 @@ class RefDirectoryRename extends RefRename { } private static boolean rename(File src, File dst) { - if (src.renameTo(dst)) + try { + FileUtils.rename(src, dst, StandardCopyOption.ATOMIC_MOVE); return true; + } catch (AtomicMoveNotSupportedException e) { + LOG.error(e.getMessage(), e); + } catch (IOException e) { + // ignore + } File dir = dst.getParentFile(); if ((dir.exists() || !dir.mkdirs()) && !dir.isDirectory()) return false; - return src.renameTo(dst); + try { + FileUtils.rename(src, dst, StandardCopyOption.ATOMIC_MOVE); + return true; + } catch (IOException e) { + LOG.error(e.getMessage(), e); + return false; + } } private boolean linkHEAD(RefUpdate target) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriter.java index 19b6b080d..525f9aecc 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriter.java @@ -80,6 +80,7 @@ import java.util.zip.CheckedOutputStream; import java.util.zip.Deflater; import java.util.zip.DeflaterOutputStream; +import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.errors.CorruptObjectException; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.LargeObjectException; @@ -99,6 +100,7 @@ import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectIdOwnerMap; +import org.eclipse.jgit.lib.ObjectIdSet; import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.ProgressMonitor; @@ -161,17 +163,8 @@ import org.eclipse.jgit.util.TemporaryBuffer; public class PackWriter implements AutoCloseable { private static final int PACK_VERSION_GENERATED = 2; - /** A collection of object ids. */ - public interface ObjectIdSet { - /** - * Returns true if the objectId is contained within the collection. - * - * @param objectId - * the objectId to find - * @return whether the collection contains the objectId. - */ - boolean contains(AnyObjectId objectId); - } + /** Empty set of objects for {@code preparePack()}. */ + public static Set NONE = Collections.emptySet(); private static final Map, Boolean> instances = new ConcurrentHashMap, Boolean>(); @@ -681,7 +674,7 @@ public class PackWriter implements AutoCloseable { * @throws IOException * when some I/O problem occur during reading objects. */ - public void preparePack(final Iterator objectsSource) + public void preparePack(@NonNull Iterator objectsSource) throws IOException { while (objectsSource.hasNext()) { addObject(objectsSource.next()); @@ -704,16 +697,18 @@ public class PackWriter implements AutoCloseable { * progress during object enumeration. * @param want * collection of objects to be marked as interesting (start - * points of graph traversal). + * points of graph traversal). Must not be {@code null}. * @param have * collection of objects to be marked as uninteresting (end - * points of graph traversal). + * points of graph traversal). Pass {@link #NONE} if all objects + * reachable from {@code want} are desired, such as when serving + * a clone. * @throws IOException * when some I/O problem occur during reading objects. */ public void preparePack(ProgressMonitor countingMonitor, - Set want, - Set have) throws IOException { + @NonNull Set want, + @NonNull Set have) throws IOException { ObjectWalk ow; if (shallowPack) ow = new DepthWalk.ObjectWalk(reader, depth); @@ -740,17 +735,19 @@ public class PackWriter implements AutoCloseable { * ObjectWalk to perform enumeration. * @param interestingObjects * collection of objects to be marked as interesting (start - * points of graph traversal). + * points of graph traversal). Must not be {@code null}. * @param uninterestingObjects * collection of objects to be marked as uninteresting (end - * points of graph traversal). + * points of graph traversal). Pass {@link #NONE} if all objects + * reachable from {@code want} are desired, such as when serving + * a clone. * @throws IOException * when some I/O problem occur during reading objects. */ public void preparePack(ProgressMonitor countingMonitor, - ObjectWalk walk, - final Set interestingObjects, - final Set uninterestingObjects) + @NonNull ObjectWalk walk, + @NonNull Set interestingObjects, + @NonNull Set uninterestingObjects) throws IOException { if (countingMonitor == null) countingMonitor = NullProgressMonitor.INSTANCE; @@ -1551,6 +1548,8 @@ public class PackWriter implements AutoCloseable { if (zbuf != null) { out.writeHeader(otp, otp.getCachedSize()); out.write(zbuf); + typeStats.cntDeltas++; + typeStats.deltaBytes += out.length() - otp.getOffset(); return; } } @@ -1606,17 +1605,12 @@ public class PackWriter implements AutoCloseable { out.write(packcsum); } - private void findObjectsToPack(final ProgressMonitor countingMonitor, - final ObjectWalk walker, final Set want, - Set have) - throws MissingObjectException, IOException, - IncorrectObjectTypeException { + private void findObjectsToPack(@NonNull ProgressMonitor countingMonitor, + @NonNull ObjectWalk walker, @NonNull Set want, + @NonNull Set have) throws IOException { final long countingStart = System.currentTimeMillis(); beginPhase(PackingPhase.COUNTING, countingMonitor, ProgressMonitor.UNKNOWN); - if (have == null) - have = Collections.emptySet(); - stats.interestingObjects = Collections.unmodifiableSet(new HashSet(want)); stats.uninterestingObjects = Collections.unmodifiableSet(new HashSet(have)); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/AlwaysFailUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/AlwaysFailUpdate.java new file mode 100644 index 000000000..12ef8734c --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/AlwaysFailUpdate.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2016, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.internal.storage.reftree; + +import java.io.IOException; + +import org.eclipse.jgit.lib.ObjectIdRef; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefDatabase; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.Repository; + +/** Update that always rejects with {@code LOCK_FAILURE}. */ +class AlwaysFailUpdate extends RefUpdate { + private final RefTreeDatabase refdb; + + AlwaysFailUpdate(RefTreeDatabase refdb, String name) { + super(new ObjectIdRef.Unpeeled(Ref.Storage.NEW, name, null)); + this.refdb = refdb; + setCheckConflicting(false); + } + + @Override + protected RefDatabase getRefDatabase() { + return refdb; + } + + @Override + protected Repository getRepository() { + return refdb.getRepository(); + } + + @Override + protected boolean tryLock(boolean deref) throws IOException { + return false; + } + + @Override + protected void unlock() { + // No locks are held here. + } + + @Override + protected Result doUpdate(Result desiredResult) { + return Result.LOCK_FAILURE; + } + + @Override + protected Result doDelete(Result desiredResult) { + return Result.LOCK_FAILURE; + } + + @Override + protected Result doLink(String target) { + return Result.LOCK_FAILURE; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/Command.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/Command.java new file mode 100644 index 000000000..dd08375f2 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/Command.java @@ -0,0 +1,316 @@ +/* + * Copyright (C) 2016, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.internal.storage.reftree; + +import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; +import static org.eclipse.jgit.lib.Constants.encode; +import static org.eclipse.jgit.lib.FileMode.TYPE_GITLINK; +import static org.eclipse.jgit.lib.FileMode.TYPE_SYMLINK; +import static org.eclipse.jgit.lib.Ref.Storage.NETWORK; +import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED; +import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON; + +import java.io.IOException; + +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdRef; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.revwalk.RevObject; +import org.eclipse.jgit.revwalk.RevTag; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.ReceiveCommand; +import org.eclipse.jgit.transport.ReceiveCommand.Result; + +/** + * Command to create, update or delete an entry inside a {@link RefTree}. + *

+ * Unlike {@link ReceiveCommand} (which can only update a reference to an + * {@link ObjectId}), a RefTree Command can also create, modify or delete + * symbolic references to a target reference. + *

+ * RefTree Commands may wrap a {@code ReceiveCommand} to allow callers to + * process an existing ReceiveCommand against a RefTree. + *

+ * Commands should be passed into {@link RefTree#apply(java.util.Collection)} + * for processing. + */ +public class Command { + /** + * Set unprocessed commands as failed due to transaction aborted. + *

+ * If a command is still {@link Result#NOT_ATTEMPTED} it will be set to + * {@link Result#REJECTED_OTHER_REASON}. If {@code why} is non-null its + * contents will be used as the message for the first command status. + * + * @param commands + * commands to mark as failed. + * @param why + * optional message to set on the first aborted command. + */ + public static void abort(Iterable commands, @Nullable String why) { + if (why == null || why.isEmpty()) { + why = JGitText.get().transactionAborted; + } + for (Command c : commands) { + if (c.getResult() == NOT_ATTEMPTED) { + c.setResult(REJECTED_OTHER_REASON, why); + why = JGitText.get().transactionAborted; + } + } + } + + private final Ref oldRef; + private final Ref newRef; + private final ReceiveCommand cmd; + private Result result; + + /** + * Create a command to create, update or delete a reference. + *

+ * At least one of {@code oldRef} or {@code newRef} must be supplied. + * + * @param oldRef + * expected value. Null if the ref should not exist. + * @param newRef + * desired value, must be peeled if not null and not symbolic. + * Null to delete the ref. + */ + public Command(@Nullable Ref oldRef, @Nullable Ref newRef) { + this.oldRef = oldRef; + this.newRef = newRef; + this.cmd = null; + this.result = NOT_ATTEMPTED; + + if (oldRef == null && newRef == null) { + throw new IllegalArgumentException(); + } + if (newRef != null && !newRef.isPeeled() && !newRef.isSymbolic()) { + throw new IllegalArgumentException(); + } + if (oldRef != null && newRef != null + && !oldRef.getName().equals(newRef.getName())) { + throw new IllegalArgumentException(); + } + } + + /** + * Construct a RefTree command wrapped around a ReceiveCommand. + * + * @param rw + * walk instance to peel the {@code newId}. + * @param cmd + * command received from a push client. + * @throws MissingObjectException + * {@code oldId} or {@code newId} is missing. + * @throws IOException + * {@code oldId} or {@code newId} cannot be peeled. + */ + public Command(RevWalk rw, ReceiveCommand cmd) + throws MissingObjectException, IOException { + this.oldRef = toRef(rw, cmd.getOldId(), cmd.getRefName(), false); + this.newRef = toRef(rw, cmd.getNewId(), cmd.getRefName(), true); + this.cmd = cmd; + } + + static Ref toRef(RevWalk rw, ObjectId id, String name, + boolean mustExist) throws MissingObjectException, IOException { + if (ObjectId.zeroId().equals(id)) { + return null; + } + + try { + RevObject o = rw.parseAny(id); + if (o instanceof RevTag) { + RevObject p = rw.peel(o); + return new ObjectIdRef.PeeledTag(NETWORK, name, id, p.copy()); + } + return new ObjectIdRef.PeeledNonTag(NETWORK, name, id); + } catch (MissingObjectException e) { + if (mustExist) { + throw e; + } + return new ObjectIdRef.Unpeeled(NETWORK, name, id); + } + } + + /** @return name of the reference affected by this command. */ + public String getRefName() { + if (cmd != null) { + return cmd.getRefName(); + } else if (newRef != null) { + return newRef.getName(); + } + return oldRef.getName(); + } + + /** + * Set the result of this command. + * + * @param result + * the command result. + */ + public void setResult(Result result) { + setResult(result, null); + } + + /** + * Set the result of this command. + * + * @param result + * the command result. + * @param why + * optional message explaining the result status. + */ + public void setResult(Result result, @Nullable String why) { + if (cmd != null) { + cmd.setResult(result, why); + } else { + this.result = result; + } + } + + /** @return result of executing this command. */ + public Result getResult() { + return cmd != null ? cmd.getResult() : result; + } + + /** @return optional message explaining command failure. */ + @Nullable + public String getMessage() { + return cmd != null ? cmd.getMessage() : null; + } + + /** + * Old peeled reference. + * + * @return the old reference; null if the command is creating the reference. + */ + @Nullable + public Ref getOldRef() { + return oldRef; + } + + /** + * New peeled reference. + * + * @return the new reference; null if the command is deleting the reference. + */ + @Nullable + public Ref getNewRef() { + return newRef; + } + + @Override + public String toString() { + StringBuilder s = new StringBuilder(); + append(s, oldRef, "CREATE"); //$NON-NLS-1$ + s.append(' '); + append(s, newRef, "DELETE"); //$NON-NLS-1$ + s.append(' ').append(getRefName()); + s.append(' ').append(getResult()); + if (getMessage() != null) { + s.append(' ').append(getMessage()); + } + return s.toString(); + } + + private static void append(StringBuilder s, Ref r, String nullName) { + if (r == null) { + s.append(nullName); + } else if (r.isSymbolic()) { + s.append(r.getTarget().getName()); + } else { + ObjectId id = r.getObjectId(); + if (id != null) { + s.append(id.name()); + } + } + } + + /** + * Check the entry is consistent with either the old or the new ref. + * + * @param entry + * current entry; null if the entry does not exist. + * @return true if entry matches {@link #getOldRef()} or + * {@link #getNewRef()}; otherwise false. + */ + boolean checkRef(@Nullable DirCacheEntry entry) { + if (entry != null && entry.getRawMode() == 0) { + entry = null; + } + return check(entry, oldRef) || check(entry, newRef); + } + + private static boolean check(@Nullable DirCacheEntry cur, + @Nullable Ref exp) { + if (cur == null) { + // Does not exist, ok if oldRef does not exist. + return exp == null; + } else if (exp == null) { + // Expected to not exist, but currently exists, fail. + return false; + } + + if (exp.isSymbolic()) { + String dst = exp.getTarget().getName(); + return cur.getRawMode() == TYPE_SYMLINK + && cur.getObjectId().equals(symref(dst)); + } + + return cur.getRawMode() == TYPE_GITLINK + && cur.getObjectId().equals(exp.getObjectId()); + } + + static ObjectId symref(String s) { + @SuppressWarnings("resource") + ObjectInserter.Formatter fmt = new ObjectInserter.Formatter(); + return fmt.idFor(OBJ_BLOB, encode(s)); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTree.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTree.java new file mode 100644 index 000000000..85690c8ca --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTree.java @@ -0,0 +1,411 @@ +/* + * Copyright (C) 2016, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.internal.storage.reftree; + +import static org.eclipse.jgit.lib.Constants.HEAD; +import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; +import static org.eclipse.jgit.lib.Constants.R_REFS; +import static org.eclipse.jgit.lib.Constants.encode; +import static org.eclipse.jgit.lib.FileMode.GITLINK; +import static org.eclipse.jgit.lib.FileMode.SYMLINK; +import static org.eclipse.jgit.lib.FileMode.TYPE_GITLINK; +import static org.eclipse.jgit.lib.FileMode.TYPE_SYMLINK; +import static org.eclipse.jgit.lib.Ref.Storage.NEW; +import static org.eclipse.jgit.lib.Ref.Storage.PACKED; +import static org.eclipse.jgit.lib.RefDatabase.MAX_SYMBOLIC_REF_DEPTH; +import static org.eclipse.jgit.transport.ReceiveCommand.Result.LOCK_FAILURE; +import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON; + +import java.io.IOException; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.dircache.DirCacheBuilder; +import org.eclipse.jgit.dircache.DirCacheEditor; +import org.eclipse.jgit.dircache.DirCacheEditor.DeletePath; +import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit; +import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.errors.DirCacheNameConflictException; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdRef; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.SymbolicRef; +import org.eclipse.jgit.revwalk.RevTree; +import org.eclipse.jgit.util.RawParseUtils; + +/** + * Tree of references in the reference graph. + *

+ * The root corresponds to the {@code "refs/"} subdirectory, for example the + * default reference {@code "refs/heads/master"} is stored at path + * {@code "heads/master"} in a {@code RefTree}. + *

+ * Normal references are stored as {@link FileMode#GITLINK} tree entries. The + * ObjectId in the tree entry is the ObjectId the reference refers to. + *

+ * Symbolic references are stored as {@link FileMode#SYMLINK} entries, with the + * blob storing the name of the target reference. + *

+ * Annotated tags also store the peeled object using a {@code GITLINK} entry + * with the suffix " ^" (space carrot), for example + * {@code "tags/v1.0"} stores the annotated tag object, while + * "tags/v1.0 ^" stores the commit the tag annotates. + *

+ * {@code HEAD} is a special case and stored as {@code "..HEAD"}. + */ +public class RefTree { + /** Suffix applied to GITLINK to indicate its the peeled value of a tag. */ + public static final String PEELED_SUFFIX = " ^"; //$NON-NLS-1$ + static final String ROOT_DOTDOT = ".."; //$NON-NLS-1$ + + /** + * Create an empty reference tree. + * + * @return a new empty reference tree. + */ + public static RefTree newEmptyTree() { + return new RefTree(DirCache.newInCore()); + } + + /** + * Load a reference tree. + * + * @param reader + * reader to scan the reference tree with. + * @param tree + * the tree to read. + * @return the ref tree read from the commit. + * @throws IOException + * the repository cannot be accessed through the reader. + * @throws CorruptObjectException + * a tree object is corrupt and cannot be read. + * @throws IncorrectObjectTypeException + * a tree object wasn't actually a tree. + * @throws MissingObjectException + * a reference tree object doesn't exist. + */ + public static RefTree read(ObjectReader reader, RevTree tree) + throws MissingObjectException, IncorrectObjectTypeException, + CorruptObjectException, IOException { + return new RefTree(DirCache.read(reader, tree)); + } + + private DirCache contents; + private Map pendingBlobs; + + private RefTree(DirCache dc) { + this.contents = dc; + } + + /** + * Read one reference. + *

+ * References are always returned peeled ({@link Ref#isPeeled()} is true). + * If the reference points to an annotated tag, the returned reference will + * be peeled and contain {@link Ref#getPeeledObjectId()}. + *

+ * If the reference is a symbolic reference and the chain depth is less than + * {@link org.eclipse.jgit.lib.RefDatabase#MAX_SYMBOLIC_REF_DEPTH} the + * returned reference is resolved. If the chain depth is longer, the + * symbolic reference is returned without resolving. + * + * @param reader + * to access objects necessary to read the requested reference. + * @param name + * name of the reference to read. + * @return the reference; null if it does not exist. + * @throws IOException + * cannot read a symbolic reference target. + */ + @Nullable + public Ref exactRef(ObjectReader reader, String name) throws IOException { + Ref r = readRef(reader, name); + if (r == null) { + return null; + } else if (r.isSymbolic()) { + return resolve(reader, r, 0); + } + + DirCacheEntry p = contents.getEntry(peeledPath(name)); + if (p != null && p.getRawMode() == TYPE_GITLINK) { + return new ObjectIdRef.PeeledTag(PACKED, r.getName(), + r.getObjectId(), p.getObjectId()); + } + return r; + } + + private Ref readRef(ObjectReader reader, String name) throws IOException { + DirCacheEntry e = contents.getEntry(refPath(name)); + return e != null ? toRef(reader, e, name) : null; + } + + private Ref toRef(ObjectReader reader, DirCacheEntry e, String name) + throws IOException { + int mode = e.getRawMode(); + if (mode == TYPE_GITLINK) { + ObjectId id = e.getObjectId(); + return new ObjectIdRef.PeeledNonTag(PACKED, name, id); + } + + if (mode == TYPE_SYMLINK) { + ObjectId id = e.getObjectId(); + String n = pendingBlobs != null ? pendingBlobs.get(id) : null; + if (n == null) { + byte[] bin = reader.open(id, OBJ_BLOB).getCachedBytes(); + n = RawParseUtils.decode(bin); + } + Ref dst = new ObjectIdRef.Unpeeled(NEW, n, null); + return new SymbolicRef(name, dst); + } + + return null; // garbage file or something; not a reference. + } + + private Ref resolve(ObjectReader reader, Ref ref, int depth) + throws IOException { + if (ref.isSymbolic() && depth < MAX_SYMBOLIC_REF_DEPTH) { + Ref r = readRef(reader, ref.getTarget().getName()); + if (r == null) { + return ref; + } + Ref dst = resolve(reader, r, depth + 1); + return new SymbolicRef(ref.getName(), dst); + } + return ref; + } + + /** + * Attempt a batch of commands against this RefTree. + *

+ * The batch is applied atomically, either all commands apply at once, or + * they all reject and the RefTree is left unmodified. + *

+ * On success (when this method returns {@code true}) the command results + * are left as-is (probably {@code NOT_ATTEMPTED}). Result fields are set + * only when this method returns {@code false} to indicate failure. + * + * @param cmdList + * to apply. All commands should still have result NOT_ATTEMPTED. + * @return true if the commands applied; false if they were rejected. + */ + public boolean apply(Collection cmdList) { + try { + DirCacheEditor ed = contents.editor(); + for (Command cmd : cmdList) { + if (!isValidRef(cmd)) { + cmd.setResult(REJECTED_OTHER_REASON, + JGitText.get().funnyRefname); + Command.abort(cmdList, null); + return false; + } + apply(ed, cmd); + } + ed.finish(); + return true; + } catch (DirCacheNameConflictException e) { + String r1 = refName(e.getPath1()); + String r2 = refName(e.getPath2()); + for (Command cmd : cmdList) { + if (r1.equals(cmd.getRefName()) + || r2.equals(cmd.getRefName())) { + cmd.setResult(LOCK_FAILURE); + break; + } + } + Command.abort(cmdList, null); + return false; + } catch (LockFailureException e) { + Command.abort(cmdList, null); + return false; + } + } + + private static boolean isValidRef(Command cmd) { + String n = cmd.getRefName(); + return HEAD.equals(n) || Repository.isValidRefName(n); + } + + private void apply(DirCacheEditor ed, final Command cmd) { + String path = refPath(cmd.getRefName()); + Ref oldRef = cmd.getOldRef(); + final Ref newRef = cmd.getNewRef(); + + if (newRef == null) { + checkRef(contents.getEntry(path), cmd); + ed.add(new DeletePath(path)); + cleanupPeeledRef(ed, oldRef); + return; + } + + if (newRef.isSymbolic()) { + final String dst = newRef.getTarget().getName(); + ed.add(new PathEdit(path) { + @Override + public void apply(DirCacheEntry ent) { + checkRef(ent, cmd); + ObjectId id = Command.symref(dst); + ent.setFileMode(SYMLINK); + ent.setObjectId(id); + if (pendingBlobs == null) { + pendingBlobs = new HashMap<>(4); + } + pendingBlobs.put(id, dst); + } + }.setReplace(false)); + cleanupPeeledRef(ed, oldRef); + return; + } + + ed.add(new PathEdit(path) { + @Override + public void apply(DirCacheEntry ent) { + checkRef(ent, cmd); + ent.setFileMode(GITLINK); + ent.setObjectId(newRef.getObjectId()); + } + }.setReplace(false)); + + if (newRef.getPeeledObjectId() != null) { + ed.add(new PathEdit(peeledPath(newRef.getName())) { + @Override + public void apply(DirCacheEntry ent) { + ent.setFileMode(GITLINK); + ent.setObjectId(newRef.getPeeledObjectId()); + } + }.setReplace(false)); + } else { + cleanupPeeledRef(ed, oldRef); + } + } + + private static void checkRef(@Nullable DirCacheEntry ent, Command cmd) { + if (!cmd.checkRef(ent)) { + cmd.setResult(LOCK_FAILURE); + throw new LockFailureException(); + } + } + + private static void cleanupPeeledRef(DirCacheEditor ed, Ref ref) { + if (ref != null && !ref.isSymbolic() + && (!ref.isPeeled() || ref.getPeeledObjectId() != null)) { + ed.add(new DeletePath(peeledPath(ref.getName()))); + } + } + + /** + * Convert a path name in a RefTree to the reference name known by Git. + * + * @param path + * name read from the RefTree structure, for example + * {@code "heads/master"}. + * @return reference name for the path, {@code "refs/heads/master"}. + */ + public static String refName(String path) { + if (path.startsWith(ROOT_DOTDOT)) { + return path.substring(2); + } + return R_REFS + path; + } + + static String refPath(String name) { + if (name.startsWith(R_REFS)) { + return name.substring(R_REFS.length()); + } + return ROOT_DOTDOT + name; + } + + private static String peeledPath(String name) { + return refPath(name) + PEELED_SUFFIX; + } + + /** + * Write this reference tree. + * + * @param inserter + * inserter to use when writing trees to the object database. + * Caller is responsible for flushing the inserter before trying + * to read the objects, or exposing them through a reference. + * @return the top level tree. + * @throws IOException + * a tree could not be written. + */ + public ObjectId writeTree(ObjectInserter inserter) throws IOException { + if (pendingBlobs != null) { + for (String s : pendingBlobs.values()) { + inserter.insert(OBJ_BLOB, encode(s)); + } + pendingBlobs = null; + } + return contents.writeTree(inserter); + } + + /** @return a deep copy of this RefTree. */ + public RefTree copy() { + RefTree r = new RefTree(DirCache.newInCore()); + DirCacheBuilder b = r.contents.builder(); + for (int i = 0; i < contents.getEntryCount(); i++) { + b.add(new DirCacheEntry(contents.getEntry(i))); + } + b.finish(); + if (pendingBlobs != null) { + r.pendingBlobs = new HashMap<>(pendingBlobs); + } + return r; + } + + private static class LockFailureException extends RuntimeException { + private static final long serialVersionUID = 1L; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeBatch.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeBatch.java new file mode 100644 index 000000000..a55a9f51e --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeBatch.java @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2016, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.internal.storage.reftree; + +import static org.eclipse.jgit.lib.Constants.OBJ_TREE; +import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED; +import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK; +import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_NONFASTFORWARD; +import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON; +import static org.eclipse.jgit.transport.ReceiveCommand.Type.UPDATE; +import static org.eclipse.jgit.transport.ReceiveCommand.Type.UPDATE_NONFASTFORWARD; + +import java.io.IOException; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.BatchRefUpdate; +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.NullProgressMonitor; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.ProgressMonitor; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.ReceiveCommand; + +/** Batch update a {@link RefTreeDatabase}. */ +class RefTreeBatch extends BatchRefUpdate { + private final RefTreeDatabase refdb; + private Ref src; + private ObjectId parentCommitId; + private ObjectId parentTreeId; + private RefTree tree; + private PersonIdent author; + private ObjectId newCommitId; + + RefTreeBatch(RefTreeDatabase refdb) { + super(refdb); + this.refdb = refdb; + } + + @Override + public void execute(RevWalk rw, ProgressMonitor monitor) + throws IOException { + List todo = new ArrayList<>(getCommands().size()); + for (ReceiveCommand c : getCommands()) { + if (!isAllowNonFastForwards()) { + if (c.getType() == UPDATE) { + c.updateType(rw); + } + if (c.getType() == UPDATE_NONFASTFORWARD) { + c.setResult(REJECTED_NONFASTFORWARD); + ReceiveCommand.abort(getCommands()); + return; + } + } + todo.add(new Command(rw, c)); + } + init(rw); + execute(rw, todo); + } + + void init(RevWalk rw) throws IOException { + src = refdb.getBootstrap().exactRef(refdb.getTxnCommitted()); + if (src != null && src.getObjectId() != null) { + RevCommit c = rw.parseCommit(src.getObjectId()); + parentCommitId = c; + parentTreeId = c.getTree(); + tree = RefTree.read(rw.getObjectReader(), c.getTree()); + } else { + parentCommitId = ObjectId.zeroId(); + parentTreeId = new ObjectInserter.Formatter() + .idFor(OBJ_TREE, new byte[] {}); + tree = RefTree.newEmptyTree(); + } + } + + @Nullable + Ref exactRef(ObjectReader reader, String name) throws IOException { + return tree.exactRef(reader, name); + } + + /** + * Execute an update from {@link RefTreeUpdate} or {@link RefTreeRename}. + * + * @param rw + * current RevWalk handling the update or rename. + * @param todo + * commands to execute. Must never be a bootstrap reference name. + * @throws IOException + * the storage system is unable to read or write data. + */ + void execute(RevWalk rw, List todo) throws IOException { + for (Command c : todo) { + if (c.getResult() != NOT_ATTEMPTED) { + Command.abort(todo, null); + return; + } + if (refdb.conflictsWithBootstrap(c.getRefName())) { + c.setResult(REJECTED_OTHER_REASON, MessageFormat + .format(JGitText.get().invalidRefName, c.getRefName())); + Command.abort(todo, null); + return; + } + } + + if (apply(todo) && newCommitId != null) { + commit(rw, todo); + } + } + + private boolean apply(List todo) throws IOException { + if (!tree.apply(todo)) { + // apply set rejection information on commands. + return false; + } + + Repository repo = refdb.getRepository(); + try (ObjectInserter ins = repo.newObjectInserter()) { + CommitBuilder b = new CommitBuilder(); + b.setTreeId(tree.writeTree(ins)); + if (parentTreeId.equals(b.getTreeId())) { + for (Command c : todo) { + c.setResult(OK); + } + return true; + } + if (!parentCommitId.equals(ObjectId.zeroId())) { + b.setParentId(parentCommitId); + } + + author = getRefLogIdent(); + if (author == null) { + author = new PersonIdent(repo); + } + b.setAuthor(author); + b.setCommitter(author); + b.setMessage(getRefLogMessage()); + newCommitId = ins.insert(b); + ins.flush(); + } + return true; + } + + private void commit(RevWalk rw, List todo) throws IOException { + ReceiveCommand commit = new ReceiveCommand( + parentCommitId, newCommitId, + refdb.getTxnCommitted()); + updateBootstrap(rw, commit); + + if (commit.getResult() == OK) { + for (Command c : todo) { + c.setResult(OK); + } + } else { + Command.abort(todo, commit.getResult().name()); + } + } + + private void updateBootstrap(RevWalk rw, ReceiveCommand commit) + throws IOException { + BatchRefUpdate u = refdb.getBootstrap().newBatchUpdate(); + u.setAllowNonFastForwards(true); + u.setPushCertificate(getPushCertificate()); + if (isRefLogDisabled()) { + u.disableRefLog(); + } else { + u.setRefLogIdent(author); + u.setRefLogMessage(getRefLogMessage(), false); + } + u.addCommand(commit); + u.execute(rw, NullProgressMonitor.INSTANCE); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeDatabase.java new file mode 100644 index 000000000..dc6031110 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeDatabase.java @@ -0,0 +1,337 @@ +/* + * Copyright (C) 2016, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.internal.storage.reftree; + +import static org.eclipse.jgit.lib.Ref.Storage.LOOSE; +import static org.eclipse.jgit.lib.Ref.Storage.PACKED; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.lib.BatchRefUpdate; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdRef; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Ref.Storage; +import org.eclipse.jgit.lib.RefDatabase; +import org.eclipse.jgit.lib.RefRename; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.SymbolicRef; +import org.eclipse.jgit.revwalk.RevObject; +import org.eclipse.jgit.revwalk.RevTag; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.util.RefList; +import org.eclipse.jgit.util.RefMap; + +/** + * Reference database backed by a {@link RefTree}. + *

+ * The storage for RefTreeDatabase has two parts. The main part is a native Git + * tree object stored under the {@code refs/txn} namespace. To avoid cycles, + * references to {@code refs/txn} are not stored in that tree object, but + * instead in a "bootstrap" layer, which is a separate {@link RefDatabase} such + * as {@link org.eclipse.jgit.internal.storage.file.RefDirectory} using local + * reference files inside of {@code $GIT_DIR/refs}. + */ +public class RefTreeDatabase extends RefDatabase { + private final Repository repo; + private final RefDatabase bootstrap; + private final String txnCommitted; + + @Nullable + private final String txnNamespace; + private volatile Scanner.Result refs; + + /** + * Create a RefTreeDb for a repository. + * + * @param repo + * the repository using references in this database. + * @param bootstrap + * bootstrap reference database storing the references that + * anchor the {@link RefTree}. + */ + public RefTreeDatabase(Repository repo, RefDatabase bootstrap) { + Config cfg = repo.getConfig(); + String committed = cfg.getString("reftree", null, "committedRef"); //$NON-NLS-1$ //$NON-NLS-2$ + if (committed == null || committed.isEmpty()) { + committed = "refs/txn/committed"; //$NON-NLS-1$ + } + + this.repo = repo; + this.bootstrap = bootstrap; + this.txnNamespace = initNamespace(committed); + this.txnCommitted = committed; + } + + /** + * Create a RefTreeDb for a repository. + * + * @param repo + * the repository using references in this database. + * @param bootstrap + * bootstrap reference database storing the references that + * anchor the {@link RefTree}. + * @param txnCommitted + * name of the bootstrap reference holding the committed RefTree. + */ + public RefTreeDatabase(Repository repo, RefDatabase bootstrap, + String txnCommitted) { + this.repo = repo; + this.bootstrap = bootstrap; + this.txnNamespace = initNamespace(txnCommitted); + this.txnCommitted = txnCommitted; + } + + private static String initNamespace(String committed) { + int s = committed.lastIndexOf('/'); + if (s < 0) { + return null; + } + return committed.substring(0, s + 1); // Keep trailing '/'. + } + + Repository getRepository() { + return repo; + } + + /** + * @return the bootstrap reference database, which must be used to access + * {@link #getTxnCommitted()}, {@link #getTxnNamespace()}. + */ + public RefDatabase getBootstrap() { + return bootstrap; + } + + /** @return name of bootstrap reference anchoring committed RefTree. */ + public String getTxnCommitted() { + return txnCommitted; + } + + /** + * @return namespace used by bootstrap layer, e.g. {@code refs/txn/}. + * Always ends in {@code '/'}. + */ + @Nullable + public String getTxnNamespace() { + return txnNamespace; + } + + @Override + public void create() throws IOException { + bootstrap.create(); + } + + @Override + public boolean performsAtomicTransactions() { + return true; + } + + @Override + public void refresh() { + bootstrap.refresh(); + } + + @Override + public void close() { + refs = null; + bootstrap.close(); + } + + @Override + public Ref getRef(String name) throws IOException { + return findRef(getRefs(ALL), name); + } + + @Override + public Ref exactRef(String name) throws IOException { + if (conflictsWithBootstrap(name)) { + return null; + } + + boolean partial = false; + Ref src = bootstrap.exactRef(txnCommitted); + Scanner.Result c = refs; + if (c == null || !c.refTreeId.equals(idOf(src))) { + c = Scanner.scanRefTree(repo, src, prefixOf(name), false); + partial = true; + } + + Ref r = c.all.get(name); + if (r != null && r.isSymbolic()) { + r = c.sym.get(name); + if (partial && r.getObjectId() == null) { + // Attempting exactRef("HEAD") with partial scan will leave + // an unresolved symref as its target e.g. refs/heads/master + // was not read by the partial scan. Scan everything instead. + return getRefs(ALL).get(name); + } + } + return r; + } + + private static String prefixOf(String name) { + int s = name.lastIndexOf('/'); + if (s >= 0) { + return name.substring(0, s); + } + return ""; //$NON-NLS-1$ + } + + @Override + public Map getRefs(String prefix) throws IOException { + if (!prefix.isEmpty() && prefix.charAt(prefix.length() - 1) != '/') { + return new HashMap<>(0); + } + + Ref src = bootstrap.exactRef(txnCommitted); + Scanner.Result c = refs; + if (c == null || !c.refTreeId.equals(idOf(src))) { + c = Scanner.scanRefTree(repo, src, prefix, true); + if (prefix.isEmpty()) { + refs = c; + } + } + return new RefMap(prefix, RefList. emptyList(), c.all, c.sym); + } + + private static ObjectId idOf(@Nullable Ref src) { + return src != null && src.getObjectId() != null + ? src.getObjectId() + : ObjectId.zeroId(); + } + + @Override + public List getAdditionalRefs() throws IOException { + return Collections.emptyList(); + } + + @Override + public Ref peel(Ref ref) throws IOException { + Ref i = ref.getLeaf(); + ObjectId id = i.getObjectId(); + if (i.isPeeled() || id == null) { + return ref; + } + try (RevWalk rw = new RevWalk(repo)) { + RevObject obj = rw.parseAny(id); + if (obj instanceof RevTag) { + ObjectId p = rw.peel(obj).copy(); + i = new ObjectIdRef.PeeledTag(PACKED, i.getName(), id, p); + } else { + i = new ObjectIdRef.PeeledNonTag(PACKED, i.getName(), id); + } + } + return recreate(ref, i); + } + + private static Ref recreate(Ref old, Ref leaf) { + if (old.isSymbolic()) { + Ref dst = recreate(old.getTarget(), leaf); + return new SymbolicRef(old.getName(), dst); + } + return leaf; + } + + @Override + public boolean isNameConflicting(String name) throws IOException { + return conflictsWithBootstrap(name) + || !getConflictingNames(name).isEmpty(); + } + + @Override + public BatchRefUpdate newBatchUpdate() { + return new RefTreeBatch(this); + } + + @Override + public RefUpdate newUpdate(String name, boolean detach) throws IOException { + if (conflictsWithBootstrap(name)) { + return new AlwaysFailUpdate(this, name); + } + + Ref r = exactRef(name); + if (r == null) { + r = new ObjectIdRef.Unpeeled(Storage.NEW, name, null); + } + + boolean detaching = detach && r.isSymbolic(); + if (detaching) { + r = new ObjectIdRef.Unpeeled(LOOSE, name, r.getObjectId()); + } + + RefTreeUpdate u = new RefTreeUpdate(this, r); + if (detaching) { + u.setDetachingSymbolicRef(); + } + return u; + } + + @Override + public RefRename newRename(String fromName, String toName) + throws IOException { + RefUpdate from = newUpdate(fromName, true); + RefUpdate to = newUpdate(toName, true); + return new RefTreeRename(this, from, to); + } + + boolean conflictsWithBootstrap(String name) { + if (txnNamespace != null && name.startsWith(txnNamespace)) { + return true; + } else if (txnCommitted.equals(name)) { + return true; + } else if (name.length() > txnCommitted.length() + && name.charAt(txnCommitted.length()) == '/' + && name.startsWith(txnCommitted)) { + return true; + } + return false; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeNames.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeNames.java new file mode 100644 index 000000000..239a74527 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeNames.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2016, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.internal.storage.reftree; + +import static org.eclipse.jgit.lib.RefDatabase.ALL; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefDatabase; + +/** Magic reference name logic for RefTrees. */ +public class RefTreeNames { + /** + * Suffix used on a {@link RefTreeDatabase#getTxnNamespace()} for user data. + *

+ * A {@link RefTreeDatabase}'s namespace may include a subspace (e.g. + * {@code "refs/txn/stage/"}) containing commit objects from the usual user + * portion of the repository (e.g. {@code "refs/heads/"}). These should be + * packed by the garbage collector alongside other user content rather than + * with the RefTree. + */ + private static final String STAGE = "stage/"; //$NON-NLS-1$ + + /** + * Determine if the reference is likely to be a RefTree. + * + * @param refdb + * database instance. + * @param ref + * reference name. + * @return {@code true} if the reference is a RefTree. + */ + public static boolean isRefTree(RefDatabase refdb, String ref) { + if (refdb instanceof RefTreeDatabase) { + RefTreeDatabase b = (RefTreeDatabase) refdb; + if (ref.equals(b.getTxnCommitted())) { + return true; + } + + String namespace = b.getTxnNamespace(); + if (namespace != null + && ref.startsWith(namespace) + && !ref.startsWith(namespace + STAGE)) { + return true; + } + } + return false; + } + + /** + * Snapshot all references from a RefTreeDatabase and its bootstrap. + *

+ * There may be name conflicts with multiple {@link Ref} objects containing + * the same name in the returned collection. + * + * @param refdb + * database instance. + * @return all known references. + * @throws IOException + * references cannot be enumerated. + */ + public static Collection allRefs(RefDatabase refdb) + throws IOException { + Collection refs = refdb.getRefs(ALL).values(); + if (!(refdb instanceof RefTreeDatabase)) { + return refs; + } + + RefDatabase bootstrap = ((RefTreeDatabase) refdb).getBootstrap(); + Collection br = bootstrap.getRefs(ALL).values(); + List all = new ArrayList<>(refs.size() + br.size()); + all.addAll(refs); + all.addAll(br); + return all; + } + + private RefTreeNames() { + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeRename.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeRename.java new file mode 100644 index 000000000..5fd7ecdd7 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeRename.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2016, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.internal.storage.reftree; + +import static org.eclipse.jgit.lib.Constants.HEAD; +import static org.eclipse.jgit.lib.RefUpdate.Result.REJECTED; +import static org.eclipse.jgit.lib.RefUpdate.Result.RENAMED; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdRef; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefRename; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.RefUpdate.Result; +import org.eclipse.jgit.lib.SymbolicRef; +import org.eclipse.jgit.revwalk.RevWalk; + +/** Single reference rename to {@link RefTreeDatabase}. */ +class RefTreeRename extends RefRename { + private final RefTreeDatabase refdb; + + RefTreeRename(RefTreeDatabase refdb, RefUpdate src, RefUpdate dst) { + super(src, dst); + this.refdb = refdb; + } + + @Override + protected Result doRename() throws IOException { + try (RevWalk rw = new RevWalk(refdb.getRepository())) { + RefTreeBatch batch = new RefTreeBatch(refdb); + batch.setRefLogIdent(getRefLogIdent()); + batch.setRefLogMessage(getRefLogMessage(), false); + batch.init(rw); + + Ref head = batch.exactRef(rw.getObjectReader(), HEAD); + Ref oldRef = batch.exactRef(rw.getObjectReader(), source.getName()); + if (oldRef == null) { + return REJECTED; + } + + Ref newRef = asNew(oldRef); + List mv = new ArrayList<>(3); + mv.add(new Command(oldRef, null)); + mv.add(new Command(null, newRef)); + if (head != null && head.isSymbolic() + && head.getTarget().getName().equals(oldRef.getName())) { + mv.add(new Command( + head, + new SymbolicRef(head.getName(), newRef))); + } + batch.execute(rw, mv); + return RefTreeUpdate.translate(mv.get(1).getResult(), RENAMED); + } + } + + private Ref asNew(Ref src) { + String name = destination.getName(); + if (src.isSymbolic()) { + return new SymbolicRef(name, src.getTarget()); + } + + ObjectId peeled = src.getPeeledObjectId(); + if (peeled != null) { + return new ObjectIdRef.PeeledTag( + src.getStorage(), + name, + src.getObjectId(), + peeled); + } + + return new ObjectIdRef.PeeledNonTag( + src.getStorage(), + name, + src.getObjectId()); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeUpdate.java new file mode 100644 index 000000000..8829c1156 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeUpdate.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2016, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.internal.storage.reftree; + +import static org.eclipse.jgit.lib.Ref.Storage.LOOSE; +import static org.eclipse.jgit.lib.Ref.Storage.NEW; + +import java.io.IOException; +import java.util.Collections; + +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdRef; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefDatabase; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.SymbolicRef; +import org.eclipse.jgit.revwalk.RevObject; +import org.eclipse.jgit.revwalk.RevTag; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.ReceiveCommand; + +/** Single reference update to {@link RefTreeDatabase}. */ +class RefTreeUpdate extends RefUpdate { + private final RefTreeDatabase refdb; + private RevWalk rw; + private RefTreeBatch batch; + private Ref oldRef; + + RefTreeUpdate(RefTreeDatabase refdb, Ref ref) { + super(ref); + this.refdb = refdb; + setCheckConflicting(false); // Done automatically by doUpdate. + } + + @Override + protected RefDatabase getRefDatabase() { + return refdb; + } + + @Override + protected Repository getRepository() { + return refdb.getRepository(); + } + + @Override + protected boolean tryLock(boolean deref) throws IOException { + rw = new RevWalk(getRepository()); + batch = new RefTreeBatch(refdb); + batch.init(rw); + oldRef = batch.exactRef(rw.getObjectReader(), getName()); + if (oldRef != null && oldRef.getObjectId() != null) { + setOldObjectId(oldRef.getObjectId()); + } else if (oldRef == null && getExpectedOldObjectId() != null) { + setOldObjectId(ObjectId.zeroId()); + } + return true; + } + + @Override + protected void unlock() { + batch = null; + if (rw != null) { + rw.close(); + rw = null; + } + } + + @Override + protected Result doUpdate(Result desiredResult) throws IOException { + return run(newRef(getName(), getNewObjectId()), desiredResult); + } + + private Ref newRef(String name, ObjectId id) + throws MissingObjectException, IOException { + RevObject o = rw.parseAny(id); + if (o instanceof RevTag) { + RevObject p = rw.peel(o); + return new ObjectIdRef.PeeledTag(LOOSE, name, id, p.copy()); + } + return new ObjectIdRef.PeeledNonTag(LOOSE, name, id); + } + + @Override + protected Result doDelete(Result desiredResult) throws IOException { + return run(null, desiredResult); + } + + @Override + protected Result doLink(String target) throws IOException { + Ref dst = new ObjectIdRef.Unpeeled(NEW, target, null); + SymbolicRef n = new SymbolicRef(getName(), dst); + Result desiredResult = getRef().getStorage() == NEW + ? Result.NEW + : Result.FORCED; + return run(n, desiredResult); + } + + private Result run(@Nullable Ref newRef, Result desiredResult) + throws IOException { + Command c = new Command(oldRef, newRef); + batch.setRefLogIdent(getRefLogIdent()); + batch.setRefLogMessage(getRefLogMessage(), isRefLogIncludingResult()); + batch.execute(rw, Collections.singletonList(c)); + return translate(c.getResult(), desiredResult); + } + + static Result translate(ReceiveCommand.Result r, Result desiredResult) { + switch (r) { + case OK: + return desiredResult; + + case LOCK_FAILURE: + return Result.LOCK_FAILURE; + + case NOT_ATTEMPTED: + return Result.NOT_ATTEMPTED; + + case REJECTED_MISSING_OBJECT: + return Result.IO_FAILURE; + + case REJECTED_CURRENT_BRANCH: + return Result.REJECTED_CURRENT_BRANCH; + + case REJECTED_OTHER_REASON: + case REJECTED_NOCREATE: + case REJECTED_NODELETE: + case REJECTED_NONFASTFORWARD: + default: + return Result.REJECTED; + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/Scanner.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/Scanner.java new file mode 100644 index 000000000..d383abf31 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/Scanner.java @@ -0,0 +1,286 @@ +/* + * Copyright (C) 2016, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.internal.storage.reftree; + +import static org.eclipse.jgit.lib.RefDatabase.MAX_SYMBOLIC_REF_DEPTH; +import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; +import static org.eclipse.jgit.lib.Constants.R_REFS; +import static org.eclipse.jgit.lib.Constants.encode; +import static org.eclipse.jgit.lib.FileMode.TYPE_GITLINK; +import static org.eclipse.jgit.lib.FileMode.TYPE_SYMLINK; +import static org.eclipse.jgit.lib.FileMode.TYPE_TREE; +import static org.eclipse.jgit.lib.Ref.Storage.NEW; +import static org.eclipse.jgit.lib.Ref.Storage.PACKED; + +import java.io.IOException; + +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdRef; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.SymbolicRef; +import org.eclipse.jgit.revwalk.RevTree; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.AbstractTreeIterator; +import org.eclipse.jgit.treewalk.CanonicalTreeParser; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.util.Paths; +import org.eclipse.jgit.util.RawParseUtils; +import org.eclipse.jgit.util.RefList; + +/** A tree parser that extracts references from a {@link RefTree}. */ +class Scanner { + private static final int MAX_SYMLINK_BYTES = 10 << 10; + private static final byte[] BINARY_R_REFS = encode(R_REFS); + private static final byte[] REFS_DOT_DOT = encode("refs/.."); //$NON-NLS-1$ + + static class Result { + final ObjectId refTreeId; + final RefList all; + final RefList sym; + + Result(ObjectId id, RefList all, RefList sym) { + this.refTreeId = id; + this.all = all; + this.sym = sym; + } + } + + /** + * Scan a {@link RefTree} and parse entries into {@link Ref} instances. + * + * @param repo + * source repository containing the commit and tree objects that + * make up the RefTree. + * @param src + * bootstrap reference such as {@code refs/txn/committed} to read + * the reference tree tip from. The current ObjectId will be + * included in {@link Result#refTreeId}. + * @param prefix + * if non-empty a reference prefix to scan only a subdirectory. + * For example {@code prefix = "refs/heads/"} will limit the scan + * to only the {@code "heads"} directory of the RefTree, avoiding + * other directories like {@code "tags"}. Empty string reads all + * entries in the RefTree. + * @param recursive + * if true recurse into subdirectories of the reference tree; + * false to read only one level. Callers may use false during an + * implementation of {@code exactRef(String)} where only one + * reference is needed out of a specific subtree. + * @return sorted list of references after parsing. + * @throws IOException + * tree cannot be accessed from the repository. + */ + static Result scanRefTree(Repository repo, @Nullable Ref src, String prefix, + boolean recursive) throws IOException { + RefList.Builder all = new RefList.Builder<>(); + RefList.Builder sym = new RefList.Builder<>(); + + ObjectId srcId; + if (src != null && src.getObjectId() != null) { + try (ObjectReader reader = repo.newObjectReader()) { + srcId = src.getObjectId(); + scan(reader, srcId, prefix, recursive, all, sym); + } + } else { + srcId = ObjectId.zeroId(); + } + + RefList aList = all.toRefList(); + for (int idx = 0; idx < sym.size();) { + Ref s = sym.get(idx); + Ref r = resolve(s, 0, aList); + if (r != null) { + sym.set(idx++, r); + } else { + // Remove broken symbolic reference, they don't exist. + sym.remove(idx); + int rm = aList.find(s.getName()); + if (0 <= rm) { + aList = aList.remove(rm); + } + } + } + return new Result(srcId, aList, sym.toRefList()); + } + + private static void scan(ObjectReader reader, AnyObjectId srcId, + String prefix, boolean recursive, + RefList.Builder all, RefList.Builder sym) + throws IncorrectObjectTypeException, IOException { + CanonicalTreeParser p = createParserAtPath(reader, srcId, prefix); + if (p == null) { + return; + } + + while (!p.eof()) { + int mode = p.getEntryRawMode(); + if (mode == TYPE_TREE) { + if (recursive) { + p = p.createSubtreeIterator(reader); + } else { + p = p.next(); + } + continue; + } + + if (!curElementHasPeelSuffix(p)) { + Ref r = toRef(reader, mode, p); + if (r != null) { + all.add(r); + if (r.isSymbolic()) { + sym.add(r); + } + } + } else if (mode == TYPE_GITLINK) { + peel(all, p); + } + p = p.next(); + } + } + + private static CanonicalTreeParser createParserAtPath(ObjectReader reader, + AnyObjectId srcId, String prefix) throws IOException { + ObjectId root = toTree(reader, srcId); + if (prefix.isEmpty()) { + return new CanonicalTreeParser(BINARY_R_REFS, reader, root); + } + + String dir = RefTree.refPath(Paths.stripTrailingSeparator(prefix)); + TreeWalk tw = TreeWalk.forPath(reader, dir, root); + if (tw == null || !tw.isSubtree()) { + return null; + } + + ObjectId id = tw.getObjectId(0); + return new CanonicalTreeParser(encode(prefix), reader, id); + } + + private static Ref resolve(Ref ref, int depth, RefList refs) + throws IOException { + if (!ref.isSymbolic()) { + return ref; + } else if (MAX_SYMBOLIC_REF_DEPTH <= depth) { + return null; + } + + Ref r = refs.get(ref.getTarget().getName()); + if (r == null) { + return ref; + } + + Ref dst = resolve(r, depth + 1, refs); + if (dst == null) { + return null; + } + return new SymbolicRef(ref.getName(), dst); + } + + @SuppressWarnings("resource") + private static RevTree toTree(ObjectReader reader, AnyObjectId id) + throws IOException { + return new RevWalk(reader).parseTree(id); + } + + private static boolean curElementHasPeelSuffix(AbstractTreeIterator itr) { + int n = itr.getEntryPathLength(); + byte[] c = itr.getEntryPathBuffer(); + return n > 2 && c[n - 2] == ' ' && c[n - 1] == '^'; + } + + private static void peel(RefList.Builder all, CanonicalTreeParser p) { + String name = refName(p, true); + for (int idx = all.size() - 1; 0 <= idx; idx--) { + Ref r = all.get(idx); + int cmp = r.getName().compareTo(name); + if (cmp == 0) { + all.set(idx, new ObjectIdRef.PeeledTag(PACKED, r.getName(), + r.getObjectId(), p.getEntryObjectId())); + break; + } else if (cmp < 0) { + // Stray peeled name without matching base name; skip entry. + break; + } + } + } + + private static Ref toRef(ObjectReader reader, int mode, + CanonicalTreeParser p) throws IOException { + if (mode == TYPE_GITLINK) { + String name = refName(p, false); + ObjectId id = p.getEntryObjectId(); + return new ObjectIdRef.PeeledNonTag(PACKED, name, id); + + } else if (mode == TYPE_SYMLINK) { + ObjectId id = p.getEntryObjectId(); + byte[] bin = reader.open(id, OBJ_BLOB) + .getCachedBytes(MAX_SYMLINK_BYTES); + String dst = RawParseUtils.decode(bin); + Ref trg = new ObjectIdRef.Unpeeled(NEW, dst, null); + String name = refName(p, false); + return new SymbolicRef(name, trg); + } + return null; + } + + private static String refName(CanonicalTreeParser p, boolean peel) { + byte[] buf = p.getEntryPathBuffer(); + int len = p.getEntryPathLength(); + if (peel) { + len -= 2; + } + int ptr = 0; + if (RawParseUtils.match(buf, ptr, REFS_DOT_DOT) > 0) { + ptr = 7; + } + return RawParseUtils.decode(buf, ptr, len); + } + + private Scanner() { + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java index 45dd7ee1a..670f9a9e1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java @@ -109,7 +109,8 @@ public class BaseRepositoryBuilder= 3 && blob[0] == (byte) 0xEF + && blob[1] == (byte) 0xBB && blob[2] == (byte) 0xBF) { + decoded = RawParseUtils.decode(RawParseUtils.UTF8_CHARSET, + blob, 3, blob.length); + } else { + decoded = RawParseUtils.decode(blob); + } + fromText(decoded); } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileTreeEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileTreeEntry.java deleted file mode 100644 index 6811417ee..000000000 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileTreeEntry.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (C) 2007, Robin Rosenberg - * Copyright (C) 2006-2007, Shawn O. Pearce - * and other copyright owners as documented in the project's IP log. - * - * This program and the accompanying materials are made available - * under the terms of the Eclipse Distribution License v1.0 which - * accompanies this distribution, is reproduced below, and is - * available at http://www.eclipse.org/org/documents/edl-v10.php - * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or - * without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * - Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * - * - Neither the name of the Eclipse Foundation, Inc. nor the - * names of its contributors may be used to endorse or promote - * products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND - * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, - * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package org.eclipse.jgit.lib; - -import java.io.IOException; - -/** - * A representation of a file (blob) object in a {@link Tree}. - * - * @deprecated To look up information about a single path, use - * {@link org.eclipse.jgit.treewalk.TreeWalk#forPath(Repository, String, org.eclipse.jgit.revwalk.RevTree)}. - * To lookup information about multiple paths at once, use a - * {@link org.eclipse.jgit.treewalk.TreeWalk} and obtain the current entry's - * information from its getter methods. - */ -@Deprecated -public class FileTreeEntry extends TreeEntry { - private FileMode mode; - - /** - * Constructor for a File (blob) object. - * - * @param parent - * The {@link Tree} holding this object (or null) - * @param id - * the SHA-1 of the blob (or null for a yet unhashed file) - * @param nameUTF8 - * raw object name in the parent tree - * @param execute - * true if the executable flag is set - */ - public FileTreeEntry(final Tree parent, final ObjectId id, - final byte[] nameUTF8, final boolean execute) { - super(parent, id, nameUTF8); - setExecutable(execute); - } - - public FileMode getMode() { - return mode; - } - - /** - * @return true if this file is executable - */ - public boolean isExecutable() { - return getMode().equals(FileMode.EXECUTABLE_FILE); - } - - /** - * @param execute set/reset the executable flag - */ - public void setExecutable(final boolean execute) { - mode = execute ? FileMode.EXECUTABLE_FILE : FileMode.REGULAR_FILE; - } - - /** - * @return an {@link ObjectLoader} that will return the data - * @throws IOException - */ - public ObjectLoader openReader() throws IOException { - return getRepository().open(getId(), Constants.OBJ_BLOB); - } - - public String toString() { - final StringBuilder r = new StringBuilder(); - r.append(ObjectId.toString(getId())); - r.append(' '); - r.append(isExecutable() ? 'X' : 'F'); - r.append(' '); - r.append(getFullName()); - return r.toString(); - } -} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java index a7a67a881..0b5efd77d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java @@ -44,21 +44,58 @@ package org.eclipse.jgit.lib; -import static org.eclipse.jgit.util.RawParseUtils.match; +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH; +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_STRING_LENGTH; +import static org.eclipse.jgit.lib.Constants.OBJ_BAD; +import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; +import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT; +import static org.eclipse.jgit.lib.Constants.OBJ_TAG; +import static org.eclipse.jgit.lib.Constants.OBJ_TREE; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.BAD_DATE; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.BAD_EMAIL; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.BAD_OBJECT_SHA1; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.BAD_PARENT_SHA1; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.BAD_TIMEZONE; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.BAD_TREE_SHA1; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.BAD_UTF8; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.DUPLICATE_ENTRIES; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.EMPTY_NAME; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.FULL_PATHNAME; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.HAS_DOT; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.HAS_DOTDOT; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.HAS_DOTGIT; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.MISSING_AUTHOR; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.MISSING_COMMITTER; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.MISSING_EMAIL; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.MISSING_OBJECT; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.MISSING_SPACE_BEFORE_DATE; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.MISSING_TAG_ENTRY; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.MISSING_TREE; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.MISSING_TYPE_ENTRY; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.NULL_SHA1; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.TREE_NOT_SORTED; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.UNKNOWN_TYPE; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.WIN32_BAD_NAME; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.ZERO_PADDED_FILEMODE; +import static org.eclipse.jgit.util.Paths.compare; +import static org.eclipse.jgit.util.Paths.compareSameName; import static org.eclipse.jgit.util.RawParseUtils.nextLF; import static org.eclipse.jgit.util.RawParseUtils.parseBase10; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.text.MessageFormat; +import java.text.Normalizer; +import java.util.EnumSet; import java.util.HashSet; import java.util.Locale; import java.util.Set; +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.errors.CorruptObjectException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.util.MutableInteger; import org.eclipse.jgit.util.RawParseUtils; +import org.eclipse.jgit.util.StringUtils; /** * Verifies that an object is formatted correctly. @@ -99,16 +136,119 @@ public class ObjectChecker { /** Header "tagger " */ public static final byte[] tagger = Constants.encodeASCII("tagger "); //$NON-NLS-1$ - private final MutableObjectId tempId = new MutableObjectId(); - - private final MutableInteger ptrout = new MutableInteger(); + /** + * Potential issues identified by the checker. + * + * @since 4.2 + */ + public enum ErrorType { + // @formatter:off + // These names match git-core so that fsck section keys also match. + /***/ NULL_SHA1, + /***/ DUPLICATE_ENTRIES, + /***/ TREE_NOT_SORTED, + /***/ ZERO_PADDED_FILEMODE, + /***/ EMPTY_NAME, + /***/ FULL_PATHNAME, + /***/ HAS_DOT, + /***/ HAS_DOTDOT, + /***/ HAS_DOTGIT, + /***/ BAD_OBJECT_SHA1, + /***/ BAD_PARENT_SHA1, + /***/ BAD_TREE_SHA1, + /***/ MISSING_AUTHOR, + /***/ MISSING_COMMITTER, + /***/ MISSING_OBJECT, + /***/ MISSING_TREE, + /***/ MISSING_TYPE_ENTRY, + /***/ MISSING_TAG_ENTRY, + /***/ BAD_DATE, + /***/ BAD_EMAIL, + /***/ BAD_TIMEZONE, + /***/ MISSING_EMAIL, + /***/ MISSING_SPACE_BEFORE_DATE, + /***/ UNKNOWN_TYPE, + + // These are unique to JGit. + /***/ WIN32_BAD_NAME, + /***/ BAD_UTF8; + // @formatter:on + + /** @return camelCaseVersion of the name. */ + public String getMessageId() { + String n = name(); + StringBuilder r = new StringBuilder(n.length()); + for (int i = 0; i < n.length(); i++) { + char c = n.charAt(i); + if (c != '_') { + r.append(StringUtils.toLowerCase(c)); + } else { + r.append(n.charAt(++i)); + } + } + return r.toString(); + } + } - private boolean allowZeroMode; + private final MutableObjectId tempId = new MutableObjectId(); + private final MutableInteger bufPtr = new MutableInteger(); + private EnumSet errors = EnumSet.allOf(ErrorType.class); + private ObjectIdSet skipList; private boolean allowInvalidPersonIdent; private boolean windows; private boolean macosx; + /** + * Enable accepting specific malformed (but not horribly broken) objects. + * + * @param objects + * collection of object names known to be broken in a non-fatal + * way that should be ignored by the checker. + * @return {@code this} + * @since 4.2 + */ + public ObjectChecker setSkipList(@Nullable ObjectIdSet objects) { + skipList = objects; + return this; + } + + /** + * Configure error types to be ignored across all objects. + * + * @param ids + * error types to ignore. The caller's set is copied. + * @return {@code this} + * @since 4.2 + */ + public ObjectChecker setIgnore(@Nullable Set ids) { + errors = EnumSet.allOf(ErrorType.class); + if (ids != null) { + errors.removeAll(ids); + } + return this; + } + + /** + * Add message type to be ignored across all objects. + * + * @param id + * error type to ignore. + * @param ignore + * true to ignore this error; false to treat the error as an + * error and throw. + * @return {@code this} + * @since 4.2 + */ + public ObjectChecker setIgnore(ErrorType id, boolean ignore) { + if (ignore) { + errors.remove(id); + } else { + errors.add(id); + } + return this; + } + /** * Enable accepting leading zero mode in tree entries. *

@@ -116,14 +256,15 @@ public class ObjectChecker { * tree entries. This is technically incorrect but gracefully allowed by * git-core. JGit rejects such trees by default, but may need to accept * them on broken histories. + *

+ * Same as {@code setIgnore(ZERO_PADDED_FILEMODE, allow)}. * * @param allow allow leading zero mode. * @return {@code this}. * @since 3.4 */ public ObjectChecker setAllowLeadingZeroFileMode(boolean allow) { - allowZeroMode = allow; - return this; + return setIgnore(ZERO_PADDED_FILEMODE, allow); } /** @@ -184,62 +325,117 @@ public class ObjectChecker { * @throws CorruptObjectException * if an error is identified. */ - public void check(final int objType, final byte[] raw) + public void check(int objType, byte[] raw) + throws CorruptObjectException { + check(idFor(objType, raw), objType, raw); + } + + /** + * Check an object for parsing errors. + * + * @param id + * identify of the object being checked. + * @param objType + * type of the object. Must be a valid object type code in + * {@link Constants}. + * @param raw + * the raw data which comprises the object. This should be in the + * canonical format (that is the format used to generate the + * ObjectId of the object). The array is never modified. + * @throws CorruptObjectException + * if an error is identified. + * @since 4.2 + */ + public void check(@Nullable AnyObjectId id, int objType, byte[] raw) throws CorruptObjectException { switch (objType) { - case Constants.OBJ_COMMIT: - checkCommit(raw); + case OBJ_COMMIT: + checkCommit(id, raw); break; - case Constants.OBJ_TAG: - checkTag(raw); + case OBJ_TAG: + checkTag(id, raw); break; - case Constants.OBJ_TREE: - checkTree(raw); + case OBJ_TREE: + checkTree(id, raw); break; - case Constants.OBJ_BLOB: + case OBJ_BLOB: checkBlob(raw); break; default: - throw new CorruptObjectException(MessageFormat.format( + report(UNKNOWN_TYPE, id, MessageFormat.format( JGitText.get().corruptObjectInvalidType2, Integer.valueOf(objType))); } } - private int id(final byte[] raw, final int ptr) { + private boolean checkId(byte[] raw) { + int p = bufPtr.value; try { - tempId.fromString(raw, ptr); - return ptr + Constants.OBJECT_ID_STRING_LENGTH; + tempId.fromString(raw, p); } catch (IllegalArgumentException e) { - return -1; + bufPtr.value = nextLF(raw, p); + return false; + } + + p += OBJECT_ID_STRING_LENGTH; + if (raw[p] == '\n') { + bufPtr.value = p + 1; + return true; } + bufPtr.value = nextLF(raw, p); + return false; } - private int personIdent(final byte[] raw, int ptr) { - if (allowInvalidPersonIdent) - return nextLF(raw, ptr) - 1; + private void checkPersonIdent(byte[] raw, @Nullable AnyObjectId id) + throws CorruptObjectException { + if (allowInvalidPersonIdent) { + bufPtr.value = nextLF(raw, bufPtr.value); + return; + } - final int emailB = nextLF(raw, ptr, '<'); - if (emailB == ptr || raw[emailB - 1] != '<') - return -1; + final int emailB = nextLF(raw, bufPtr.value, '<'); + if (emailB == bufPtr.value || raw[emailB - 1] != '<') { + report(MISSING_EMAIL, id, JGitText.get().corruptObjectMissingEmail); + bufPtr.value = nextLF(raw, bufPtr.value); + return; + } final int emailE = nextLF(raw, emailB, '>'); - if (emailE == emailB || raw[emailE - 1] != '>') - return -1; - if (emailE == raw.length || raw[emailE] != ' ') - return -1; + if (emailE == emailB || raw[emailE - 1] != '>') { + report(BAD_EMAIL, id, JGitText.get().corruptObjectBadEmail); + bufPtr.value = nextLF(raw, bufPtr.value); + return; + } + if (emailE == raw.length || raw[emailE] != ' ') { + report(MISSING_SPACE_BEFORE_DATE, id, + JGitText.get().corruptObjectBadDate); + bufPtr.value = nextLF(raw, bufPtr.value); + return; + } + + parseBase10(raw, emailE + 1, bufPtr); // when + if (emailE + 1 == bufPtr.value || bufPtr.value == raw.length + || raw[bufPtr.value] != ' ') { + report(BAD_DATE, id, JGitText.get().corruptObjectBadDate); + bufPtr.value = nextLF(raw, bufPtr.value); + return; + } - parseBase10(raw, emailE + 1, ptrout); // when - ptr = ptrout.value; - if (emailE + 1 == ptr) - return -1; - if (ptr == raw.length || raw[ptr] != ' ') - return -1; + int p = bufPtr.value + 1; + parseBase10(raw, p, bufPtr); // tz offset + if (p == bufPtr.value) { + report(BAD_TIMEZONE, id, JGitText.get().corruptObjectBadTimezone); + bufPtr.value = nextLF(raw, bufPtr.value); + return; + } - parseBase10(raw, ptr + 1, ptrout); // tz offset - if (ptr + 1 == ptrout.value) - return -1; - return ptrout.value; + p = bufPtr.value; + if (raw[p] == '\n') { + bufPtr.value = p + 1; + } else { + report(BAD_TIMEZONE, id, JGitText.get().corruptObjectBadTimezone); + bufPtr.value = nextLF(raw, p); + } } /** @@ -250,36 +446,50 @@ public class ObjectChecker { * @throws CorruptObjectException * if any error was detected. */ - public void checkCommit(final byte[] raw) throws CorruptObjectException { - int ptr = 0; + public void checkCommit(byte[] raw) throws CorruptObjectException { + checkCommit(idFor(OBJ_COMMIT, raw), raw); + } - if ((ptr = match(raw, ptr, tree)) < 0) - throw new CorruptObjectException( - JGitText.get().corruptObjectNotreeHeader); - if ((ptr = id(raw, ptr)) < 0 || raw[ptr++] != '\n') - throw new CorruptObjectException( - JGitText.get().corruptObjectInvalidTree); + /** + * Check a commit for errors. + * + * @param id + * identity of the object being checked. + * @param raw + * the commit data. The array is never modified. + * @throws CorruptObjectException + * if any error was detected. + * @since 4.2 + */ + public void checkCommit(@Nullable AnyObjectId id, byte[] raw) + throws CorruptObjectException { + bufPtr.value = 0; - while (match(raw, ptr, parent) >= 0) { - ptr += parent.length; - if ((ptr = id(raw, ptr)) < 0 || raw[ptr++] != '\n') - throw new CorruptObjectException( + if (!match(raw, tree)) { + report(MISSING_TREE, id, JGitText.get().corruptObjectNotreeHeader); + } else if (!checkId(raw)) { + report(BAD_TREE_SHA1, id, JGitText.get().corruptObjectInvalidTree); + } + + while (match(raw, parent)) { + if (!checkId(raw)) { + report(BAD_PARENT_SHA1, id, JGitText.get().corruptObjectInvalidParent); + } } - if ((ptr = match(raw, ptr, author)) < 0) - throw new CorruptObjectException( - JGitText.get().corruptObjectNoAuthor); - if ((ptr = personIdent(raw, ptr)) < 0 || raw[ptr++] != '\n') - throw new CorruptObjectException( - JGitText.get().corruptObjectInvalidAuthor); + if (match(raw, author)) { + checkPersonIdent(raw, id); + } else { + report(MISSING_AUTHOR, id, JGitText.get().corruptObjectNoAuthor); + } - if ((ptr = match(raw, ptr, committer)) < 0) - throw new CorruptObjectException( + if (match(raw, committer)) { + checkPersonIdent(raw, id); + } else { + report(MISSING_COMMITTER, id, JGitText.get().corruptObjectNoCommitter); - if ((ptr = personIdent(raw, ptr)) < 0 || raw[ptr++] != '\n') - throw new CorruptObjectException( - JGitText.get().corruptObjectInvalidCommitter); + } } /** @@ -290,50 +500,47 @@ public class ObjectChecker { * @throws CorruptObjectException * if any error was detected. */ - public void checkTag(final byte[] raw) throws CorruptObjectException { - int ptr = 0; + public void checkTag(byte[] raw) throws CorruptObjectException { + checkTag(idFor(OBJ_TAG, raw), raw); + } - if ((ptr = match(raw, ptr, object)) < 0) - throw new CorruptObjectException( + /** + * Check an annotated tag for errors. + * + * @param id + * identity of the object being checked. + * @param raw + * the tag data. The array is never modified. + * @throws CorruptObjectException + * if any error was detected. + * @since 4.2 + */ + public void checkTag(@Nullable AnyObjectId id, byte[] raw) + throws CorruptObjectException { + bufPtr.value = 0; + if (!match(raw, object)) { + report(MISSING_OBJECT, id, JGitText.get().corruptObjectNoObjectHeader); - if ((ptr = id(raw, ptr)) < 0 || raw[ptr++] != '\n') - throw new CorruptObjectException( + } else if (!checkId(raw)) { + report(BAD_OBJECT_SHA1, id, JGitText.get().corruptObjectInvalidObject); + } - if ((ptr = match(raw, ptr, type)) < 0) - throw new CorruptObjectException( + if (!match(raw, type)) { + report(MISSING_TYPE_ENTRY, id, JGitText.get().corruptObjectNoTypeHeader); - ptr = nextLF(raw, ptr); + } + bufPtr.value = nextLF(raw, bufPtr.value); - if ((ptr = match(raw, ptr, tag)) < 0) - throw new CorruptObjectException( + if (!match(raw, tag)) { + report(MISSING_TAG_ENTRY, id, JGitText.get().corruptObjectNoTagHeader); - ptr = nextLF(raw, ptr); - - if ((ptr = match(raw, ptr, tagger)) > 0) { - if ((ptr = personIdent(raw, ptr)) < 0 || raw[ptr++] != '\n') - throw new CorruptObjectException( - JGitText.get().corruptObjectInvalidTagger); } - } - - private static int lastPathChar(final int mode) { - return FileMode.TREE.equals(mode) ? '/' : '\0'; - } + bufPtr.value = nextLF(raw, bufPtr.value); - private static int pathCompare(final byte[] raw, int aPos, final int aEnd, - final int aMode, int bPos, final int bEnd, final int bMode) { - while (aPos < aEnd && bPos < bEnd) { - final int cmp = (raw[aPos++] & 0xff) - (raw[bPos++] & 0xff); - if (cmp != 0) - return cmp; + if (match(raw, tagger)) { + checkPersonIdent(raw, id); } - - if (aPos < aEnd) - return (raw[aPos] & 0xff) - lastPathChar(bMode); - if (bPos < bEnd) - return lastPathChar(aMode) - (raw[bPos] & 0xff); - return 0; } private static boolean duplicateName(final byte[] raw, @@ -363,8 +570,9 @@ public class ObjectChecker { if (nextNamePos + 1 == nextPtr) return false; - final int cmp = pathCompare(raw, thisNamePos, thisNameEnd, - FileMode.TREE.getBits(), nextNamePos, nextPtr - 1, nextMode); + int cmp = compareSameName( + raw, thisNamePos, thisNameEnd, + raw, nextNamePos, nextPtr - 1, nextMode); if (cmp < 0) return false; else if (cmp == 0) @@ -382,7 +590,23 @@ public class ObjectChecker { * @throws CorruptObjectException * if any error was detected. */ - public void checkTree(final byte[] raw) throws CorruptObjectException { + public void checkTree(byte[] raw) throws CorruptObjectException { + checkTree(idFor(OBJ_TREE, raw), raw); + } + + /** + * Check a canonical formatted tree for errors. + * + * @param id + * identity of the object being checked. + * @param raw + * the raw tree data. The array is never modified. + * @throws CorruptObjectException + * if any error was detected. + * @since 4.2 + */ + public void checkTree(@Nullable AnyObjectId id, byte[] raw) + throws CorruptObjectException { final int sz = raw.length; int ptr = 0; int lastNameB = 0, lastNameE = 0, lastMode = 0; @@ -393,74 +617,90 @@ public class ObjectChecker { while (ptr < sz) { int thisMode = 0; for (;;) { - if (ptr == sz) + if (ptr == sz) { throw new CorruptObjectException( JGitText.get().corruptObjectTruncatedInMode); + } final byte c = raw[ptr++]; if (' ' == c) break; - if (c < '0' || c > '7') + if (c < '0' || c > '7') { throw new CorruptObjectException( JGitText.get().corruptObjectInvalidModeChar); - if (thisMode == 0 && c == '0' && !allowZeroMode) - throw new CorruptObjectException( + } + if (thisMode == 0 && c == '0') { + report(ZERO_PADDED_FILEMODE, id, JGitText.get().corruptObjectInvalidModeStartsZero); + } thisMode <<= 3; thisMode += c - '0'; } - if (FileMode.fromBits(thisMode).getObjectType() == Constants.OBJ_BAD) + if (FileMode.fromBits(thisMode).getObjectType() == OBJ_BAD) { throw new CorruptObjectException(MessageFormat.format( JGitText.get().corruptObjectInvalidMode2, Integer.valueOf(thisMode))); + } final int thisNameB = ptr; - ptr = scanPathSegment(raw, ptr, sz); - if (ptr == sz || raw[ptr] != 0) + ptr = scanPathSegment(raw, ptr, sz, id); + if (ptr == sz || raw[ptr] != 0) { throw new CorruptObjectException( JGitText.get().corruptObjectTruncatedInName); - checkPathSegment2(raw, thisNameB, ptr); + } + checkPathSegment2(raw, thisNameB, ptr, id); if (normalized != null) { - if (!normalized.add(normalize(raw, thisNameB, ptr))) - throw new CorruptObjectException( + if (!normalized.add(normalize(raw, thisNameB, ptr))) { + report(DUPLICATE_ENTRIES, id, JGitText.get().corruptObjectDuplicateEntryNames); - } else if (duplicateName(raw, thisNameB, ptr)) - throw new CorruptObjectException( + } + } else if (duplicateName(raw, thisNameB, ptr)) { + report(DUPLICATE_ENTRIES, id, JGitText.get().corruptObjectDuplicateEntryNames); + } if (lastNameB != 0) { - final int cmp = pathCompare(raw, lastNameB, lastNameE, - lastMode, thisNameB, ptr, thisMode); - if (cmp > 0) - throw new CorruptObjectException( + int cmp = compare( + raw, lastNameB, lastNameE, lastMode, + raw, thisNameB, ptr, thisMode); + if (cmp > 0) { + report(TREE_NOT_SORTED, id, JGitText.get().corruptObjectIncorrectSorting); + } } lastNameB = thisNameB; lastNameE = ptr; lastMode = thisMode; - ptr += 1 + Constants.OBJECT_ID_LENGTH; - if (ptr > sz) + ptr += 1 + OBJECT_ID_LENGTH; + if (ptr > sz) { throw new CorruptObjectException( JGitText.get().corruptObjectTruncatedInObjectId); + } + if (ObjectId.zeroId().compareTo(raw, ptr - OBJECT_ID_LENGTH) == 0) { + report(NULL_SHA1, id, JGitText.get().corruptObjectZeroId); + } } } - private int scanPathSegment(byte[] raw, int ptr, int end) - throws CorruptObjectException { + private int scanPathSegment(byte[] raw, int ptr, int end, + @Nullable AnyObjectId id) throws CorruptObjectException { for (; ptr < end; ptr++) { byte c = raw[ptr]; - if (c == 0) + if (c == 0) { return ptr; - if (c == '/') - throw new CorruptObjectException( + } + if (c == '/') { + report(FULL_PATHNAME, id, JGitText.get().corruptObjectNameContainsSlash); + } if (windows && isInvalidOnWindows(c)) { - if (c > 31) + if (c > 31) { throw new CorruptObjectException(String.format( JGitText.get().corruptObjectNameContainsChar, Byte.valueOf(c))); + } throw new CorruptObjectException(String.format( JGitText.get().corruptObjectNameContainsByte, Integer.valueOf(c & 0xff))); @@ -469,6 +709,26 @@ public class ObjectChecker { return ptr; } + @SuppressWarnings("resource") + @Nullable + private ObjectId idFor(int objType, byte[] raw) { + if (skipList != null) { + return new ObjectInserter.Formatter().idFor(objType, raw); + } + return null; + } + + private void report(@NonNull ErrorType err, @Nullable AnyObjectId id, + String why) throws CorruptObjectException { + if (errors.contains(err) + && (id == null || skipList == null || !skipList.contains(id))) { + if (id != null) { + throw new CorruptObjectException(err, id, why); + } + throw new CorruptObjectException(why); + } + } + /** * Check tree path entry for validity. *

@@ -519,73 +779,82 @@ public class ObjectChecker { */ public void checkPathSegment(byte[] raw, int ptr, int end) throws CorruptObjectException { - int e = scanPathSegment(raw, ptr, end); + int e = scanPathSegment(raw, ptr, end, null); if (e < end && raw[e] == 0) throw new CorruptObjectException( JGitText.get().corruptObjectNameContainsNullByte); - checkPathSegment2(raw, ptr, end); + checkPathSegment2(raw, ptr, end, null); } - private void checkPathSegment2(byte[] raw, int ptr, int end) - throws CorruptObjectException { - if (ptr == end) - throw new CorruptObjectException( - JGitText.get().corruptObjectNameZeroLength); + private void checkPathSegment2(byte[] raw, int ptr, int end, + @Nullable AnyObjectId id) throws CorruptObjectException { + if (ptr == end) { + report(EMPTY_NAME, id, JGitText.get().corruptObjectNameZeroLength); + return; + } + if (raw[ptr] == '.') { switch (end - ptr) { case 1: - throw new CorruptObjectException( - JGitText.get().corruptObjectNameDot); + report(HAS_DOT, id, JGitText.get().corruptObjectNameDot); + break; case 2: - if (raw[ptr + 1] == '.') - throw new CorruptObjectException( + if (raw[ptr + 1] == '.') { + report(HAS_DOTDOT, id, JGitText.get().corruptObjectNameDotDot); + } break; case 4: - if (isGit(raw, ptr + 1)) - throw new CorruptObjectException(String.format( + if (isGit(raw, ptr + 1)) { + report(HAS_DOTGIT, id, String.format( JGitText.get().corruptObjectInvalidName, RawParseUtils.decode(raw, ptr, end))); + } break; default: - if (end - ptr > 4 && isNormalizedGit(raw, ptr + 1, end)) - throw new CorruptObjectException(String.format( + if (end - ptr > 4 && isNormalizedGit(raw, ptr + 1, end)) { + report(HAS_DOTGIT, id, String.format( JGitText.get().corruptObjectInvalidName, RawParseUtils.decode(raw, ptr, end))); + } } } else if (isGitTilde1(raw, ptr, end)) { - throw new CorruptObjectException(String.format( + report(HAS_DOTGIT, id, String.format( JGitText.get().corruptObjectInvalidName, RawParseUtils.decode(raw, ptr, end))); } - - if (macosx && isMacHFSGit(raw, ptr, end)) - throw new CorruptObjectException(String.format( + if (macosx && isMacHFSGit(raw, ptr, end, id)) { + report(HAS_DOTGIT, id, String.format( JGitText.get().corruptObjectInvalidNameIgnorableUnicode, RawParseUtils.decode(raw, ptr, end))); + } if (windows) { // Windows ignores space and dot at end of file name. - if (raw[end - 1] == ' ' || raw[end - 1] == '.') - throw new CorruptObjectException(String.format( + if (raw[end - 1] == ' ' || raw[end - 1] == '.') { + report(WIN32_BAD_NAME, id, String.format( JGitText.get().corruptObjectInvalidNameEnd, Character.valueOf(((char) raw[end - 1])))); - if (end - ptr >= 3) - checkNotWindowsDevice(raw, ptr, end); + } + if (end - ptr >= 3) { + checkNotWindowsDevice(raw, ptr, end, id); + } } } // Mac's HFS+ folds permutations of ".git" and Unicode ignorable characters // to ".git" therefore we should prevent such names - private static boolean isMacHFSGit(byte[] raw, int ptr, int end) - throws CorruptObjectException { + private boolean isMacHFSGit(byte[] raw, int ptr, int end, + @Nullable AnyObjectId id) throws CorruptObjectException { boolean ignorable = false; byte[] git = new byte[] { '.', 'g', 'i', 't' }; int g = 0; while (ptr < end) { switch (raw[ptr]) { case (byte) 0xe2: // http://www.utf8-chartable.de/unicode-utf8-table.pl?start=8192 - checkTruncatedIgnorableUTF8(raw, ptr, end); + if (!checkTruncatedIgnorableUTF8(raw, ptr, end, id)) { + return false; + } switch (raw[ptr + 1]) { case (byte) 0x80: switch (raw[ptr + 2]) { @@ -622,7 +891,9 @@ public class ObjectChecker { return false; } case (byte) 0xef: // http://www.utf8-chartable.de/unicode-utf8-table.pl?start=65024 - checkTruncatedIgnorableUTF8(raw, ptr, end); + if (!checkTruncatedIgnorableUTF8(raw, ptr, end, id)) { + return false; + } // U+FEFF 0xefbbbf ZERO WIDTH NO-BREAK SPACE if ((raw[ptr + 1] == (byte) 0xbb) && (raw[ptr + 2] == (byte) 0xbf)) { @@ -643,12 +914,15 @@ public class ObjectChecker { return false; } - private static void checkTruncatedIgnorableUTF8(byte[] raw, int ptr, int end) - throws CorruptObjectException { - if ((ptr + 2) >= end) - throw new CorruptObjectException(MessageFormat.format( + private boolean checkTruncatedIgnorableUTF8(byte[] raw, int ptr, int end, + @Nullable AnyObjectId id) throws CorruptObjectException { + if ((ptr + 2) >= end) { + report(BAD_UTF8, id, MessageFormat.format( JGitText.get().corruptObjectInvalidNameInvalidUtf8, toHexString(raw, ptr, end))); + return false; + } + return true; } private static String toHexString(byte[] raw, int ptr, int end) { @@ -658,33 +932,36 @@ public class ObjectChecker { return b.toString(); } - private static void checkNotWindowsDevice(byte[] raw, int ptr, int end) - throws CorruptObjectException { + private void checkNotWindowsDevice(byte[] raw, int ptr, int end, + @Nullable AnyObjectId id) throws CorruptObjectException { switch (toLower(raw[ptr])) { case 'a': // AUX if (end - ptr >= 3 && toLower(raw[ptr + 1]) == 'u' && toLower(raw[ptr + 2]) == 'x' - && (end - ptr == 3 || raw[ptr + 3] == '.')) - throw new CorruptObjectException( + && (end - ptr == 3 || raw[ptr + 3] == '.')) { + report(WIN32_BAD_NAME, id, JGitText.get().corruptObjectInvalidNameAux); + } break; case 'c': // CON, COM[1-9] if (end - ptr >= 3 && toLower(raw[ptr + 2]) == 'n' && toLower(raw[ptr + 1]) == 'o' - && (end - ptr == 3 || raw[ptr + 3] == '.')) - throw new CorruptObjectException( + && (end - ptr == 3 || raw[ptr + 3] == '.')) { + report(WIN32_BAD_NAME, id, JGitText.get().corruptObjectInvalidNameCon); + } if (end - ptr >= 4 && toLower(raw[ptr + 2]) == 'm' && toLower(raw[ptr + 1]) == 'o' && isPositiveDigit(raw[ptr + 3]) - && (end - ptr == 4 || raw[ptr + 4] == '.')) - throw new CorruptObjectException(String.format( + && (end - ptr == 4 || raw[ptr + 4] == '.')) { + report(WIN32_BAD_NAME, id, String.format( JGitText.get().corruptObjectInvalidNameCom, Character.valueOf(((char) raw[ptr + 3])))); + } break; case 'l': // LPT[1-9] @@ -692,28 +969,31 @@ public class ObjectChecker { && toLower(raw[ptr + 1]) == 'p' && toLower(raw[ptr + 2]) == 't' && isPositiveDigit(raw[ptr + 3]) - && (end - ptr == 4 || raw[ptr + 4] == '.')) - throw new CorruptObjectException(String.format( + && (end - ptr == 4 || raw[ptr + 4] == '.')) { + report(WIN32_BAD_NAME, id, String.format( JGitText.get().corruptObjectInvalidNameLpt, Character.valueOf(((char) raw[ptr + 3])))); + } break; case 'n': // NUL if (end - ptr >= 3 && toLower(raw[ptr + 1]) == 'u' && toLower(raw[ptr + 2]) == 'l' - && (end - ptr == 3 || raw[ptr + 3] == '.')) - throw new CorruptObjectException( + && (end - ptr == 3 || raw[ptr + 3] == '.')) { + report(WIN32_BAD_NAME, id, JGitText.get().corruptObjectInvalidNameNul); + } break; case 'p': // PRN if (end - ptr >= 3 && toLower(raw[ptr + 1]) == 'r' && toLower(raw[ptr + 2]) == 'n' - && (end - ptr == 3 || raw[ptr + 3] == '.')) - throw new CorruptObjectException( + && (end - ptr == 3 || raw[ptr + 3] == '.')) { + report(WIN32_BAD_NAME, id, JGitText.get().corruptObjectInvalidNamePrn); + } break; } } @@ -766,6 +1046,15 @@ public class ObjectChecker { return false; } + private boolean match(byte[] b, byte[] src) { + int r = RawParseUtils.match(b, bufPtr.value, src); + if (r < 0) { + return false; + } + bufPtr.value = r; + return true; + } + private static char toLower(byte b) { if ('A' <= b && b <= 'Z') return (char) (b + ('a' - 'A')); @@ -790,58 +1079,6 @@ public class ObjectChecker { private String normalize(byte[] raw, int ptr, int end) { String n = RawParseUtils.decode(raw, ptr, end).toLowerCase(Locale.US); - return macosx ? Normalizer.normalize(n) : n; - } - - private static class Normalizer { - // TODO Simplify invocation to Normalizer after dropping Java 5. - private static final Method normalize; - private static final Object nfc; - static { - Method method; - Object formNfc; - try { - Class formClazz = Class.forName("java.text.Normalizer$Form"); //$NON-NLS-1$ - formNfc = formClazz.getField("NFC").get(null); //$NON-NLS-1$ - method = Class.forName("java.text.Normalizer") //$NON-NLS-1$ - .getMethod("normalize", CharSequence.class, formClazz); //$NON-NLS-1$ - } catch (ClassNotFoundException e) { - method = null; - formNfc = null; - } catch (NoSuchFieldException e) { - method = null; - formNfc = null; - } catch (NoSuchMethodException e) { - method = null; - formNfc = null; - } catch (SecurityException e) { - method = null; - formNfc = null; - } catch (IllegalArgumentException e) { - method = null; - formNfc = null; - } catch (IllegalAccessException e) { - method = null; - formNfc = null; - } - normalize = method; - nfc = formNfc; - } - - static String normalize(String in) { - if (normalize == null) - return in; - try { - return (String) normalize.invoke(null, in, nfc); - } catch (IllegalAccessException e) { - return in; - } catch (InvocationTargetException e) { - if (e.getCause() instanceof RuntimeException) - throw (RuntimeException) e.getCause(); - if (e.getCause() instanceof Error) - throw (Error) e.getCause(); - return in; - } - } + return macosx ? Normalizer.normalize(n, Normalizer.Form.NFC) : n; } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdOwnerMap.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdOwnerMap.java index 95b16d917..442261cbd 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdOwnerMap.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdOwnerMap.java @@ -67,8 +67,8 @@ import java.util.NoSuchElementException; * @param * type of subclass of ObjectId that will be stored in the map. */ -public class ObjectIdOwnerMap implements - Iterable { +public class ObjectIdOwnerMap + implements Iterable, ObjectIdSet { /** Size of the initial directory, will grow as necessary. */ private static final int INITIAL_DIRECTORY = 1024; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdRef.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdRef.java index f481c772d..c286f5e46 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdRef.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdRef.java @@ -44,6 +44,9 @@ package org.eclipse.jgit.lib; +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.annotations.Nullable; + /** A {@link Ref} that points directly at an {@link ObjectId}. */ public abstract class ObjectIdRef implements Ref { /** Any reference whose peeled value is not yet known. */ @@ -56,13 +59,15 @@ public abstract class ObjectIdRef implements Ref { * @param name * name of this ref. * @param id - * current value of the ref. May be null to indicate a ref - * that does not exist yet. + * current value of the ref. May be {@code null} to indicate + * a ref that does not exist yet. */ - public Unpeeled(Storage st, String name, ObjectId id) { + public Unpeeled(@NonNull Storage st, @NonNull String name, + @Nullable ObjectId id) { super(st, name, id); } + @Nullable public ObjectId getPeeledObjectId() { return null; } @@ -88,11 +93,13 @@ public abstract class ObjectIdRef implements Ref { * @param p * the first non-tag object that tag {@code id} points to. */ - public PeeledTag(Storage st, String name, ObjectId id, ObjectId p) { + public PeeledTag(@NonNull Storage st, @NonNull String name, + @Nullable ObjectId id, @NonNull ObjectId p) { super(st, name, id); peeledObjectId = p; } + @NonNull public ObjectId getPeeledObjectId() { return peeledObjectId; } @@ -112,13 +119,15 @@ public abstract class ObjectIdRef implements Ref { * @param name * name of this ref. * @param id - * current value of the ref. May be null to indicate a ref - * that does not exist yet. + * current value of the ref. May be {@code null} to indicate + * a ref that does not exist yet. */ - public PeeledNonTag(Storage st, String name, ObjectId id) { + public PeeledNonTag(@NonNull Storage st, @NonNull String name, + @Nullable ObjectId id) { super(st, name, id); } + @Nullable public ObjectId getPeeledObjectId() { return null; } @@ -142,15 +151,17 @@ public abstract class ObjectIdRef implements Ref { * @param name * name of this ref. * @param id - * current value of the ref. May be null to indicate a ref that - * does not exist yet. + * current value of the ref. May be {@code null} to indicate a + * ref that does not exist yet. */ - protected ObjectIdRef(Storage st, String name, ObjectId id) { + protected ObjectIdRef(@NonNull Storage st, @NonNull String name, + @Nullable ObjectId id) { this.name = name; this.storage = st; this.objectId = id; } + @NonNull public String getName() { return name; } @@ -159,22 +170,27 @@ public abstract class ObjectIdRef implements Ref { return false; } + @NonNull public Ref getLeaf() { return this; } + @NonNull public Ref getTarget() { return this; } + @Nullable public ObjectId getObjectId() { return objectId; } + @NonNull public Storage getStorage() { return storage; } + @NonNull @Override public String toString() { StringBuilder r = new StringBuilder(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/SymlinkTreeEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdSet.java similarity index 61% rename from org.eclipse.jgit/src/org/eclipse/jgit/lib/SymlinkTreeEntry.java rename to org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdSet.java index c7e41bce0..0b5848463 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/SymlinkTreeEntry.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdSet.java @@ -1,6 +1,5 @@ /* - * Copyright (C) 2007, Robin Rosenberg - * Copyright (C) 2006-2007, Shawn O. Pearce + * Copyright (C) 2015, Google Inc. * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -45,41 +44,21 @@ package org.eclipse.jgit.lib; /** - * A tree entry representing a symbolic link. + * Simple set of ObjectIds. + *

+ * Usually backed by a read-only data structure such as + * {@link org.eclipse.jgit.internal.storage.file.PackIndex}. Mutable types like + * {@link ObjectIdOwnerMap} also implement the interface by checking keys. * - * Note. Java cannot really handle these as file system objects. - * - * @deprecated To look up information about a single path, use - * {@link org.eclipse.jgit.treewalk.TreeWalk#forPath(Repository, String, org.eclipse.jgit.revwalk.RevTree)}. - * To lookup information about multiple paths at once, use a - * {@link org.eclipse.jgit.treewalk.TreeWalk} and obtain the current entry's - * information from its getter methods. + * @since 4.2 */ -@Deprecated -public class SymlinkTreeEntry extends TreeEntry { - +public interface ObjectIdSet { /** - * Construct a {@link SymlinkTreeEntry} with the specified name and SHA-1 in - * the specified parent + * Returns true if the objectId is contained within the collection. * - * @param parent - * @param id - * @param nameUTF8 + * @param objectId + * the objectId to find + * @return whether the collection contains the objectId. */ - public SymlinkTreeEntry(final Tree parent, final ObjectId id, - final byte[] nameUTF8) { - super(parent, id, nameUTF8); - } - - public FileMode getMode() { - return FileMode.SYMLINK; - } - - public String toString() { - final StringBuilder r = new StringBuilder(); - r.append(ObjectId.toString(getId())); - r.append(" S "); //$NON-NLS-1$ - r.append(getFullName()); - return r.toString(); - } + boolean contains(AnyObjectId objectId); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdSubclassMap.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdSubclassMap.java index 48aa109e7..faed64bfe 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdSubclassMap.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdSubclassMap.java @@ -60,7 +60,8 @@ import java.util.NoSuchElementException; * @param * type of subclass of ObjectId that will be stored in the map. */ -public class ObjectIdSubclassMap implements Iterable { +public class ObjectIdSubclassMap + implements Iterable, ObjectIdSet { private static final int INITIAL_TABLE_SIZE = 2048; int size; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Ref.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Ref.java index f119c44fe..a78a90fe5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Ref.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Ref.java @@ -43,6 +43,9 @@ package org.eclipse.jgit.lib; +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.annotations.Nullable; + /** * Pairing of a name and the {@link ObjectId} it currently has. *

@@ -126,6 +129,7 @@ public interface Ref { * * @return name of this ref. */ + @NonNull public String getName(); /** @@ -156,6 +160,7 @@ public interface Ref { * * @return the reference that actually stores the ObjectId value. */ + @NonNull public abstract Ref getLeaf(); /** @@ -170,22 +175,27 @@ public interface Ref { * * @return the target reference, or {@code this}. */ + @NonNull public abstract Ref getTarget(); /** * Cached value of this ref. * - * @return the value of this ref at the last time we read it. + * @return the value of this ref at the last time we read it. May be + * {@code null} to indicate a ref that does not exist yet or a + * symbolic ref pointing to an unborn branch. */ + @Nullable public abstract ObjectId getObjectId(); /** * Cached value of ref^{} (the ref peeled to commit). * * @return if this ref is an annotated tag the id of the commit (or tree or - * blob) that the annotated tag refers to; null if this ref does not - * refer to an annotated tag. + * blob) that the annotated tag refers to; {@code null} if this ref + * does not refer to an annotated tag. */ + @Nullable public abstract ObjectId getPeeledObjectId(); /** @@ -201,5 +211,6 @@ public interface Ref { * * @return type of ref. */ + @NonNull public abstract Storage getStorage(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java index 986666f2f..c0c3862c8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java @@ -82,8 +82,10 @@ public abstract class RefDatabase { *

* If the reference is nested deeper than this depth, the implementation * should either fail, or at least claim the reference does not exist. + * + * @since 4.2 */ - protected static final int MAX_SYMBOLIC_REF_DEPTH = 5; + public static final int MAX_SYMBOLIC_REF_DEPTH = 5; /** Magic value for {@link #getRefs(String)} to return all references. */ public static final String ALL = "";//$NON-NLS-1$ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefWriter.java index 747fa62b5..3a02b2281 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefWriter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefWriter.java @@ -119,13 +119,20 @@ public abstract class RefWriter { continue; } - r.getObjectId().copyTo(tmp, w); + ObjectId objectId = r.getObjectId(); + if (objectId == null) { + // Symrefs to unborn branches aren't advertised in the info/refs + // file. + continue; + } + objectId.copyTo(tmp, w); w.write('\t'); w.write(r.getName()); w.write('\n'); - if (r.getPeeledObjectId() != null) { - r.getPeeledObjectId().copyTo(tmp, w); + ObjectId peeledObjectId = r.getPeeledObjectId(); + if (peeledObjectId != null) { + peeledObjectId.copyTo(tmp, w); w.write('\t'); w.write(r.getName()); w.write("^{}\n"); //$NON-NLS-1$ @@ -167,14 +174,21 @@ public abstract class RefWriter { if (r.getStorage() != Ref.Storage.PACKED) continue; - r.getObjectId().copyTo(tmp, w); + ObjectId objectId = r.getObjectId(); + if (objectId == null) { + // A packed ref cannot be a symref, let alone a symref + // to an unborn branch. + throw new NullPointerException(); + } + objectId.copyTo(tmp, w); w.write(' '); w.write(r.getName()); w.write('\n'); - if (r.getPeeledObjectId() != null) { + ObjectId peeledObjectId = r.getPeeledObjectId(); + if (peeledObjectId != null) { w.write('^'); - r.getPeeledObjectId().copyTo(tmp, w); + peeledObjectId.copyTo(tmp, w); w.write('\n'); } } 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 49a970d03..f8266133a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java @@ -911,12 +911,16 @@ public abstract class Repository implements AutoCloseable { @Nullable public String getFullBranch() throws IOException { Ref head = getRef(Constants.HEAD); - if (head == null) + if (head == null) { return null; - if (head.isSymbolic()) + } + if (head.isSymbolic()) { return head.getTarget().getName(); - if (head.getObjectId() != null) - return head.getObjectId().name(); + } + ObjectId objectId = head.getObjectId(); + if (objectId != null) { + return objectId.name(); + } return null; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/SymbolicRef.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/SymbolicRef.java index 43b1510f9..eeab921a7 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/SymbolicRef.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/SymbolicRef.java @@ -43,6 +43,9 @@ package org.eclipse.jgit.lib; +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.annotations.Nullable; + /** * A reference that indirectly points at another {@link Ref}. *

@@ -62,11 +65,12 @@ public class SymbolicRef implements Ref { * @param target * the ref we reference and derive our value from. */ - public SymbolicRef(String refName, Ref target) { + public SymbolicRef(@NonNull String refName, @NonNull Ref target) { this.name = refName; this.target = target; } + @NonNull public String getName() { return name; } @@ -75,6 +79,7 @@ public class SymbolicRef implements Ref { return true; } + @NonNull public Ref getLeaf() { Ref dst = getTarget(); while (dst.isSymbolic()) @@ -82,18 +87,22 @@ public class SymbolicRef implements Ref { return dst; } + @NonNull public Ref getTarget() { return target; } + @Nullable public ObjectId getObjectId() { return getLeaf().getObjectId(); } + @NonNull public Storage getStorage() { return Storage.LOOSE; } + @Nullable public ObjectId getPeeledObjectId() { return getLeaf().getPeeledObjectId(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Tree.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Tree.java deleted file mode 100644 index 43bd489dc..000000000 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Tree.java +++ /dev/null @@ -1,601 +0,0 @@ -/* - * Copyright (C) 2007, Robin Rosenberg - * Copyright (C) 2007-2008, Robin Rosenberg - * Copyright (C) 2006-2008, Shawn O. Pearce - * and other copyright owners as documented in the project's IP log. - * - * This program and the accompanying materials are made available - * under the terms of the Eclipse Distribution License v1.0 which - * accompanies this distribution, is reproduced below, and is - * available at http://www.eclipse.org/org/documents/edl-v10.php - * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or - * without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * - Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * - * - Neither the name of the Eclipse Foundation, Inc. nor the - * names of its contributors may be used to endorse or promote - * products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND - * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, - * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package org.eclipse.jgit.lib; - -import java.io.IOException; -import java.text.MessageFormat; - -import org.eclipse.jgit.errors.CorruptObjectException; -import org.eclipse.jgit.errors.EntryExistsException; -import org.eclipse.jgit.errors.MissingObjectException; -import org.eclipse.jgit.errors.ObjectWritingException; -import org.eclipse.jgit.internal.JGitText; -import org.eclipse.jgit.util.RawParseUtils; - -/** - * A representation of a Git tree entry. A Tree is a directory in Git. - * - * @deprecated To look up information about a single path, use - * {@link org.eclipse.jgit.treewalk.TreeWalk#forPath(Repository, String, org.eclipse.jgit.revwalk.RevTree)}. - * To lookup information about multiple paths at once, use a - * {@link org.eclipse.jgit.treewalk.TreeWalk} and obtain the current entry's - * information from its getter methods. - */ -@Deprecated -public class Tree extends TreeEntry { - private static final TreeEntry[] EMPTY_TREE = {}; - - /** - * Compare two names represented as bytes. Since git treats names of trees and - * blobs differently we have one parameter that represents a '/' for trees. For - * other objects the value should be NUL. The names are compare by their positive - * byte value (0..255). - * - * A blob and a tree with the same name will not compare equal. - * - * @param a name - * @param b name - * @param lasta '/' if a is a tree, else NUL - * @param lastb '/' if b is a tree, else NUL - * - * @return < 0 if a is sorted before b, 0 if they are the same, else b - */ - public static final int compareNames(final byte[] a, final byte[] b, final int lasta,final int lastb) { - return compareNames(a, b, 0, b.length, lasta, lastb); - } - - private static final int compareNames(final byte[] a, final byte[] nameUTF8, - final int nameStart, final int nameEnd, final int lasta, int lastb) { - int j,k; - for (j = 0, k = nameStart; j < a.length && k < nameEnd; j++, k++) { - final int aj = a[j] & 0xff; - final int bk = nameUTF8[k] & 0xff; - if (aj < bk) - return -1; - else if (aj > bk) - return 1; - } - if (j < a.length) { - int aj = a[j]&0xff; - if (aj < lastb) - return -1; - else if (aj > lastb) - return 1; - else - if (j == a.length - 1) - return 0; - else - return -1; - } - if (k < nameEnd) { - int bk = nameUTF8[k] & 0xff; - if (lasta < bk) - return -1; - else if (lasta > bk) - return 1; - else - if (k == nameEnd - 1) - return 0; - else - return 1; - } - if (lasta < lastb) - return -1; - else if (lasta > lastb) - return 1; - - final int namelength = nameEnd - nameStart; - if (a.length == namelength) - return 0; - else if (a.length < namelength) - return -1; - else - return 1; - } - - private static final byte[] substring(final byte[] s, final int nameStart, - final int nameEnd) { - if (nameStart == 0 && nameStart == s.length) - return s; - final byte[] n = new byte[nameEnd - nameStart]; - System.arraycopy(s, nameStart, n, 0, n.length); - return n; - } - - private static final int binarySearch(final TreeEntry[] entries, - final byte[] nameUTF8, final int nameUTF8last, final int nameStart, final int nameEnd) { - if (entries.length == 0) - return -1; - int high = entries.length; - int low = 0; - do { - final int mid = (low + high) >>> 1; - final int cmp = compareNames(entries[mid].getNameUTF8(), nameUTF8, - nameStart, nameEnd, TreeEntry.lastChar(entries[mid]), nameUTF8last); - if (cmp < 0) - low = mid + 1; - else if (cmp == 0) - return mid; - else - high = mid; - } while (low < high); - return -(low + 1); - } - - private final Repository db; - - private TreeEntry[] contents; - - /** - * Constructor for a new Tree - * - * @param repo The repository that owns the Tree. - */ - public Tree(final Repository repo) { - super(null, null, null); - db = repo; - contents = EMPTY_TREE; - } - - /** - * Construct a Tree object with known content and hash value - * - * @param repo - * @param myId - * @param raw - * @throws IOException - */ - public Tree(final Repository repo, final ObjectId myId, final byte[] raw) - throws IOException { - super(null, myId, null); - db = repo; - readTree(raw); - } - - /** - * Construct a new Tree under another Tree - * - * @param parent - * @param nameUTF8 - */ - public Tree(final Tree parent, final byte[] nameUTF8) { - super(parent, null, nameUTF8); - db = parent.getRepository(); - contents = EMPTY_TREE; - } - - /** - * Construct a Tree with a known SHA-1 under another tree. Data is not yet - * specified and will have to be loaded on demand. - * - * @param parent - * @param id - * @param nameUTF8 - */ - public Tree(final Tree parent, final ObjectId id, final byte[] nameUTF8) { - super(parent, id, nameUTF8); - db = parent.getRepository(); - } - - public FileMode getMode() { - return FileMode.TREE; - } - - /** - * @return true if this Tree is the top level Tree. - */ - public boolean isRoot() { - return getParent() == null; - } - - public Repository getRepository() { - return db; - } - - /** - * @return true of the data of this Tree is loaded - */ - public boolean isLoaded() { - return contents != null; - } - - /** - * Forget the in-memory data for this tree. - */ - public void unload() { - if (isModified()) - throw new IllegalStateException(JGitText.get().cannotUnloadAModifiedTree); - contents = null; - } - - /** - * Adds a new or existing file with the specified name to this tree. - * Trees are added if necessary as the name may contain '/':s. - * - * @param name Name - * @return a {@link FileTreeEntry} for the added file. - * @throws IOException - */ - public FileTreeEntry addFile(final String name) throws IOException { - return addFile(Repository.gitInternalSlash(Constants.encode(name)), 0); - } - - /** - * Adds a new or existing file with the specified name to this tree. - * Trees are added if necessary as the name may contain '/':s. - * - * @param s an array containing the name - * @param offset when the name starts in the tree. - * - * @return a {@link FileTreeEntry} for the added file. - * @throws IOException - */ - public FileTreeEntry addFile(final byte[] s, final int offset) - throws IOException { - int slash; - int p; - - for (slash = offset; slash < s.length && s[slash] != '/'; slash++) { - // search for path component terminator - } - - ensureLoaded(); - byte xlast = slash= 0 && slash < s.length && contents[p] instanceof Tree) - return ((Tree) contents[p]).addFile(s, slash + 1); - - final byte[] newName = substring(s, offset, slash); - if (p >= 0) - throw new EntryExistsException(RawParseUtils.decode(newName)); - else if (slash < s.length) { - final Tree t = new Tree(this, newName); - insertEntry(p, t); - return t.addFile(s, slash + 1); - } else { - final FileTreeEntry f = new FileTreeEntry(this, null, newName, - false); - insertEntry(p, f); - return f; - } - } - - /** - * Adds a new or existing Tree with the specified name to this tree. - * Trees are added if necessary as the name may contain '/':s. - * - * @param name Name - * @return a {@link FileTreeEntry} for the added tree. - * @throws IOException - */ - public Tree addTree(final String name) throws IOException { - return addTree(Repository.gitInternalSlash(Constants.encode(name)), 0); - } - - /** - * Adds a new or existing Tree with the specified name to this tree. - * Trees are added if necessary as the name may contain '/':s. - * - * @param s an array containing the name - * @param offset when the name starts in the tree. - * - * @return a {@link FileTreeEntry} for the added tree. - * @throws IOException - */ - public Tree addTree(final byte[] s, final int offset) throws IOException { - int slash; - int p; - - for (slash = offset; slash < s.length && s[slash] != '/'; slash++) { - // search for path component terminator - } - - ensureLoaded(); - p = binarySearch(contents, s, (byte)'/', offset, slash); - if (p >= 0 && slash < s.length && contents[p] instanceof Tree) - return ((Tree) contents[p]).addTree(s, slash + 1); - - final byte[] newName = substring(s, offset, slash); - if (p >= 0) - throw new EntryExistsException(RawParseUtils.decode(newName)); - - final Tree t = new Tree(this, newName); - insertEntry(p, t); - return slash == s.length ? t : t.addTree(s, slash + 1); - } - - /** - * Add the specified tree entry to this tree. - * - * @param e - * @throws IOException - */ - public void addEntry(final TreeEntry e) throws IOException { - final int p; - - ensureLoaded(); - p = binarySearch(contents, e.getNameUTF8(), TreeEntry.lastChar(e), 0, e.getNameUTF8().length); - if (p < 0) { - e.attachParent(this); - insertEntry(p, e); - } else { - throw new EntryExistsException(e.getName()); - } - } - - private void insertEntry(int p, final TreeEntry e) { - final TreeEntry[] c = contents; - final TreeEntry[] n = new TreeEntry[c.length + 1]; - p = -(p + 1); - for (int k = c.length - 1; k >= p; k--) - n[k + 1] = c[k]; - n[p] = e; - for (int k = p - 1; k >= 0; k--) - n[k] = c[k]; - contents = n; - setModified(); - } - - void removeEntry(final TreeEntry e) { - final TreeEntry[] c = contents; - final int p = binarySearch(c, e.getNameUTF8(), TreeEntry.lastChar(e), 0, - e.getNameUTF8().length); - if (p >= 0) { - final TreeEntry[] n = new TreeEntry[c.length - 1]; - for (int k = c.length - 1; k > p; k--) - n[k - 1] = c[k]; - for (int k = p - 1; k >= 0; k--) - n[k] = c[k]; - contents = n; - setModified(); - } - } - - /** - * @return number of members in this tree - * @throws IOException - */ - public int memberCount() throws IOException { - ensureLoaded(); - return contents.length; - } - - /** - * Return all members of the tree sorted in Git order. - * - * Entries are sorted by the numerical unsigned byte - * values with (sub)trees having an implicit '/'. An - * example of a tree with three entries. a:b is an - * actual file name here. - * - *

- * 100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a.b - * 040000 tree 4277b6e69d25e5efa77c455340557b384a4c018a a - * 100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a:b - * - * @return all entries in this Tree, sorted. - * @throws IOException - */ - public TreeEntry[] members() throws IOException { - ensureLoaded(); - final TreeEntry[] c = contents; - if (c.length != 0) { - final TreeEntry[] r = new TreeEntry[c.length]; - for (int k = c.length - 1; k >= 0; k--) - r[k] = c[k]; - return r; - } else - return c; - } - - private boolean exists(final String s, byte slast) throws IOException { - return findMember(s, slast) != null; - } - - /** - * @param path to the tree. - * @return true if a tree with the specified path can be found under this - * tree. - * @throws IOException - */ - public boolean existsTree(String path) throws IOException { - return exists(path,(byte)'/'); - } - - /** - * @param path of the non-tree entry. - * @return true if a blob, symlink, or gitlink with the specified name - * can be found under this tree. - * @throws IOException - */ - public boolean existsBlob(String path) throws IOException { - return exists(path,(byte)0); - } - - private TreeEntry findMember(final String s, byte slast) throws IOException { - return findMember(Repository.gitInternalSlash(Constants.encode(s)), slast, 0); - } - - private TreeEntry findMember(final byte[] s, final byte slast, final int offset) - throws IOException { - int slash; - int p; - - for (slash = offset; slash < s.length && s[slash] != '/'; slash++) { - // search for path component terminator - } - - ensureLoaded(); - byte xlast = slash= 0) { - final TreeEntry r = contents[p]; - if (slash < s.length-1) - return r instanceof Tree ? ((Tree) r).findMember(s, slast, slash + 1) - : null; - return r; - } - return null; - } - - /** - * @param s - * blob name - * @return a {@link TreeEntry} representing an object with the specified - * relative path. - * @throws IOException - */ - public TreeEntry findBlobMember(String s) throws IOException { - return findMember(s,(byte)0); - } - - /** - * @param s Tree Name - * @return a Tree with the name s or null - * @throws IOException - */ - public TreeEntry findTreeMember(String s) throws IOException { - return findMember(s,(byte)'/'); - } - - private void ensureLoaded() throws IOException, MissingObjectException { - if (!isLoaded()) { - ObjectLoader ldr = db.open(getId(), Constants.OBJ_TREE); - readTree(ldr.getCachedBytes()); - } - } - - private void readTree(final byte[] raw) throws IOException { - final int rawSize = raw.length; - int rawPtr = 0; - TreeEntry[] temp; - int nextIndex = 0; - - while (rawPtr < rawSize) { - while (rawPtr < rawSize && raw[rawPtr] != 0) - rawPtr++; - rawPtr++; - rawPtr += Constants.OBJECT_ID_LENGTH; - nextIndex++; - } - - temp = new TreeEntry[nextIndex]; - rawPtr = 0; - nextIndex = 0; - while (rawPtr < rawSize) { - int c = raw[rawPtr++]; - if (c < '0' || c > '7') - throw new CorruptObjectException(getId(), JGitText.get().corruptObjectInvalidEntryMode); - int mode = c - '0'; - for (;;) { - c = raw[rawPtr++]; - if (' ' == c) - break; - else if (c < '0' || c > '7') - throw new CorruptObjectException(getId(), JGitText.get().corruptObjectInvalidMode); - mode <<= 3; - mode += c - '0'; - } - - int nameLen = 0; - while (raw[rawPtr + nameLen] != 0) - nameLen++; - final byte[] name = new byte[nameLen]; - System.arraycopy(raw, rawPtr, name, 0, nameLen); - rawPtr += nameLen + 1; - - final ObjectId id = ObjectId.fromRaw(raw, rawPtr); - rawPtr += Constants.OBJECT_ID_LENGTH; - - final TreeEntry ent; - if (FileMode.REGULAR_FILE.equals(mode)) - ent = new FileTreeEntry(this, id, name, false); - else if (FileMode.EXECUTABLE_FILE.equals(mode)) - ent = new FileTreeEntry(this, id, name, true); - else if (FileMode.TREE.equals(mode)) - ent = new Tree(this, id, name); - else if (FileMode.SYMLINK.equals(mode)) - ent = new SymlinkTreeEntry(this, id, name); - else if (FileMode.GITLINK.equals(mode)) - ent = new GitlinkTreeEntry(this, id, name); - else - throw new CorruptObjectException(getId(), MessageFormat.format( - JGitText.get().corruptObjectInvalidMode2, Integer.toOctalString(mode))); - temp[nextIndex++] = ent; - } - - contents = temp; - } - - /** - * Format this Tree in canonical format. - * - * @return canonical encoding of the tree object. - * @throws IOException - * the tree cannot be loaded, or its not in a writable state. - */ - public byte[] format() throws IOException { - TreeFormatter fmt = new TreeFormatter(); - for (TreeEntry e : members()) { - ObjectId id = e.getId(); - if (id == null) - throw new ObjectWritingException(MessageFormat.format(JGitText - .get().objectAtPathDoesNotHaveId, e.getFullName())); - - fmt.append(e.getNameUTF8(), e.getMode(), id); - } - return fmt.toByteArray(); - } - - public String toString() { - final StringBuilder r = new StringBuilder(); - r.append(ObjectId.toString(getId())); - r.append(" T "); //$NON-NLS-1$ - r.append(getFullName()); - return r.toString(); - } - -} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TreeEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TreeEntry.java deleted file mode 100644 index a1ffa6805..000000000 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TreeEntry.java +++ /dev/null @@ -1,256 +0,0 @@ -/* - * Copyright (C) 2007-2008, Robin Rosenberg - * Copyright (C) 2006-2007, Shawn O. Pearce - * and other copyright owners as documented in the project's IP log. - * - * This program and the accompanying materials are made available - * under the terms of the Eclipse Distribution License v1.0 which - * accompanies this distribution, is reproduced below, and is - * available at http://www.eclipse.org/org/documents/edl-v10.php - * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or - * without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * - Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * - * - Neither the name of the Eclipse Foundation, Inc. nor the - * names of its contributors may be used to endorse or promote - * products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND - * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, - * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package org.eclipse.jgit.lib; - -import java.io.IOException; - -import org.eclipse.jgit.util.RawParseUtils; - -/** - * This class represents an entry in a tree, like a blob or another tree. - * - * @deprecated To look up information about a single path, use - * {@link org.eclipse.jgit.treewalk.TreeWalk#forPath(Repository, String, org.eclipse.jgit.revwalk.RevTree)}. - * To lookup information about multiple paths at once, use a - * {@link org.eclipse.jgit.treewalk.TreeWalk} and obtain the current entry's - * information from its getter methods. - */ -@Deprecated -public abstract class TreeEntry implements Comparable { - private byte[] nameUTF8; - - private Tree parent; - - private ObjectId id; - - /** - * Construct a named tree entry. - * - * @param myParent - * @param myId - * @param myNameUTF8 - */ - protected TreeEntry(final Tree myParent, final ObjectId myId, - final byte[] myNameUTF8) { - nameUTF8 = myNameUTF8; - parent = myParent; - id = myId; - } - - /** - * @return parent of this tree. - */ - public Tree getParent() { - return parent; - } - - /** - * Delete this entry. - */ - public void delete() { - getParent().removeEntry(this); - detachParent(); - } - - /** - * Detach this entry from it's parent. - */ - public void detachParent() { - parent = null; - } - - void attachParent(final Tree p) { - parent = p; - } - - /** - * @return the repository owning this entry. - */ - public Repository getRepository() { - return getParent().getRepository(); - } - - /** - * @return the raw byte name of this entry. - */ - public byte[] getNameUTF8() { - return nameUTF8; - } - - /** - * @return the name of this entry. - */ - public String getName() { - if (nameUTF8 != null) - return RawParseUtils.decode(nameUTF8); - return null; - } - - /** - * Rename this entry. - * - * @param n The new name - * @throws IOException - */ - public void rename(final String n) throws IOException { - rename(Constants.encode(n)); - } - - /** - * Rename this entry. - * - * @param n The new name - * @throws IOException - */ - public void rename(final byte[] n) throws IOException { - final Tree t = getParent(); - if (t != null) { - delete(); - } - nameUTF8 = n; - if (t != null) { - t.addEntry(this); - } - } - - /** - * @return true if this entry is new or modified since being loaded. - */ - public boolean isModified() { - return getId() == null; - } - - /** - * Mark this entry as modified. - */ - public void setModified() { - setId(null); - } - - /** - * @return SHA-1 of this tree entry (null for new unhashed entries) - */ - public ObjectId getId() { - return id; - } - - /** - * Set (update) the SHA-1 of this entry. Invalidates the id's of all - * entries above this entry as they will have to be recomputed. - * - * @param n SHA-1 for this entry. - */ - public void setId(final ObjectId n) { - // If we have a parent and our id is being cleared or changed then force - // the parent's id to become unset as it depends on our id. - // - final Tree p = getParent(); - if (p != null && id != n) { - if ((id == null && n != null) || (id != null && n == null) - || !id.equals(n)) { - p.setId(null); - } - } - - id = n; - } - - /** - * @return repository relative name of this entry - */ - public String getFullName() { - final StringBuilder r = new StringBuilder(); - appendFullName(r); - return r.toString(); - } - - /** - * @return repository relative name of the entry - * FIXME better encoding - */ - public byte[] getFullNameUTF8() { - return getFullName().getBytes(); - } - - public int compareTo(final Object o) { - if (this == o) - return 0; - if (o instanceof TreeEntry) - return Tree.compareNames(nameUTF8, ((TreeEntry) o).nameUTF8, lastChar(this), lastChar((TreeEntry)o)); - return -1; - } - - /** - * Helper for accessing tree/blob methods. - * - * @param treeEntry - * @return '/' for Tree entries and NUL for non-treeish objects. - */ - final public static int lastChar(TreeEntry treeEntry) { - if (!(treeEntry instanceof Tree)) - return '\0'; - else - return '/'; - } - - /** - * @return mode (type of object) - */ - public abstract FileMode getMode(); - - private void appendFullName(final StringBuilder r) { - final TreeEntry p = getParent(); - final String n = getName(); - if (p != null) { - p.appendFullName(r); - if (r.length() > 0) { - r.append('/'); - } - } - if (n != null) { - r.append(n); - } - } -} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java index 191f3d836..82cbf368c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java @@ -46,6 +46,7 @@ import java.util.ArrayList; import java.util.List; import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.util.ChangeIdUtil; @@ -76,22 +77,22 @@ public class MergeMessageFormatter { List commits = new ArrayList(); List others = new ArrayList(); for (Ref ref : refsToMerge) { - if (ref.getName().startsWith(Constants.R_HEADS)) + if (ref.getName().startsWith(Constants.R_HEADS)) { branches.add("'" + Repository.shortenRefName(ref.getName()) //$NON-NLS-1$ + "'"); //$NON-NLS-1$ - - else if (ref.getName().startsWith(Constants.R_REMOTES)) + } else if (ref.getName().startsWith(Constants.R_REMOTES)) { remoteBranches.add("'" //$NON-NLS-1$ + Repository.shortenRefName(ref.getName()) + "'"); //$NON-NLS-1$ - - else if (ref.getName().startsWith(Constants.R_TAGS)) + } else if (ref.getName().startsWith(Constants.R_TAGS)) { tags.add("'" + Repository.shortenRefName(ref.getName()) + "'"); //$NON-NLS-1$ //$NON-NLS-2$ - - else if (ref.getName().equals(ref.getObjectId().getName())) - commits.add("'" + ref.getName() + "'"); //$NON-NLS-1$ //$NON-NLS-2$ - - else - others.add(ref.getName()); + } else { + ObjectId objectId = ref.getObjectId(); + if (objectId != null && ref.getName().equals(objectId.getName())) { + commits.add("'" + ref.getName() + "'"); //$NON-NLS-1$ //$NON-NLS-2$ + } else { + others.add(ref.getName()); + } + } } List listings = new ArrayList(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/notes/NonNoteEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NonNoteEntry.java index 6a2d44bca..362328a96 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/notes/NonNoteEntry.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NonNoteEntry.java @@ -47,6 +47,7 @@ import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.TreeFormatter; +import org.eclipse.jgit.util.Paths; /** A tree entry found in a note branch that isn't a valid note. */ class NonNoteEntry extends ObjectId { @@ -74,27 +75,8 @@ class NonNoteEntry extends ObjectId { } int pathCompare(byte[] bBuf, int bPos, int bLen, FileMode bMode) { - return pathCompare(name, 0, name.length, mode, // - bBuf, bPos, bLen, bMode); - } - - private static int pathCompare(final byte[] aBuf, int aPos, final int aEnd, - final FileMode aMode, final byte[] bBuf, int bPos, final int bEnd, - final FileMode bMode) { - while (aPos < aEnd && bPos < bEnd) { - int cmp = (aBuf[aPos++] & 0xff) - (bBuf[bPos++] & 0xff); - if (cmp != 0) - return cmp; - } - - if (aPos < aEnd) - return (aBuf[aPos] & 0xff) - lastPathChar(bMode); - if (bPos < bEnd) - return lastPathChar(aMode) - (bBuf[bPos] & 0xff); - return 0; - } - - private static int lastPathChar(final FileMode mode) { - return FileMode.TREE.equals(mode.getBits()) ? '/' : '\0'; + return Paths.compare( + name, 0, name.length, mode.getBits(), + bBuf, bPos, bLen, bMode.getBits()); } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommit.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommit.java index c23e4e328..e67ada602 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommit.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommit.java @@ -44,12 +44,17 @@ package org.eclipse.jgit.revwalk; +import static java.nio.charset.StandardCharsets.UTF_8; + import java.io.IOException; import java.nio.charset.Charset; +import java.nio.charset.IllegalCharsetNameException; +import java.nio.charset.UnsupportedCharsetException; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.lib.AnyObjectId; @@ -441,12 +446,12 @@ public class RevCommit extends RevObject { * @return decoded commit message as a string. Never null. */ public final String getFullMessage() { - final byte[] raw = buffer; - final int msgB = RawParseUtils.commitMessage(raw, 0); - if (msgB < 0) + byte[] raw = buffer; + int msgB = RawParseUtils.commitMessage(raw, 0); + if (msgB < 0) { return ""; //$NON-NLS-1$ - final Charset enc = RawParseUtils.parseEncoding(raw); - return RawParseUtils.decode(enc, raw, msgB, raw.length); + } + return RawParseUtils.decode(guessEncoding(), raw, msgB, raw.length); } /** @@ -465,16 +470,17 @@ public class RevCommit extends RevObject { * spanned multiple lines. Embedded LFs are converted to spaces. */ public final String getShortMessage() { - final byte[] raw = buffer; - final int msgB = RawParseUtils.commitMessage(raw, 0); - if (msgB < 0) + byte[] raw = buffer; + int msgB = RawParseUtils.commitMessage(raw, 0); + if (msgB < 0) { return ""; //$NON-NLS-1$ + } - final Charset enc = RawParseUtils.parseEncoding(raw); - final int msgE = RawParseUtils.endOfParagraph(raw, msgB); - String str = RawParseUtils.decode(enc, raw, msgB, msgE); - if (hasLF(raw, msgB, msgE)) + int msgE = RawParseUtils.endOfParagraph(raw, msgB); + String str = RawParseUtils.decode(guessEncoding(), raw, msgB, msgE); + if (hasLF(raw, msgB, msgE)) { str = StringUtils.replaceLineBreaksWithSpace(str); + } return str; } @@ -485,6 +491,23 @@ public class RevCommit extends RevObject { return false; } + /** + * Determine the encoding of the commit message buffer. + *

+ * Locates the "encoding" header (if present) and returns its value. Due to + * corruption in the wild this may be an invalid encoding name that is not + * recognized by any character encoding library. + *

+ * If no encoding header is present, null. + * + * @return the preferred encoding of {@link #getRawBuffer()}; or null. + * @since 4.2 + */ + @Nullable + public final String getEncodingName() { + return RawParseUtils.parseEncodingName(buffer); + } + /** * Determine the encoding of the commit message buffer. *

@@ -492,14 +515,28 @@ public class RevCommit extends RevObject { * character set to apply to this buffer to evaluate its contents as * character data. *

- * If no encoding header is present, {@link Constants#CHARSET} is assumed. + * If no encoding header is present {@code UTF-8} is assumed. * * @return the preferred encoding of {@link #getRawBuffer()}. + * @throws IllegalCharsetNameException + * if the character set requested by the encoding header is + * malformed and unsupportable. + * @throws UnsupportedCharsetException + * if the JRE does not support the character set requested by + * the encoding header. */ public final Charset getEncoding() { return RawParseUtils.parseEncoding(buffer); } + private Charset guessEncoding() { + try { + return getEncoding(); + } catch (IllegalCharsetNameException | UnsupportedCharsetException e) { + return UTF_8; + } + } + /** * Parse the footer lines (e.g. "Signed-off-by") for machine processing. *

@@ -529,7 +566,7 @@ public class RevCommit extends RevObject { final int msgB = RawParseUtils.commitMessage(raw, 0); final ArrayList r = new ArrayList(4); - final Charset enc = getEncoding(); + final Charset enc = guessEncoding(); for (;;) { ptr = RawParseUtils.prevLF(raw, ptr); if (ptr <= msgB) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java index bf2785e0d..81a54bf7e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java @@ -45,8 +45,12 @@ package org.eclipse.jgit.revwalk; +import static java.nio.charset.StandardCharsets.UTF_8; + import java.io.IOException; import java.nio.charset.Charset; +import java.nio.charset.IllegalCharsetNameException; +import java.nio.charset.UnsupportedCharsetException; import org.eclipse.jgit.errors.CorruptObjectException; import org.eclipse.jgit.errors.IncorrectObjectTypeException; @@ -162,7 +166,7 @@ public class RevTag extends RevObject { int p = pos.value += 4; // "tag " final int nameEnd = RawParseUtils.nextLF(rawTag, p) - 1; - tagName = RawParseUtils.decode(Constants.CHARSET, rawTag, p, nameEnd); + tagName = RawParseUtils.decode(UTF_8, rawTag, p, nameEnd); if (walk.isRetainBody()) buffer = rawTag; @@ -207,12 +211,12 @@ public class RevTag extends RevObject { * @return decoded tag message as a string. Never null. */ public final String getFullMessage() { - final byte[] raw = buffer; - final int msgB = RawParseUtils.tagMessage(raw, 0); - if (msgB < 0) + byte[] raw = buffer; + int msgB = RawParseUtils.tagMessage(raw, 0); + if (msgB < 0) { return ""; //$NON-NLS-1$ - final Charset enc = RawParseUtils.parseEncoding(raw); - return RawParseUtils.decode(enc, raw, msgB, raw.length); + } + return RawParseUtils.decode(guessEncoding(), raw, msgB, raw.length); } /** @@ -231,19 +235,28 @@ public class RevTag extends RevObject { * multiple lines. Embedded LFs are converted to spaces. */ public final String getShortMessage() { - final byte[] raw = buffer; - final int msgB = RawParseUtils.tagMessage(raw, 0); - if (msgB < 0) + byte[] raw = buffer; + int msgB = RawParseUtils.tagMessage(raw, 0); + if (msgB < 0) { return ""; //$NON-NLS-1$ + } - final Charset enc = RawParseUtils.parseEncoding(raw); - final int msgE = RawParseUtils.endOfParagraph(raw, msgB); - String str = RawParseUtils.decode(enc, raw, msgB, msgE); - if (RevCommit.hasLF(raw, msgB, msgE)) + int msgE = RawParseUtils.endOfParagraph(raw, msgB); + String str = RawParseUtils.decode(guessEncoding(), raw, msgB, msgE); + if (RevCommit.hasLF(raw, msgB, msgE)) { str = StringUtils.replaceLineBreaksWithSpace(str); + } return str; } + private Charset guessEncoding() { + try { + return RawParseUtils.parseEncoding(buffer); + } catch (IllegalCharsetNameException | UnsupportedCharsetException e) { + return UTF_8; + } + } + /** * Get a reference to the object this tag was placed on. *

diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java index 7f9cec734..aa36aeb1b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java @@ -143,7 +143,9 @@ abstract class BasePackConnection extends BaseConnection { final int timeout = transport.getTimeout(); if (timeout > 0) { final Thread caller = Thread.currentThread(); - myTimer = new InterruptTimer(caller.getName() + "-Timer"); //$NON-NLS-1$ + if (myTimer == null) { + myTimer = new InterruptTimer(caller.getName() + "-Timer"); //$NON-NLS-1$ + } timeoutIn = new TimeoutInputStream(myIn, myTimer); timeoutOut = new TimeoutOutputStream(myOut, myTimer); timeoutIn.setTimeout(timeout * 1000); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java index cf13582db..754cf361a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java @@ -464,8 +464,12 @@ public abstract class BasePackFetchConnection extends BasePackConnection final PacketLineOut p = statelessRPC ? pckState : pckOut; boolean first = true; for (final Ref r : want) { + ObjectId objectId = r.getObjectId(); + if (objectId == null) { + continue; + } try { - if (walk.parseAny(r.getObjectId()).has(REACHABLE)) { + if (walk.parseAny(objectId).has(REACHABLE)) { // We already have this object. Asking for it is // not a very good idea. // @@ -478,7 +482,7 @@ public abstract class BasePackFetchConnection extends BasePackConnection final StringBuilder line = new StringBuilder(46); line.append("want "); //$NON-NLS-1$ - line.append(r.getObjectId().name()); + line.append(objectId.name()); if (first) { line.append(enableCapabilities()); first = false; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java index 0834c359a..963de35d4 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java @@ -239,8 +239,11 @@ public abstract class BasePackPushConnection extends BasePackConnection implemen final StringBuilder sb = new StringBuilder(); ObjectId oldId = rru.getExpectedOldObjectId(); if (oldId == null) { - Ref adv = getRef(rru.getRemoteName()); - oldId = adv != null ? adv.getObjectId() : ObjectId.zeroId(); + final Ref advertised = getRef(rru.getRemoteName()); + oldId = advertised != null ? advertised.getObjectId() : null; + if (oldId == null) { + oldId = ObjectId.zeroId(); + } } sb.append(oldId.name()); sb.append(' '); @@ -382,7 +385,8 @@ public abstract class BasePackPushConnection extends BasePackConnection implemen final int oldTimeout = timeoutIn.getTimeout(); final int sendTime = (int) Math.min(packTransferTime, 28800000L); try { - timeoutIn.setTimeout(10 * Math.max(sendTime, oldTimeout)); + int timeout = 10 * Math.max(sendTime, oldTimeout); + timeoutIn.setTimeout((timeout < 0) ? Integer.MAX_VALUE : timeout); return pckIn.readString(); } finally { timeoutIn.setTimeout(oldTimeout); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java index 776a9f695..a20e65255 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java @@ -293,18 +293,20 @@ public abstract class BaseReceivePack { db = into; walk = new RevWalk(db); - final ReceiveConfig cfg = db.getConfig().get(ReceiveConfig.KEY); - objectChecker = cfg.newObjectChecker(); - allowCreates = cfg.allowCreates; + TransferConfig tc = db.getConfig().get(TransferConfig.KEY); + objectChecker = tc.newReceiveObjectChecker(); + + ReceiveConfig rc = db.getConfig().get(ReceiveConfig.KEY); + allowCreates = rc.allowCreates; allowAnyDeletes = true; - allowBranchDeletes = cfg.allowDeletes; - allowNonFastForwards = cfg.allowNonFastForwards; - allowOfsDelta = cfg.allowOfsDelta; + allowBranchDeletes = rc.allowDeletes; + allowNonFastForwards = rc.allowNonFastForwards; + allowOfsDelta = rc.allowOfsDelta; advertiseRefsHook = AdvertiseRefsHook.DEFAULT; refFilter = RefFilter.DEFAULT; advertisedHaves = new HashSet(); clientShallowCommits = new HashSet(); - signedPushConfig = cfg.signedPush; + signedPushConfig = rc.signedPush; } /** Configuration for receive operations. */ @@ -315,32 +317,13 @@ public abstract class BaseReceivePack { } }; - final boolean checkReceivedObjects; - final boolean allowLeadingZeroFileMode; - final boolean allowInvalidPersonIdent; - final boolean safeForWindows; - final boolean safeForMacOS; - final boolean allowCreates; final boolean allowDeletes; final boolean allowNonFastForwards; final boolean allowOfsDelta; - final SignedPushConfig signedPush; ReceiveConfig(final Config config) { - checkReceivedObjects = config.getBoolean( - "receive", "fsckobjects", //$NON-NLS-1$ //$NON-NLS-2$ - config.getBoolean("transfer", "fsckobjects", false)); //$NON-NLS-1$ //$NON-NLS-2$ - allowLeadingZeroFileMode = checkReceivedObjects - && config.getBoolean("fsck", "allowLeadingZeroFileMode", false); //$NON-NLS-1$ //$NON-NLS-2$ - allowInvalidPersonIdent = checkReceivedObjects - && config.getBoolean("fsck", "allowInvalidPersonIdent", false); //$NON-NLS-1$ //$NON-NLS-2$ - safeForWindows = checkReceivedObjects - && config.getBoolean("fsck", "safeForWindows", false); //$NON-NLS-1$ //$NON-NLS-2$ - safeForMacOS = checkReceivedObjects - && config.getBoolean("fsck", "safeForMacOS", false); //$NON-NLS-1$ //$NON-NLS-2$ - allowCreates = true; allowDeletes = !config.getBoolean("receive", "denydeletes", false); //$NON-NLS-1$ //$NON-NLS-2$ allowNonFastForwards = !config.getBoolean("receive", //$NON-NLS-1$ @@ -349,16 +332,6 @@ public abstract class BaseReceivePack { true); signedPush = SignedPushConfig.KEY.parse(config); } - - ObjectChecker newObjectChecker() { - if (!checkReceivedObjects) - return null; - return new ObjectChecker() - .setAllowLeadingZeroFileMode(allowLeadingZeroFileMode) - .setAllowInvalidPersonIdent(allowInvalidPersonIdent) - .setSafeForWindows(safeForWindows) - .setSafeForMacOS(safeForMacOS); - } } /** @@ -1372,16 +1345,21 @@ public abstract class BaseReceivePack { } } - if (cmd.getType() == ReceiveCommand.Type.DELETE && ref != null - && !ObjectId.zeroId().equals(cmd.getOldId()) - && !ref.getObjectId().equals(cmd.getOldId())) { - // Delete commands can be sent with the old id matching our - // advertised value, *OR* with the old id being 0{40}. Any - // other requested old id is invalid. - // - cmd.setResult(Result.REJECTED_OTHER_REASON, - JGitText.get().invalidOldIdSent); - continue; + if (cmd.getType() == ReceiveCommand.Type.DELETE && ref != null) { + ObjectId id = ref.getObjectId(); + if (id == null) { + id = ObjectId.zeroId(); + } + if (!ObjectId.zeroId().equals(cmd.getOldId()) + && !id.equals(cmd.getOldId())) { + // Delete commands can be sent with the old id matching our + // advertised value, *OR* with the old id being 0{40}. Any + // other requested old id is invalid. + // + cmd.setResult(Result.REJECTED_OTHER_REASON, + JGitText.get().invalidOldIdSent); + continue; + } } if (cmd.getType() == ReceiveCommand.Type.UPDATE) { @@ -1391,8 +1369,15 @@ public abstract class BaseReceivePack { cmd.setResult(Result.REJECTED_OTHER_REASON, JGitText.get().noSuchRef); continue; } + ObjectId id = ref.getObjectId(); + if (id == null) { + // We cannot update unborn branch + cmd.setResult(Result.REJECTED_OTHER_REASON, + JGitText.get().cannotUpdateUnbornBranch); + continue; + } - if (!ref.getObjectId().equals(cmd.getOldId())) { + if (!id.equals(cmd.getOldId())) { // A properly functioning client will send the same // object id we advertised. // @@ -1468,10 +1453,7 @@ public abstract class BaseReceivePack { * @since 3.6 */ protected void failPendingCommands() { - for (ReceiveCommand cmd : commands) { - if (cmd.getResult() == Result.NOT_ATTEMPTED) - cmd.setResult(Result.REJECTED_OTHER_REASON, JGitText.get().transactionAborted); - } + ReceiveCommand.abort(commands); } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ChainingCredentialsProvider.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ChainingCredentialsProvider.java index 3e0ee2f64..3941d3c55 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ChainingCredentialsProvider.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ChainingCredentialsProvider.java @@ -113,19 +113,18 @@ public class ChainingCredentialsProvider extends CredentialsProvider { throws UnsupportedCredentialItem { for (CredentialsProvider p : credentialProviders) { if (p.supports(items)) { - p.get(uri, items); - if (isAnyNull(items)) + if (!p.get(uri, items)) { + if (p.isInteractive()) { + return false; // user cancelled the request + } continue; + } + if (isAnyNull(items)) { + continue; + } return true; } } return false; } - - private boolean isAnyNull(CredentialItem... items) { - for (CredentialItem i : items) - if (i == null) - return true; - return false; - } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Connection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Connection.java index 0ff9fcea7..da288ec31 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Connection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Connection.java @@ -59,8 +59,7 @@ import org.eclipse.jgit.lib.Ref; * * @see Transport */ -public interface Connection { - +public interface Connection extends AutoCloseable { /** * Get the complete map of refs advertised as available for fetching or * pushing. @@ -108,6 +107,10 @@ public interface Connection { *

* If additional messages were produced by the remote peer, these should * still be retained in the connection instance for {@link #getMessages()}. + *

+ * {@code AutoClosable.close()} declares that it throws {@link Exception}. + * Implementers shouldn't throw checked exceptions. This override narrows + * the signature to prevent them from doing so. */ public void close(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/CredentialsProvider.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/CredentialsProvider.java index 464d0f9ee..4800f6826 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/CredentialsProvider.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/CredentialsProvider.java @@ -80,6 +80,20 @@ public abstract class CredentialsProvider { defaultProvider = p; } + /** + * @param items + * credential items to check + * @return {@code true} if any of the passed items is null, {@code false} + * otherwise + * @since 4.2 + */ + protected static boolean isAnyNull(CredentialItem... items) { + for (CredentialItem i : items) + if (i == null) + return true; + return false; + } + /** * Check if the provider is interactive with the end-user. * diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java index 9aae1c37a..c4b3f8304 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java @@ -397,11 +397,17 @@ class FetchProcess { private void expandFetchTags() throws TransportException { final Map haveRefs = localRefs(); for (final Ref r : conn.getRefs()) { - if (!isTag(r)) + if (!isTag(r)) { + continue; + } + ObjectId id = r.getObjectId(); + if (id == null) { continue; + } final Ref local = haveRefs.get(r.getName()); - if (local == null || !r.getObjectId().equals(local.getObjectId())) + if (local == null || !id.equals(local.getObjectId())) { wantTag(r); + } } } @@ -413,6 +419,11 @@ class FetchProcess { private void want(final Ref src, final RefSpec spec) throws TransportException { final ObjectId newId = src.getObjectId(); + if (newId == null) { + throw new NullPointerException(MessageFormat.format( + JGitText.get().transportProvidedRefWithNoObjectId, + src.getName())); + } if (spec.getDestination() != null) { final TrackingRefUpdate tru = createUpdate(spec, newId); if (newId.equals(tru.getOldObjectId())) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/JschSession.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/JschSession.java index 85109a5bf..1dfe5d979 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/JschSession.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/JschSession.java @@ -149,14 +149,27 @@ public class JschSession implements RemoteSession { channel.setCommand(commandName); setupStreams(); channel.connect(timeout > 0 ? timeout * 1000 : 0); - if (!channel.isConnected()) + if (!channel.isConnected()) { + closeOutputStream(); throw new TransportException(uri, JGitText.get().connectionFailed); + } } catch (JSchException e) { + closeOutputStream(); throw new TransportException(uri, e.getMessage(), e); } } + private void closeOutputStream() { + if (outputStream != null) { + try { + outputStream.close(); + } catch (IOException ioe) { + // ignore + } + } + } + private void setupStreams() throws IOException { inputStream = channel.getInputStream(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRCCredentialsProvider.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRCCredentialsProvider.java index 74909998c..4037545e9 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRCCredentialsProvider.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRCCredentialsProvider.java @@ -105,12 +105,11 @@ public class NetRCCredentialsProvider extends CredentialsProvider { throw new UnsupportedCredentialItem(uri, i.getClass().getName() + ":" + i.getPromptText()); //$NON-NLS-1$ } - return true; + return !isAnyNull(items); } @Override public boolean isInteractive() { return false; } - } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackParser.java index 6e5fc9f00..b96fe885e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackParser.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackParser.java @@ -1049,8 +1049,11 @@ public abstract class PackParser { final byte[] data) throws IOException { if (objCheck != null) { try { - objCheck.check(type, data); + objCheck.check(id, type, data); } catch (CorruptObjectException e) { + if (e.getErrorType() != null) { + throw e; + } throw new CorruptObjectException(MessageFormat.format( JGitText.get().invalidObject, Constants.typeString(type), diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProgressSpinner.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProgressSpinner.java new file mode 100644 index 000000000..ac048a14a --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProgressSpinner.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2015, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.transport; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.concurrent.TimeUnit; + +/** + * A simple spinner connected to an {@code OutputStream}. + *

+ * This is class is not thread-safe. The update method may only be used from a + * single thread. Updates are sent only as frequently as {@link #update()} is + * invoked by the caller, and are capped at no more than 2 times per second by + * requiring at least 500 milliseconds between updates. + * + * @since 4.2 + */ +public class ProgressSpinner { + private static final long MIN_REFRESH_MILLIS = 500; + private static final char[] STATES = new char[] { '-', '\\', '|', '/' }; + + private final OutputStream out; + private String msg; + private int state; + private boolean write; + private boolean shown; + private long nextUpdateMillis; + + /** + * Initialize a new spinner. + * + * @param out + * where to send output to. + */ + public ProgressSpinner(OutputStream out) { + this.out = out; + this.write = true; + } + + /** + * Begin a time consuming task. + * + * @param title + * description of the task, suitable for human viewing. + * @param delay + * delay to wait before displaying anything at all. + * @param delayUnits + * unit for {@code delay}. + */ + public void beginTask(String title, long delay, TimeUnit delayUnits) { + msg = title; + state = 0; + shown = false; + + long now = System.currentTimeMillis(); + if (delay > 0) { + nextUpdateMillis = now + delayUnits.toMillis(delay); + } else { + send(now); + } + } + + /** Update the spinner if it is showing. */ + public void update() { + long now = System.currentTimeMillis(); + if (now >= nextUpdateMillis) { + send(now); + state = (state + 1) % STATES.length; + } + } + + private void send(long now) { + StringBuilder buf = new StringBuilder(msg.length() + 16); + buf.append('\r').append(msg).append("... ("); //$NON-NLS-1$ + buf.append(STATES[state]); + buf.append(") "); //$NON-NLS-1$ + shown = true; + write(buf.toString()); + nextUpdateMillis = now + MIN_REFRESH_MILLIS; + } + + /** + * Denote the current task completed. + * + * @param result + * text to print after the task's title + * {@code "$title ... $result"}. + */ + public void endTask(String result) { + if (shown) { + write('\r' + msg + "... " + result + "\n"); //$NON-NLS-1$ //$NON-NLS-2$ + } + } + + private void write(String s) { + if (write) { + try { + out.write(s.getBytes(UTF_8)); + out.flush(); + } catch (IOException e) { + write = false; + } + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java index 4fd192dbb..5cea88215 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java @@ -188,8 +188,13 @@ class PushProcess { final Map result = new HashMap(); for (final RemoteRefUpdate rru : toPush.values()) { final Ref advertisedRef = connection.getRef(rru.getRemoteName()); - final ObjectId advertisedOld = (advertisedRef == null ? ObjectId - .zeroId() : advertisedRef.getObjectId()); + ObjectId advertisedOld = null; + if (advertisedRef != null) { + advertisedOld = advertisedRef.getObjectId(); + } + if (advertisedOld == null) { + advertisedOld = ObjectId.zeroId(); + } if (rru.getNewObjectId().equals(advertisedOld)) { if (rru.isDelete()) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java index 5702b6d7b..2b21c4a8f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java @@ -43,6 +43,9 @@ package org.eclipse.jgit.transport; +import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED; +import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON; + import java.io.IOException; import java.text.MessageFormat; import java.util.ArrayList; @@ -168,6 +171,25 @@ public class ReceiveCommand { return filter((Iterable) commands, want); } + /** + * Set unprocessed commands as failed due to transaction aborted. + *

+ * If a command is still {@link Result#NOT_ATTEMPTED} it will be set to + * {@link Result#REJECTED_OTHER_REASON}. + * + * @param commands + * commands to mark as failed. + * @since 4.2 + */ + public static void abort(Iterable commands) { + for (ReceiveCommand c : commands) { + if (c.getResult() == NOT_ATTEMPTED) { + c.setResult(REJECTED_OTHER_REASON, + JGitText.get().transactionAborted); + } + } + } + private final ObjectId oldId; private final ObjectId newId; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java index 66ffc3abe..0e803bdaf 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java @@ -457,10 +457,6 @@ public class RefSpec implements Serializable { if (i != -1) { if (s.indexOf('*', i + 1) > i) return false; - if (i > 0 && s.charAt(i - 1) != '/') - return false; - if (i < s.length() - 1 && s.charAt(i + 1) != '/') - return false; } return true; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandInputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandInputStream.java index cf388e271..fe9f2a315 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandInputStream.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandInputStream.java @@ -78,12 +78,8 @@ import org.eclipse.jgit.util.RawParseUtils; * @see SideBandOutputStream */ class SideBandInputStream extends InputStream { - private static final String PFX_REMOTE = JGitText.get().prefixRemote; - static final int CH_DATA = 1; - static final int CH_PROGRESS = 2; - static final int CH_ERROR = 3; private static Pattern P_UNBOUNDED = Pattern @@ -174,7 +170,7 @@ class SideBandInputStream extends InputStream { continue; case CH_ERROR: eof = true; - throw new TransportException(PFX_REMOTE + readString(available)); + throw new TransportException(remote(readString(available))); default: throw new PackProtocolException( MessageFormat.format(JGitText.get().invalidChannel, @@ -241,7 +237,18 @@ class SideBandInputStream extends InputStream { } private void beginTask(final int totalWorkUnits) { - monitor.beginTask(PFX_REMOTE + currentTask, totalWorkUnits); + monitor.beginTask(remote(currentTask), totalWorkUnits); + } + + private static String remote(String msg) { + String prefix = JGitText.get().prefixRemote; + StringBuilder r = new StringBuilder(prefix.length() + msg.length() + 1); + r.append(prefix); + if (prefix.length() > 0 && prefix.charAt(prefix.length() - 1) != ' ') { + r.append(' '); + } + r.append(msg); + return r.toString(); } private String readString(final int len) throws IOException { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java index f0c513427..72c9c8b93 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java @@ -43,12 +43,20 @@ package org.eclipse.jgit.transport; +import static org.eclipse.jgit.util.StringUtils.equalsIgnoreCase; +import static org.eclipse.jgit.util.StringUtils.toLowerCase; + +import java.io.File; +import java.util.EnumSet; import java.util.HashMap; import java.util.Map; +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.internal.storage.file.LazyObjectIdSetFile; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.Config.SectionParser; import org.eclipse.jgit.lib.ObjectChecker; +import org.eclipse.jgit.lib.ObjectIdSet; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.util.SystemReader; @@ -58,6 +66,8 @@ import org.eclipse.jgit.util.SystemReader; * parameters. */ public class TransferConfig { + private static final String FSCK = "fsck"; //$NON-NLS-1$ + /** Key for {@link Config#get(SectionParser)}. */ public static final Config.SectionParser KEY = new SectionParser() { public TransferConfig parse(final Config cfg) { @@ -65,8 +75,14 @@ public class TransferConfig { } }; - private final boolean checkReceivedObjects; - private final boolean allowLeadingZeroFileMode; + enum FsckMode { + ERROR, WARN, IGNORE; + } + + private final boolean fetchFsck; + private final boolean receiveFsck; + private final String fsckSkipList; + private final EnumSet ignore; private final boolean allowInvalidPersonIdent; private final boolean safeForWindows; private final boolean safeForMacOS; @@ -79,20 +95,47 @@ public class TransferConfig { } TransferConfig(final Config rc) { - checkReceivedObjects = rc.getBoolean( - "fetch", "fsckobjects", //$NON-NLS-1$ //$NON-NLS-2$ - rc.getBoolean("transfer", "fsckobjects", false)); //$NON-NLS-1$ //$NON-NLS-2$ - allowLeadingZeroFileMode = checkReceivedObjects - && rc.getBoolean("fsck", "allowLeadingZeroFileMode", false); //$NON-NLS-1$ //$NON-NLS-2$ - allowInvalidPersonIdent = checkReceivedObjects - && rc.getBoolean("fsck", "allowInvalidPersonIdent", false); //$NON-NLS-1$ //$NON-NLS-2$ - safeForWindows = checkReceivedObjects - && rc.getBoolean("fsck", "safeForWindows", //$NON-NLS-1$ //$NON-NLS-2$ + boolean fsck = rc.getBoolean("transfer", "fsckobjects", false); //$NON-NLS-1$ //$NON-NLS-2$ + fetchFsck = rc.getBoolean("fetch", "fsckobjects", fsck); //$NON-NLS-1$ //$NON-NLS-2$ + receiveFsck = rc.getBoolean("receive", "fsckobjects", fsck); //$NON-NLS-1$ //$NON-NLS-2$ + fsckSkipList = rc.getString(FSCK, null, "skipList"); //$NON-NLS-1$ + allowInvalidPersonIdent = rc.getBoolean(FSCK, "allowInvalidPersonIdent", false); //$NON-NLS-1$ + safeForWindows = rc.getBoolean(FSCK, "safeForWindows", //$NON-NLS-1$ SystemReader.getInstance().isWindows()); - safeForMacOS = checkReceivedObjects - && rc.getBoolean("fsck", "safeForMacOS", //$NON-NLS-1$ //$NON-NLS-2$ + safeForMacOS = rc.getBoolean(FSCK, "safeForMacOS", //$NON-NLS-1$ SystemReader.getInstance().isMacOS()); + ignore = EnumSet.noneOf(ObjectChecker.ErrorType.class); + EnumSet set = EnumSet + .noneOf(ObjectChecker.ErrorType.class); + for (String key : rc.getNames(FSCK)) { + if (equalsIgnoreCase(key, "skipList") //$NON-NLS-1$ + || equalsIgnoreCase(key, "allowLeadingZeroFileMode") //$NON-NLS-1$ + || equalsIgnoreCase(key, "allowInvalidPersonIdent") //$NON-NLS-1$ + || equalsIgnoreCase(key, "safeForWindows") //$NON-NLS-1$ + || equalsIgnoreCase(key, "safeForMacOS")) { //$NON-NLS-1$ + continue; + } + + ObjectChecker.ErrorType id = FsckKeyNameHolder.parse(key); + if (id != null) { + switch (rc.getEnum(FSCK, null, key, FsckMode.ERROR)) { + case ERROR: + ignore.remove(id); + break; + case WARN: + case IGNORE: + ignore.add(id); + break; + } + set.add(id); + } + } + if (!set.contains(ObjectChecker.ErrorType.ZERO_PADDED_FILEMODE) + && rc.getBoolean(FSCK, "allowLeadingZeroFileMode", false)) { //$NON-NLS-1$ + ignore.add(ObjectChecker.ErrorType.ZERO_PADDED_FILEMODE); + } + allowTipSha1InWant = rc.getBoolean( "uploadpack", "allowtipsha1inwant", false); //$NON-NLS-1$ //$NON-NLS-2$ allowReachableSha1InWant = rc.getBoolean( @@ -105,14 +148,38 @@ public class TransferConfig { * enabled in the repository configuration. * @since 3.6 */ + @Nullable public ObjectChecker newObjectChecker() { - if (!checkReceivedObjects) + return newObjectChecker(fetchFsck); + } + + /** + * @return checker to verify objects pushed into this repository, or null if + * checking is not enabled in the repository configuration. + * @since 4.2 + */ + @Nullable + public ObjectChecker newReceiveObjectChecker() { + return newObjectChecker(receiveFsck); + } + + private ObjectChecker newObjectChecker(boolean check) { + if (!check) { return null; + } return new ObjectChecker() - .setAllowLeadingZeroFileMode(allowLeadingZeroFileMode) + .setIgnore(ignore) .setAllowInvalidPersonIdent(allowInvalidPersonIdent) .setSafeForWindows(safeForWindows) - .setSafeForMacOS(safeForMacOS); + .setSafeForMacOS(safeForMacOS) + .setSkipList(skipList()); + } + + private ObjectIdSet skipList() { + if (fsckSkipList != null && !fsckSkipList.isEmpty()) { + return new LazyObjectIdSetFile(new File(fsckSkipList)); + } + return null; } /** @@ -161,4 +228,34 @@ public class TransferConfig { } }; } + + static class FsckKeyNameHolder { + private static final Map errors; + + static { + errors = new HashMap<>(); + for (ObjectChecker.ErrorType m : ObjectChecker.ErrorType.values()) { + errors.put(keyNameFor(m.name()), m); + } + } + + @Nullable + static ObjectChecker.ErrorType parse(String key) { + return errors.get(toLowerCase(key)); + } + + private static String keyNameFor(String name) { + StringBuilder r = new StringBuilder(name.length()); + for (int i = 0; i < name.length(); i++) { + char c = name.charAt(i); + if (c != '_') { + r.append(c); + } + } + return toLowerCase(r.toString()); + } + + private FsckKeyNameHolder() { + } + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java index 6af153cbc..9e6d1f68f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java @@ -98,7 +98,7 @@ import org.eclipse.jgit.storage.pack.PackConfig; * Transport instances and the connections they create are not thread-safe. * Callers must ensure a transport is accessed by only one thread at a time. */ -public abstract class Transport { +public abstract class Transport implements AutoCloseable { /** Type of operation a Transport is being opened for. */ public enum Operation { /** Transport is to fetch objects locally. */ @@ -1353,6 +1353,10 @@ public abstract class Transport { * must close that network socket, disconnecting the two peers. If the * remote repository is actually local (same system) this method must close * any open file handles used to read the "remote" repository. + *

+ * {@code AutoClosable.close()} declares that it throws {@link Exception}. + * Implementers shouldn't throw checked exceptions. This override narrows + * the signature to prevent them from doing so. */ public abstract void close(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java index 2f9dfa1d6..3ee2feb14 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java @@ -83,7 +83,7 @@ public class URIish implements Serializable { * capturing groups: the first containing the user and the second containing * the password */ - private static final String OPT_USER_PWD_P = "(?:([^/:@]+)(?::([^\\\\/]+))?@)?"; //$NON-NLS-1$ + private static final String OPT_USER_PWD_P = "(?:([^/:]+)(?::([^\\\\/]+))?@)?"; //$NON-NLS-1$ /** * Part of a pattern which matches the host part of URIs. Defines one diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkFetchConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkFetchConnection.java index 1c6b8b736..17edfdc4f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkFetchConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkFetchConnection.java @@ -267,6 +267,10 @@ class WalkFetchConnection extends BaseFetchConnection { final HashSet inWorkQueue = new HashSet(); for (final Ref r : want) { final ObjectId id = r.getObjectId(); + if (id == null) { + throw new NullPointerException(MessageFormat.format( + JGitText.get().transportProvidedRefWithNoObjectId, r.getName())); + } try { final RevObject obj = revWalk.parseAny(id); if (obj.has(COMPLETE)) @@ -633,10 +637,11 @@ class WalkFetchConnection extends BaseFetchConnection { final byte[] raw = uol.getCachedBytes(); if (objCheck != null) { try { - objCheck.check(type, raw); + objCheck.check(id, type, raw); } catch (CorruptObjectException e) { - throw new TransportException(MessageFormat.format(JGitText.get().transportExceptionInvalid - , Constants.typeString(type), id.name(), e.getMessage())); + throw new TransportException(MessageFormat.format( + JGitText.get().transportExceptionInvalid, + Constants.typeString(type), id.name(), e.getMessage())); } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java index 5e7188957..58136355e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java @@ -59,6 +59,7 @@ import org.eclipse.jgit.lib.MutableObjectId; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.treewalk.filter.TreeFilter; +import org.eclipse.jgit.util.Paths; /** * Walks a Git tree (directory) in Git sort order. @@ -382,20 +383,9 @@ public abstract class AbstractTreeIterator { } private int pathCompare(byte[] b, int bPos, int bEnd, int bMode, int aPos) { - final byte[] a = path; - final int aEnd = pathLen; - - for (; aPos < aEnd && bPos < bEnd; aPos++, bPos++) { - final int cmp = (a[aPos] & 0xff) - (b[bPos] & 0xff); - if (cmp != 0) - return cmp; - } - - if (aPos < aEnd) - return (a[aPos] & 0xff) - lastPathChar(bMode); - if (bPos < bEnd) - return lastPathChar(mode) - (b[bPos] & 0xff); - return lastPathChar(mode) - lastPathChar(bMode); + return Paths.compare( + path, aPos, pathLen, mode, + b, bPos, bEnd, bMode); } private static int alreadyMatch(AbstractTreeIterator a, @@ -412,10 +402,6 @@ public abstract class AbstractTreeIterator { } } - private static int lastPathChar(final int mode) { - return FileMode.TREE.equals(mode) ? '/' : '\0'; - } - /** * Check if the current entry of both iterators has the same id. *

@@ -691,6 +677,14 @@ public abstract class AbstractTreeIterator { // Do nothing by default. Most iterators do not care. } + /** + * @return true if the iterator implements {@link #stopWalk()}. + * @since 4.2 + */ + protected boolean needsStopWalk() { + return false; + } + /** * @return the length of the name component of the path for the current entry */ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/EmptyTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/EmptyTreeIterator.java index 8dbf80e6a..ec4a84eff 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/EmptyTreeIterator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/EmptyTreeIterator.java @@ -142,4 +142,9 @@ public class EmptyTreeIterator extends AbstractTreeIterator { if (parent != null) parent.stopWalk(); } + + @Override + protected boolean needsStopWalk() { + return parent != null && parent.needsStopWalk(); + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/NameConflictTreeWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/NameConflictTreeWalk.java index 350f56396..d2195a874 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/NameConflictTreeWalk.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/NameConflictTreeWalk.java @@ -43,6 +43,8 @@ package org.eclipse.jgit.treewalk; +import java.io.IOException; + import org.eclipse.jgit.dircache.DirCacheBuilder; import org.eclipse.jgit.errors.CorruptObjectException; import org.eclipse.jgit.lib.FileMode; @@ -338,6 +340,41 @@ public class NameConflictTreeWalk extends TreeWalk { dfConflict = null; } + void stopWalk() throws IOException { + if (!needsStopWalk()) { + return; + } + + // Name conflicts make aborting early difficult. Multiple paths may + // exist between the file and directory versions of a name. To ensure + // the directory version is skipped over (as it was previously visited + // during the file version step) requires popping up the stack and + // finishing out each subtree that the walker dove into. Siblings in + // parents do not need to be recursed into, bounding the cost. + for (;;) { + AbstractTreeIterator t = min(); + if (t.eof()) { + if (depth > 0) { + exitSubtree(); + popEntriesEqual(); + continue; + } + return; + } + currentHead = t; + skipEntriesEqual(); + } + } + + private boolean needsStopWalk() { + for (AbstractTreeIterator t : trees) { + if (t.needsStopWalk()) { + return true; + } + } + return false; + } + /** * True if the current entry is covered by a directory/file conflict. * diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java index 06dc0bf6d..5cd713da7 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java @@ -57,6 +57,7 @@ import org.eclipse.jgit.attributes.Attributes; import org.eclipse.jgit.attributes.AttributesNode; import org.eclipse.jgit.attributes.AttributesNodeProvider; import org.eclipse.jgit.attributes.AttributesProvider; +import org.eclipse.jgit.dircache.DirCacheBuildIterator; import org.eclipse.jgit.dircache.DirCacheIterator; import org.eclipse.jgit.errors.CorruptObjectException; import org.eclipse.jgit.errors.IncorrectObjectTypeException; @@ -256,7 +257,7 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { private boolean postOrderTraversal; - private int depth; + int depth; private boolean advance; @@ -573,18 +574,13 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { * @param p * an iterator to walk over. The iterator should be new, with no * parent, and should still be positioned before the first entry. - * The tree which the iterator operates on must have the same root - * as other trees in the walk. - * + * The tree which the iterator operates on must have the same + * root as other trees in the walk. * @return position of this tree within the walker. - * @throws CorruptObjectException - * the iterator was unable to obtain its first entry, due to - * possible data corruption within the backing data store. */ - public int addTree(final AbstractTreeIterator p) - throws CorruptObjectException { - final int n = trees.length; - final AbstractTreeIterator[] newTrees = new AbstractTreeIterator[n + 1]; + public int addTree(AbstractTreeIterator p) { + int n = trees.length; + AbstractTreeIterator[] newTrees = new AbstractTreeIterator[n + 1]; System.arraycopy(trees, 0, newTrees, 0, n); newTrees[n] = p; @@ -665,12 +661,29 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { return true; } } catch (StopWalkException stop) { - for (final AbstractTreeIterator t : trees) - t.stopWalk(); + stopWalk(); return false; } } + /** + * Notify iterators the walk is aborting. + *

+ * Primarily to notify {@link DirCacheBuildIterator} the walk is aborting so + * that it can copy any remaining entries. + * + * @throws IOException + * if traversal of remaining entries throws an exception during + * object access. This should never occur as remaining trees + * should already be in memory, however the methods used to + * finish traversal are declared to throw IOException. + */ + void stopWalk() throws IOException { + for (AbstractTreeIterator t : trees) { + t.stopWalk(); + } + } + /** * Obtain the tree iterator for the current entry. *

@@ -861,10 +874,13 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { * Test if the supplied path matches the current entry's path. *

* This method tests that the supplied path is exactly equal to the current - * entry, or is one of its parent directories. It is faster to use this + * entry or is one of its parent directories. It is faster to use this * method then to use {@link #getPathString()} to first create a String * object, then test startsWith or some other type of string * match function. + *

+ * If the current entry is a subtree, then all paths within the subtree + * are considered to match it. * * @param p * path buffer to test. Callers should ensure the path does not @@ -900,7 +916,7 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { // If p[ci] == '/' then pattern matches this subtree, // otherwise we cannot be certain so we return -1. // - return p[ci] == '/' ? 0 : -1; + return p[ci] == '/' && FileMode.TREE.equals(t.mode) ? 0 : -1; } // Both strings are identical. @@ -1062,7 +1078,7 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { } } - private void exitSubtree() { + void exitSubtree() { depth--; for (int i = 0; i < trees.length; i++) trees[i] = trees[i].parent; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java index 94beeeb56..0d617ee7f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java @@ -89,6 +89,7 @@ import org.eclipse.jgit.submodule.SubmoduleWalk; import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FS.ExecutionResult; import org.eclipse.jgit.util.IO; +import org.eclipse.jgit.util.Paths; import org.eclipse.jgit.util.RawParseUtils; import org.eclipse.jgit.util.io.EolCanonicalizingInputStream; @@ -692,31 +693,13 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { } private static final Comparator ENTRY_CMP = new Comparator() { - public int compare(final Entry o1, final Entry o2) { - final byte[] a = o1.encodedName; - final byte[] b = o2.encodedName; - final int aLen = o1.encodedNameLen; - final int bLen = o2.encodedNameLen; - int cPos; - - for (cPos = 0; cPos < aLen && cPos < bLen; cPos++) { - final int cmp = (a[cPos] & 0xff) - (b[cPos] & 0xff); - if (cmp != 0) - return cmp; - } - - if (cPos < aLen) - return (a[cPos] & 0xff) - lastPathChar(o2); - if (cPos < bLen) - return lastPathChar(o1) - (b[cPos] & 0xff); - return lastPathChar(o1) - lastPathChar(o2); + public int compare(Entry a, Entry b) { + return Paths.compare( + a.encodedName, 0, a.encodedNameLen, a.getMode().getBits(), + b.encodedName, 0, b.encodedNameLen, b.getMode().getBits()); } }; - static int lastPathChar(final Entry e) { - return e.getMode() == FileMode.TREE ? '/' : '\0'; - } - /** * Constructor helper. * diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/PathFilterGroup.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/PathFilterGroup.java index bdfde0bfc..7601956c4 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/PathFilterGroup.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/PathFilterGroup.java @@ -245,9 +245,9 @@ public class PathFilterGroup { int hash = hasher.nextHash(); if (fullpaths.contains(rp, hasher.length(), hash)) return true; - if (!hasher.hasNext()) - if (prefixes.contains(rp, hasher.length(), hash)) - return true; + if (!hasher.hasNext() && walker.isSubtree() + && prefixes.contains(rp, hasher.length(), hash)) + return true; } final int cmp = walker.isPathPrefix(max, max.length); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/ChangeIdUtil.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/ChangeIdUtil.java index 35fc99e54..e14096e59 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/ChangeIdUtil.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/ChangeIdUtil.java @@ -42,7 +42,6 @@ */ package org.eclipse.jgit.util; -import java.io.IOException; import java.util.regex.Pattern; import org.eclipse.jgit.lib.Constants; @@ -90,12 +89,10 @@ public class ChangeIdUtil { * The commit message * @return the change id SHA1 string (without the 'I') or null if the * message is not complete enough - * @throws IOException */ public static ObjectId computeChangeId(final ObjectId treeId, final ObjectId firstParentId, final PersonIdent author, - final PersonIdent committer, final String message) - throws IOException { + final PersonIdent committer, final String message) { String cleanMessage = clean(message); if (cleanMessage.length() == 0) return null; @@ -116,8 +113,7 @@ public class ChangeIdUtil { b.append("\n\n"); //$NON-NLS-1$ b.append(cleanMessage); try (ObjectInserter f = new ObjectInserter.Formatter()) { - return f.idFor(Constants.OBJ_COMMIT, // - b.toString().getBytes(Constants.CHARACTER_ENCODING)); + return f.idFor(Constants.OBJ_COMMIT, Constants.encode(b.toString())); } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java index 727ea79cc..aa101f73f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java @@ -409,7 +409,9 @@ public class FileUtils { throws IOException { Path nioPath = path.toPath(); if (Files.exists(nioPath, LinkOption.NOFOLLOW_LINKS)) { - if (Files.isRegularFile(nioPath)) { + BasicFileAttributes attrs = Files.readAttributes(nioPath, + BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); + if (attrs.isRegularFile() || attrs.isSymbolicLink()) { delete(path); } else { delete(path, EMPTY_DIRECTORIES_ONLY | RECURSIVE); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/Paths.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/Paths.java new file mode 100644 index 000000000..6be7ddbe1 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/Paths.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2016, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.util; + +import static org.eclipse.jgit.lib.FileMode.TYPE_MASK; +import static org.eclipse.jgit.lib.FileMode.TYPE_TREE; + +/** + * Utility functions for paths inside of a Git repository. + * + * @since 4.2 + */ +public class Paths { + /** + * Remove trailing {@code '/'} if present. + * + * @param path + * input path to potentially remove trailing {@code '/'} from. + * @return null if {@code path == null}; {@code path} after removing a + * trailing {@code '/'}. + */ + public static String stripTrailingSeparator(String path) { + if (path == null || path.isEmpty()) { + return path; + } + + int i = path.length(); + if (path.charAt(path.length() - 1) != '/') { + return path; + } + do { + i--; + } while (path.charAt(i - 1) == '/'); + return path.substring(0, i); + } + + /** + * Compare two paths according to Git path sort ordering rules. + * + * @param aPath + * first path buffer. The range {@code [aPos, aEnd)} is used. + * @param aPos + * index into {@code aPath} where the first path starts. + * @param aEnd + * 1 past last index of {@code aPath}. + * @param aMode + * mode of the first file. Trees are sorted as though + * {@code aPath[aEnd] == '/'}, even if aEnd does not exist. + * @param bPath + * second path buffer. The range {@code [bPos, bEnd)} is used. + * @param bPos + * index into {@code bPath} where the second path starts. + * @param bEnd + * 1 past last index of {@code bPath}. + * @param bMode + * mode of the second file. Trees are sorted as though + * {@code bPath[bEnd] == '/'}, even if bEnd does not exist. + * @return <0 if {@code aPath} sorts before {@code bPath}; + * 0 if the paths are the same; + * >0 if {@code aPath} sorts after {@code bPath}. + */ + public static int compare(byte[] aPath, int aPos, int aEnd, int aMode, + byte[] bPath, int bPos, int bEnd, int bMode) { + int cmp = coreCompare( + aPath, aPos, aEnd, aMode, + bPath, bPos, bEnd, bMode); + if (cmp == 0) { + cmp = lastPathChar(aMode) - lastPathChar(bMode); + } + return cmp; + } + + /** + * Compare two paths, checking for identical name. + *

+ * Unlike {@code compare} this method returns {@code 0} when the paths have + * the same characters in their names, even if the mode differs. It is + * intended for use in validation routines detecting duplicate entries. + *

+ * Returns {@code 0} if the names are identical and a conflict exists + * between {@code aPath} and {@code bPath}, as they share the same name. + *

+ * Returns {@code <0} if all possibles occurrences of {@code aPath} sort + * before {@code bPath} and no conflict can happen. In a properly sorted + * tree there are no other occurrences of {@code aPath} and therefore there + * are no duplicate names. + *

+ * Returns {@code >0} when it is possible for a duplicate occurrence of + * {@code aPath} to appear later, after {@code bPath}. Callers should + * continue to examine candidates for {@code bPath} until the method returns + * one of the other return values. + * + * @param aPath + * first path buffer. The range {@code [aPos, aEnd)} is used. + * @param aPos + * index into {@code aPath} where the first path starts. + * @param aEnd + * 1 past last index of {@code aPath}. + * @param bPath + * second path buffer. The range {@code [bPos, bEnd)} is used. + * @param bPos + * index into {@code bPath} where the second path starts. + * @param bEnd + * 1 past last index of {@code bPath}. + * @param bMode + * mode of the second file. Trees are sorted as though + * {@code bPath[bEnd] == '/'}, even if bEnd does not exist. + * @return <0 if no duplicate name could exist; + * 0 if the paths have the same name; + * >0 other {@code bPath} should still be checked by caller. + */ + public static int compareSameName( + byte[] aPath, int aPos, int aEnd, + byte[] bPath, int bPos, int bEnd, int bMode) { + return coreCompare( + aPath, aPos, aEnd, TYPE_TREE, + bPath, bPos, bEnd, bMode); + } + + private static int coreCompare( + byte[] aPath, int aPos, int aEnd, int aMode, + byte[] bPath, int bPos, int bEnd, int bMode) { + while (aPos < aEnd && bPos < bEnd) { + int cmp = (aPath[aPos++] & 0xff) - (bPath[bPos++] & 0xff); + if (cmp != 0) { + return cmp; + } + } + if (aPos < aEnd) { + return (aPath[aPos] & 0xff) - lastPathChar(bMode); + } + if (bPos < bEnd) { + return lastPathChar(aMode) - (bPath[bPos] & 0xff); + } + return 0; + } + + private static int lastPathChar(int mode) { + if ((mode & TYPE_MASK) == TYPE_TREE) { + return '/'; + } + return 0; + } + + private Paths() { + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/RawParseUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/RawParseUtils.java index 45c339fb4..f2955f7e6 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/RawParseUtils.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/RawParseUtils.java @@ -44,6 +44,8 @@ package org.eclipse.jgit.util; +import static java.nio.charset.StandardCharsets.ISO_8859_1; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.eclipse.jgit.lib.ObjectChecker.author; import static org.eclipse.jgit.lib.ObjectChecker.committer; import static org.eclipse.jgit.lib.ObjectChecker.encoding; @@ -60,6 +62,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.PersonIdent; @@ -70,7 +73,7 @@ public final class RawParseUtils { * * @since 2.2 */ - public static final Charset UTF8_CHARSET = Charset.forName("UTF-8"); //$NON-NLS-1$ + public static final Charset UTF8_CHARSET = UTF_8; private static final byte[] digits10; @@ -81,8 +84,9 @@ public final class RawParseUtils { private static final Map encodingAliases; static { - encodingAliases = new HashMap(); - encodingAliases.put("latin-1", Charset.forName("ISO-8859-1")); //$NON-NLS-1$ //$NON-NLS-2$ + encodingAliases = new HashMap<>(); + encodingAliases.put("latin-1", ISO_8859_1); //$NON-NLS-1$ + encodingAliases.put("iso-latin-1", ISO_8859_1); //$NON-NLS-1$ digits10 = new byte['9' + 1]; Arrays.fill(digits10, (byte) -1); @@ -670,6 +674,27 @@ public final class RawParseUtils { return match(b, ptr, encoding); } + /** + * Parse the "encoding " header as a string. + *

+ * Locates the "encoding " header (if present) and returns its value. + * + * @param b + * buffer to scan. + * @return the encoding header as specified in the commit; null if the + * header was not present and should be assumed. + * @since 4.2 + */ + @Nullable + public static String parseEncodingName(final byte[] b) { + int enc = encoding(b, 0); + if (enc < 0) { + return null; + } + int lf = nextLF(b, enc); + return decode(UTF_8, b, enc, lf - 1); + } + /** * Parse the "encoding " header into a character set reference. *

@@ -677,29 +702,33 @@ public final class RawParseUtils { * {@link #encoding(byte[], int)} and then returns the proper character set * to apply to this buffer to evaluate its contents as character data. *

- * If no encoding header is present, {@link Constants#CHARSET} is assumed. + * If no encoding header is present {@code UTF-8} is assumed. * * @param b * buffer to scan. * @return the Java character set representation. Never null. + * @throws IllegalCharsetNameException + * if the character set requested by the encoding header is + * malformed and unsupportable. + * @throws UnsupportedCharsetException + * if the JRE does not support the character set requested by + * the encoding header. */ public static Charset parseEncoding(final byte[] b) { - final int enc = encoding(b, 0); - if (enc < 0) - return Constants.CHARSET; - final int lf = nextLF(b, enc); - String decoded = decode(Constants.CHARSET, b, enc, lf - 1); + String enc = parseEncodingName(b); + if (enc == null) { + return UTF_8; + } + + String name = enc.trim(); try { - return Charset.forName(decoded); - } catch (IllegalCharsetNameException badName) { - Charset aliased = charsetForAlias(decoded); - if (aliased != null) - return aliased; - throw badName; - } catch (UnsupportedCharsetException badName) { - Charset aliased = charsetForAlias(decoded); - if (aliased != null) + return Charset.forName(name); + } catch (IllegalCharsetNameException + | UnsupportedCharsetException badName) { + Charset aliased = charsetForAlias(name); + if (aliased != null) { return aliased; + } throw badName; } } @@ -738,7 +767,15 @@ public final class RawParseUtils { * parsed. */ public static PersonIdent parsePersonIdent(final byte[] raw, final int nameB) { - final Charset cs = parseEncoding(raw); + Charset cs; + try { + cs = parseEncoding(raw); + } catch (IllegalCharsetNameException | UnsupportedCharsetException e) { + // Assume UTF-8 for person identities, usually this is correct. + // If not decode() will fall back to the ISO-8859-1 encoding. + cs = UTF_8; + } + final int emailB = nextLF(raw, nameB, '<'); final int emailE = nextLF(raw, emailB, '>'); if (emailB >= raw.length || raw[emailB] == '\n' || @@ -886,7 +923,7 @@ public final class RawParseUtils { */ public static String decode(final byte[] buffer, final int start, final int end) { - return decode(Constants.CHARSET, buffer, start, end); + return decode(UTF_8, buffer, start, end); } /** @@ -960,23 +997,21 @@ public final class RawParseUtils { public static String decodeNoFallback(final Charset cs, final byte[] buffer, final int start, final int end) throws CharacterCodingException { - final ByteBuffer b = ByteBuffer.wrap(buffer, start, end - start); + ByteBuffer b = ByteBuffer.wrap(buffer, start, end - start); b.mark(); // Try our built-in favorite. The assumption here is that // decoding will fail if the data is not actually encoded // using that encoder. - // try { - return decode(b, Constants.CHARSET); + return decode(b, UTF_8); } catch (CharacterCodingException e) { b.reset(); } - if (!cs.equals(Constants.CHARSET)) { + if (!cs.equals(UTF_8)) { // Try the suggested encoding, it might be right since it was // provided by the caller. - // try { return decode(b, cs); } catch (CharacterCodingException e) { @@ -986,9 +1021,8 @@ public final class RawParseUtils { // Try the default character set. A small group of people // might actually use the same (or very similar) locale. - // - final Charset defcs = Charset.defaultCharset(); - if (!defcs.equals(cs) && !defcs.equals(Constants.CHARSET)) { + Charset defcs = Charset.defaultCharset(); + if (!defcs.equals(cs) && !defcs.equals(UTF_8)) { try { return decode(b, defcs); } catch (CharacterCodingException e) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/StreamCopyThread.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/StreamCopyThread.java index 24b8b5333..8d39a22ac 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/StreamCopyThread.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/StreamCopyThread.java @@ -47,6 +47,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.InterruptedIOException; import java.io.OutputStream; +import java.util.concurrent.atomic.AtomicInteger; /** Thread to copy from an input stream to an output stream. */ public class StreamCopyThread extends Thread { @@ -58,6 +59,8 @@ public class StreamCopyThread extends Thread { private volatile boolean done; + private final AtomicInteger flushCount = new AtomicInteger(0); + /** * Create a thread to copy data from an input stream to an output stream. * @@ -82,6 +85,7 @@ public class StreamCopyThread extends Thread { * the request. */ public void flush() { + flushCount.incrementAndGet(); interrupt(); } @@ -109,22 +113,30 @@ public class StreamCopyThread extends Thread { public void run() { try { final byte[] buf = new byte[BUFFER_SIZE]; - int interruptCounter = 0; + int flushCountBeforeRead = 0; + boolean readInterrupted = false; for (;;) { try { - if (interruptCounter > 0) { + if (readInterrupted) { dst.flush(); - interruptCounter--; + readInterrupted = false; + if (!flushCount.compareAndSet(flushCountBeforeRead, 0)) { + // There was a flush() call since last blocked read. + // Set interrupt status, so next blocked read will throw + // an InterruptedIOException and we will flush again. + interrupt(); + } } if (done) break; + flushCountBeforeRead = flushCount.get(); final int n; try { n = src.read(buf); } catch (InterruptedIOException wakey) { - interruptCounter++; + readInterrupted = true; continue; } if (n < 0) @@ -141,7 +153,7 @@ public class StreamCopyThread extends Thread { // set interrupt status, which will be checked // when we block in src.read - if (writeInterrupted) + if (writeInterrupted || flushCount.get() > 0) interrupt(); break; } diff --git a/tools/default.defs b/tools/default.defs new file mode 100644 index 000000000..3481fa1f8 --- /dev/null +++ b/tools/default.defs @@ -0,0 +1,42 @@ +def java_sources( + name, + srcs, + visibility = ['PUBLIC'] + ): + java_library( + name = name, + resources = srcs, + visibility = visibility, + ) + +def maven_jar( + name, + group, + artifact, + version, + bin_sha1, + src_sha1, + visibility = ['PUBLIC']): + jar_name = '%s__jar' % name + src_name = '%s__src' % name + + remote_file( + name = jar_name, + sha1 = bin_sha1, + url = 'mvn:%s:%s:jar:%s' % (group, artifact, version), + out = '%s.jar' % jar_name, + ) + + remote_file( + name = src_name, + sha1 = src_sha1, + url = 'mvn:%s:%s:src:%s' % (group, artifact, version), + out = '%s.jar' % src_name, + ) + + prebuilt_jar( + name = name, + binary_jar = ':' + jar_name, + source_jar = ':' + src_name, + visibility = visibility) + diff --git a/tools/git.defs b/tools/git.defs new file mode 100644 index 000000000..557dff231 --- /dev/null +++ b/tools/git.defs @@ -0,0 +1,9 @@ +def git_version(): + import subprocess + cmd = ['git', 'describe', '--always', '--match', 'v[0-9].*', '--dirty'] + p = subprocess.Popen(cmd, stdout = subprocess.PIPE) + v = p.communicate()[0].strip() + r = p.returncode + if r != 0: + raise subprocess.CalledProcessError(r, ' '.join(cmd)) + return v