diff options
228 files changed, 14729 insertions, 3908 deletions
diff --git a/.buckconfig b/.buckconfig new file mode 100644 index 0000000000..b2e07acccf --- /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 0000000000..9daac2cea3 --- /dev/null +++ b/.buckversion @@ -0,0 +1 @@ +1b03b4313b91b634bd604fc3487a05f877e59dee diff --git a/.gitignore b/.gitignore index 139e5aee6d..6c62199484 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /target /.project +/buck-cache +/buck-out @@ -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 0000000000..524612bde6 --- /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 0000000000..6e7dec3062 --- /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 0000000000..ae170324e3 --- /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 0000000000..f48f33a1ae --- /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 07f364c535..8058f309f9 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 d42d6f29ee..de81bf82bf 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 0000000000..3743557aa5 --- /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 0000000000..d2ced7a247 --- /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 dd52a89e6c..0af30f659d 100644 --- a/org.eclipse.jgit.http.test/pom.xml +++ b/org.eclipse.jgit.http.test/pom.xml @@ -134,6 +134,10 @@ <artifactId>maven-surefire-plugin</artifactId> <configuration> <argLine>-Djava.io.tmpdir=${project.build.directory} -Xmx300m</argLine> + <includes> + <include>**/*Test.java</include> + <include>**/*Tests.java</include> + </includes> </configuration> </plugin> </plugins> 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 362a09d64f..677132d732 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<String, Ref> 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 fba1a52640..4b15d4b533 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 6fb130231a..ce78442785 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<AccessEvent> 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 9ca0789e29..82861ed9b7 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<String, Ref> 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<AccessEvent> 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<AccessEvent> 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<AccessEvent> 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<AccessEvent> 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<AccessEvent> 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 0000000000..68976a68ae --- /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 0000000000..7e2543220a --- /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 ac9685d375..8439c39c8b 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<R extends Repository> { 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<R extends Repository> { 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<R extends Repository> { Set<ObjectId> all = new HashSet<ObjectId>(); for (Ref r : db.getAllRefs().values()) all.add(r.getObjectId()); - pw.preparePack(m, all, Collections.<ObjectId> emptySet()); + pw.preparePack(m, all, PackWriter.NONE); final ObjectId name = pw.computeName(); @@ -1155,8 +1155,7 @@ public class TestRepository<R extends Repository> { 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 0000000000..a3859c9b49 --- /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 db23c3b55f..2514fdff7e 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 0000000000..5c137f28fe --- /dev/null +++ b/org.eclipse.jgit.pgm.test/org.eclipse.jgit.pgm--All-Tests (Java8) (de).launch @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<launchConfiguration type="org.eclipse.jdt.junit.launchconfig"> +<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS"> +<listEntry value="/org.eclipse.jgit.pgm.test/tst"/> +</listAttribute> +<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES"> +<listEntry value="2"/> +</listAttribute> +<booleanAttribute key="org.eclipse.debug.core.appendEnvironmentVariables" value="true"/> +<mapAttribute key="org.eclipse.debug.core.environmentVariables"> +<mapEntry key="LANG" value="de_DE.UTF-8"/> +</mapAttribute> +<listAttribute key="org.eclipse.debug.ui.favoriteGroups"> +<listEntry value="org.eclipse.debug.ui.launchGroup.debug"/> +<listEntry value="org.eclipse.debug.ui.launchGroup.run"/> +</listAttribute> +<stringAttribute key="org.eclipse.jdt.junit.CONTAINER" value="=org.eclipse.jgit.pgm.test/tst"/> +<booleanAttribute key="org.eclipse.jdt.junit.KEEPRUNNING_ATTR" value="false"/> +<stringAttribute key="org.eclipse.jdt.junit.TESTNAME" value=""/> +<stringAttribute key="org.eclipse.jdt.junit.TEST_KIND" value="org.eclipse.jdt.junit.loader.junit4"/> +<booleanAttribute key="org.eclipse.jdt.launching.ATTR_USE_START_ON_FIRST_THREAD" value="true"/> +<listAttribute key="org.eclipse.jdt.launching.CLASSPATH"> +<listEntry value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <runtimeClasspathEntry containerPath="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.7" path="1" type="4"/> "/> +<listEntry value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <runtimeClasspathEntry id="org.eclipse.jdt.launching.classpathentry.defaultClasspath"> <memento exportedEntriesOnly="false" project="org.eclipse.jgit.pgm.test"/> </runtimeClasspathEntry> "/> +</listAttribute> +<booleanAttribute key="org.eclipse.jdt.launching.DEFAULT_CLASSPATH" value="false"/> +<stringAttribute key="org.eclipse.jdt.launching.JRE_CONTAINER" value="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/> +<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value=""/> +<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="org.eclipse.jgit.pgm.test"/> +</launchConfiguration> 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 559a6d5d40..a6af077aa5 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<String> result = new ArrayList<String>(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<String> result = new ArrayList<String>(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<String> 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<String> 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 d77b1505ae..3f396563c2 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<String> arguments = new ArrayList<String>(); + 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<String> 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<String> result = execute(cmd, db); + for (String line : result) { + System.out.println(line); + } + } + } } public static List<String> 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<String> 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<String> 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 <command> [<args>]', 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<String> outLines() { + return IO.readLines(out.toString()); + } + + public String errString() { + return err.toString(); + } + + public List<String> 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 4253080a66..3edd9b88e8 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 4222a2dcc3..a503ffdad0 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 d1bd5baceb..55f4d8b1b8 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; @@ -63,9 +69,19 @@ 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 cb36d057e4..e690ad6964 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 0000000000..6bccb6d4a7 --- /dev/null +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CommitTest.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2015, Andrey Loskutov <loskutov@gmx.de> + * 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 6352a26524..086e72e9a4 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 975e8c4f76..47199016d4 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 90efae286b..0eee771d2c 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; @@ -98,6 +101,31 @@ public class RepoTest extends CLIRepositoryTestCase { } @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(); xmlContent.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n") 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 dae477928b..16c5889c48 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 { @@ -62,6 +63,20 @@ public class ResetTest extends CLIRepositoryTestCase { } @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(); assertStringArrayEquals("", @@ -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 854c52d88b..368047c602 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; @@ -56,6 +57,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 ab09db5a56..0fe25f550a 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 0000000000..edcf2fc28f --- /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 567fd05750..bc9205c7ce 100644 --- a/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF @@ -19,8 +19,10 @@ Import-Package: org.apache.commons.compress.archivers;version="[1.3,2.0)", org.eclipse.jgit.dircache;version="[4.2.0,4.3.0)", org.eclipse.jgit.errors;version="[4.2.0,4.3.0)", org.eclipse.jgit.gitrepo;version="[4.2.0,4.3.0)", + org.eclipse.jgit.internal.ketch;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 +33,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 c13f63e80f..6aa20041b5 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 ca2ead2925..2642491321 100644 --- a/org.eclipse.jgit.pgm/pom.xml +++ b/org.eclipse.jgit.pgm/pom.xml @@ -95,6 +95,17 @@ </dependency> <dependency> + <groupId>org.eclipse.jgit</groupId> + <artifactId>org.eclipse.jgit.http.apache</artifactId> + <version>${project.version}</version> + </dependency> + + <dependency> + <groupId>org.apache.httpcomponents</groupId> + <artifactId>httpclient</artifactId> + </dependency> + + <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>${slf4j-version}</version> 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 335336da28..b4b1261b37 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 <name> 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 65aa24f356..045f3571e5 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> branches = new ArrayList<String>(); + @Argument(metaVar = "metaVar_name") + private String branch; private final Map<String, Ref> printRefs = new LinkedHashMap<String, Ref>(); @@ -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<String> 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 45794629ec..94517dbf2f 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<String> paths = new ArrayList<String>(); @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 cd6953cb05..04078287fb 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/Daemon.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Daemon.java index 04182d6dbe..03f3fac0b6 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Daemon.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Daemon.java @@ -45,18 +45,29 @@ package org.eclipse.jgit.pgm; import java.io.File; import java.net.InetSocketAddress; +import java.net.URISyntaxException; import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executors; +import org.eclipse.jgit.internal.ketch.KetchLeader; +import org.eclipse.jgit.internal.ketch.KetchLeaderCache; +import org.eclipse.jgit.internal.ketch.KetchPreReceive; +import org.eclipse.jgit.internal.ketch.KetchSystem; +import org.eclipse.jgit.internal.ketch.KetchText; +import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.pgm.internal.CLIText; import org.eclipse.jgit.storage.file.FileBasedConfig; import org.eclipse.jgit.storage.file.WindowCacheConfig; import org.eclipse.jgit.storage.pack.PackConfig; import org.eclipse.jgit.transport.DaemonClient; import org.eclipse.jgit.transport.DaemonService; +import org.eclipse.jgit.transport.ReceivePack; import org.eclipse.jgit.transport.resolver.FileResolver; +import org.eclipse.jgit.transport.resolver.ReceivePackFactory; +import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; +import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; import org.eclipse.jgit.util.FS; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.Option; @@ -90,6 +101,13 @@ class Daemon extends TextBuiltin { @Option(name = "--export-all", usage = "usage_exportWithoutGitDaemonExportOk") boolean exportAll; + @Option(name = "--ketch") + KetchServerType ketchServerType; + + enum KetchServerType { + LEADER; + } + @Argument(required = true, metaVar = "metaVar_directory", usage = "usage_directoriesToExport") final List<File> directory = new ArrayList<File>(); @@ -146,7 +164,9 @@ class Daemon extends TextBuiltin { service(d, n).setOverridable(true); for (final String n : forbidOverride) service(d, n).setOverridable(false); - + if (ketchServerType == KetchServerType.LEADER) { + startKetchLeader(d); + } d.start(); outw.println(MessageFormat.format(CLIText.get().listeningOn, d.getAddress())); } @@ -159,4 +179,29 @@ class Daemon extends TextBuiltin { throw die(MessageFormat.format(CLIText.get().serviceNotSupported, n)); return svc; } + + private void startKetchLeader(org.eclipse.jgit.transport.Daemon daemon) { + KetchSystem system = new KetchSystem(); + final KetchLeaderCache leaders = new KetchLeaderCache(system); + final ReceivePackFactory<DaemonClient> factory; + + factory = daemon.getReceivePackFactory(); + daemon.setReceivePackFactory(new ReceivePackFactory<DaemonClient>() { + @Override + public ReceivePack create(DaemonClient req, Repository repo) + throws ServiceNotEnabledException, + ServiceNotAuthorizedException { + ReceivePack rp = factory.create(req, repo); + KetchLeader leader; + try { + leader = leaders.get(repo); + } catch (URISyntaxException err) { + throw new ServiceNotEnabledException( + KetchText.get().invalidFollowerUri, err); + } + rp.setPreReceiveHook(new KetchPreReceive(leader)); + return rp; + } + }); + } }
\ No newline at end of file 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 f07df1a4b5..a25f1e9305 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 ceb0d6b2fe..d701f22c38 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<String> arguments = new ArrayList<String>(); + 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,21 +241,39 @@ 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. * * @param aGitdir @@ -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 cd65af9549..e739b58ae7 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 70868e920e..24916bd1c2 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<RemoteConfig> 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 db88008e10..ea59527fed 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 4d3af4b560..9cee37b791 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<String> paths = new ArrayList<String>(); + @Argument(required = false, index = 1, metaVar = "metaVar_paths") + @Option(name = "--", metaVar = "metaVar_paths", multiValued = true, handler = RestOfArgumentsHandler.class) + private List<String> 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 e32fc9cab4..c5ecb8496e 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<String, Ref> 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 be82d070f7..6a6322131a 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<String> 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 56cfc7e8ef..0dc549c7d7 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 0000000000..fbd4672f28 --- /dev/null +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/RebuildRefTree.java @@ -0,0 +1,168 @@ +/* + * 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 static org.eclipse.jgit.lib.Constants.HEAD; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +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.lib.StoredConfig; +import org.eclipse.jgit.pgm.Command; +import org.eclipse.jgit.pgm.TextBuiltin; +import org.eclipse.jgit.revwalk.RevWalk; +import org.kohsuke.args4j.Option; + +@Command(usage = "usage_RebuildRefTree") +class RebuildRefTree extends TextBuiltin { + @Option(name = "--enable", usage = "set extensions.refsStorage = reftree") + boolean enable; + + 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); + 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$ + } + + if (enable && !(db.getRefDatabase() instanceof RefTreeDatabase)) { + StoredConfig cfg = db.getConfig(); + cfg.setInt("core", null, "repositoryformatversion", 1); //$NON-NLS-1$ //$NON-NLS-2$ + cfg.setString("extensions", null, "refsStorage", "reftree"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + cfg.save(); + errw.println("Enabled reftree."); //$NON-NLS-1$ + errw.flush(); + } + } + } + + private RefTree rebuild(RefDatabase refdb) throws IOException { + RefTree tree = RefTree.newEmptyTree(); + List<org.eclipse.jgit.internal.storage.reftree.Command> cmds + = new ArrayList<>(); + + Ref head = refdb.exactRef(HEAD); + if (head != null) { + cmds.add(new org.eclipse.jgit.internal.storage.reftree.Command( + null, + head)); + } + + for (Ref r : refdb.getRefs(RefDatabase.ALL).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 f5d581ad01..2812137266 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 3f77aa6687..b531ba65a4 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<OptionHandler> 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<OptionHandler> unsetRequiredOptions() { + List<OptionHandler> options = getOptions(); + List<OptionHandler> backup = new ArrayList<>(options); + for (Iterator<OptionHandler> 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<OptionHandler> backup) { + List<OptionHandler> 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<OptionHandler> getOptions() { + List<OptionHandler> options = null; + try { + Field field = org.kohsuke.args4j.CmdLineParser.class + .getDeclaredField("options"); //$NON-NLS-1$ + field.setAccessible(true); + options = (List<OptionHandler>) 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<OptionHandler> options = getOptions(); + if (options.isEmpty()) { + super.printSingleLineUsage(w, rb); + return; + } + List<OptionHandler> 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<OptionHandler> 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 0000000000..3de7a81091 --- /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<List<?>> { + + /** + * @param parser + * @param option + * @param setter + */ + public OptionWithValuesListHandler(CmdLineParser parser, + OptionDef option, Setter<List<?>> setter) { + super(parser, option, setter); + } + + @Override + public int parseArguments(Parameters params) throws CmdLineException { + final List<String> 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 0000000000..3df3336b4e --- /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 37fd367171..f78fe5b24b 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 0000000000..f12a529e14 --- /dev/null +++ b/org.eclipse.jgit.test/org.eclipse.jgit.core--All-Tests (Java 8) (de).launch @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<launchConfiguration type="org.eclipse.jdt.junit.launchconfig"> +<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS"> +<listEntry value="/org.eclipse.jgit.test/tst"/> +</listAttribute> +<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES"> +<listEntry value="2"/> +</listAttribute> +<booleanAttribute key="org.eclipse.debug.core.appendEnvironmentVariables" value="true"/> +<mapAttribute key="org.eclipse.debug.core.environmentVariables"> +<mapEntry key="LANG" value="de_DE.UTF-8"/> +</mapAttribute> +<listAttribute key="org.eclipse.debug.ui.favoriteGroups"> +<listEntry value="org.eclipse.debug.ui.launchGroup.debug"/> +<listEntry value="org.eclipse.debug.ui.launchGroup.run"/> +</listAttribute> +<stringAttribute key="org.eclipse.jdt.junit.CONTAINER" value="=org.eclipse.jgit.test/tst"/> +<booleanAttribute key="org.eclipse.jdt.junit.KEEPRUNNING_ATTR" value="false"/> +<stringAttribute key="org.eclipse.jdt.junit.TESTNAME" value=""/> +<stringAttribute key="org.eclipse.jdt.junit.TEST_KIND" value="org.eclipse.jdt.junit.loader.junit4"/> +<booleanAttribute key="org.eclipse.jdt.launching.ATTR_USE_START_ON_FIRST_THREAD" value="true"/> +<listAttribute key="org.eclipse.jdt.launching.CLASSPATH"> +<listEntry value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <runtimeClasspathEntry containerPath="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6" path="1" type="4"/> "/> +<listEntry value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <runtimeClasspathEntry id="org.eclipse.jdt.launching.classpathentry.defaultClasspath"> <memento exportedEntriesOnly="false" project="org.eclipse.jgit.test"/> </runtimeClasspathEntry> "/> +</listAttribute> +<booleanAttribute key="org.eclipse.jdt.launching.DEFAULT_CLASSPATH" value="false"/> +<stringAttribute key="org.eclipse.jdt.launching.JRE_CONTAINER" value="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/> +<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value=""/> +<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="org.eclipse.jgit.test"/> +<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Xmx256m"/> +</launchConfiguration> 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 d4bd68e686..4fefdfddab 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,7 +798,6 @@ 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)); @@ -805,6 +805,102 @@ public class AddCommandTest extends RepositoryTestCase { } @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(); config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null, 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 0d03047d53..b39a68a22e 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; @@ -477,6 +480,34 @@ public class CommitCommandTest extends RepositoryTestCase { } @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 { DirCache index = db.lockDirCache(); 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 db811cdf59..3343af06dd 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; @@ -99,6 +106,64 @@ public class PathCheckoutCommandTest extends RepositoryTestCase { } @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(); File written = writeTrashFile(FILE1, ""); 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 a67f2b912a..66f25e8e51 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/attributes/AttributesNodeWorkingTreeIteratorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeWorkingTreeIteratorTest.java index 6ad19a2491..4215ba23e5 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeWorkingTreeIteratorTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeWorkingTreeIteratorTest.java @@ -56,7 +56,6 @@ import java.util.Collections; import java.util.List; import org.eclipse.jgit.attributes.Attribute.State; -import org.eclipse.jgit.errors.CorruptObjectException; import org.eclipse.jgit.junit.JGitTestUtil; import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.lib.FileMode; @@ -253,7 +252,7 @@ public class AttributesNodeWorkingTreeIteratorTest extends RepositoryTestCase { writeTrashFile(name, data.toString()); } - private TreeWalk beginWalk() throws CorruptObjectException { + private TreeWalk beginWalk() { TreeWalk newWalk = new TreeWalk(db); newWalk.addTree(new FileTreeIterator(db)); return newWalk; 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 63ec85861d..c85e156352 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 b6649b3f05..524d0b8e7e 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("<project path=\"foo\" name=\"").append(defaultUri) .append("\" revision=\"").append(BRANCH).append("\" >") .append("<copyfile src=\"hello.txt\" dest=\"Hello\" />") + .append("<copyfile src=\"hello.txt\" dest=\"foo/Hello\" />") .append("</project>").append("</manifest>"); 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/ignore/IgnoreNodeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreNodeTest.java index 5893d8c407..c026efcde3 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreNodeTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreNodeTest.java @@ -55,7 +55,6 @@ import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; -import org.eclipse.jgit.errors.CorruptObjectException; import org.eclipse.jgit.ignore.IgnoreNode.MatchResult; import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.lib.FileMode; @@ -509,7 +508,7 @@ public class IgnoreNodeTest extends RepositoryTestCase { .toString()); } - private void beginWalk() throws CorruptObjectException { + private void beginWalk() { walk = new TreeWalk(db); walk.addTree(new FileTreeIterator(db)); } 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 2d72d2373b..dca356434b 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 bc880a13ef..01d6ee68e8 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<ObjectId> EMPTY_SET_OBJECT = Collections - .<ObjectId> emptySet(); - private static final List<RevObject> EMPTY_LIST_REVS = Collections .<RevObject> 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 } @@ -438,6 +437,38 @@ public class PackWriterTest extends SampleDataRepositoryTestCase { } @Test + public void testDeltaStatistics() throws Exception { + config.setDeltaCompress(true); + FileRepository repo = createBareRepository(); + TestRepository<FileRepository> testRepo = new TestRepository<FileRepository>(repo); + ArrayList<RevObject> 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); writeVerifyPack4(false); @@ -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.<ObjectIdSet> 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.<ObjectId> 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<ObjectId> interestings = new HashSet<ObjectId>(); 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 a92ff8d04e..f4d655f86b 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; @@ -420,29 +418,6 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase { } @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(); final TagBuilder t = new TagBuilder(); @@ -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/LocalDiskRefTreeDatabaseTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/LocalDiskRefTreeDatabaseTest.java new file mode 100644 index 0000000000..47f70d7bdb --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/LocalDiskRefTreeDatabaseTest.java @@ -0,0 +1,126 @@ +/* + * 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.MASTER; +import static org.eclipse.jgit.lib.Constants.ORIG_HEAD; +import static org.eclipse.jgit.lib.Constants.R_HEADS; +import static org.eclipse.jgit.lib.RefDatabase.ALL; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +import org.eclipse.jgit.internal.storage.file.FileRepository; +import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase; +import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefDatabase; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.storage.file.FileRepositoryBuilder; +import org.junit.Before; +import org.junit.Test; + +public class LocalDiskRefTreeDatabaseTest extends LocalDiskRepositoryTestCase { + private FileRepository repo; + private RefTreeDatabase refdb; + private RefDatabase bootstrap; + + private TestRepository<FileRepository> testRepo; + private RevCommit A; + private RevCommit B; + + @Before + public void setUp() throws Exception { + FileRepository init = createWorkRepository(); + FileBasedConfig cfg = init.getConfig(); + cfg.setInt("core", null, "repositoryformatversion", 1); + cfg.setString("extensions", null, "refsStorage", "reftree"); + cfg.save(); + + repo = (FileRepository) new FileRepositoryBuilder() + .setGitDir(init.getDirectory()) + .build(); + refdb = (RefTreeDatabase) repo.getRefDatabase(); + bootstrap = refdb.getBootstrap(); + addRepoToClose(repo); + + RefUpdate head = refdb.newUpdate(HEAD, true); + head.link(R_HEADS + MASTER); + + testRepo = new TestRepository<>(init); + A = testRepo.commit().create(); + B = testRepo.commit(testRepo.getRevWalk().parseCommit(A)); + } + + @Test + public void testHeadOrigHead() throws IOException { + RefUpdate master = refdb.newUpdate(HEAD, false); + master.setExpectedOldObjectId(ObjectId.zeroId()); + master.setNewObjectId(A); + assertEquals(RefUpdate.Result.NEW, master.update()); + assertEquals(A, refdb.exactRef(HEAD).getObjectId()); + + RefUpdate orig = refdb.newUpdate(ORIG_HEAD, true); + orig.setNewObjectId(B); + assertEquals(RefUpdate.Result.NEW, orig.update()); + + File origFile = new File(repo.getDirectory(), ORIG_HEAD); + assertEquals(B.name() + '\n', read(origFile)); + assertEquals(B, bootstrap.exactRef(ORIG_HEAD).getObjectId()); + assertEquals(B, refdb.exactRef(ORIG_HEAD).getObjectId()); + assertFalse(refdb.getRefs(ALL).containsKey(ORIG_HEAD)); + + List<Ref> addl = refdb.getAdditionalRefs(); + assertEquals(2, addl.size()); + assertEquals(ORIG_HEAD, addl.get(1).getName()); + assertEquals(B, addl.get(1).getObjectId()); + } +} 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 0000000000..e4d0f1d291 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/RefTreeDatabaseTest.java @@ -0,0 +1,718 @@ +/* + * 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.ORIG_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<InMemRefTreeRepo> 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()); + assertTrue("no references", refdb.getAdditionalRefs().isEmpty()); + } + + @Test + public void testGetAdditionalRefs() throws IOException { + update("refs/heads/master", A); + + List<Ref> addl = refdb.getAdditionalRefs(); + assertEquals(1, addl.size()); + assertEquals("refs/txn/committed", addl.get(0).getName()); + assertEquals(getTxnCommitted(), addl.get(0).getObjectId()); + } + + @Test + public void testGetRefs_HeadOnOneBranch() throws IOException { + symref(HEAD, "refs/heads/master"); + update("refs/heads/master", A); + + Map<String, Ref> 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<String, Ref> 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<String, Ref> 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<String, Ref> 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<String, Ref> 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<String, Ref> 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<String, Ref> 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<String, Ref> orig = refdb.getRefs(ALL); + + update("refs/heads/next", B); + Map<String, Ref> 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<String, Ref> 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<String, Ref> 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 testUpdate_RefusesOrigHeadOnBare() throws IOException { + assertTrue(refdb.getRepository().isBare()); + ObjectId txnId = getTxnCommitted(); + + RefUpdate orig = refdb.newUpdate(ORIG_HEAD, true); + orig.setNewObjectId(B); + assertEquals(RefUpdate.Result.LOCK_FAILURE, orig.update()); + assertEquals(txnId, getTxnCommitted()); + + ReceiveCommand cmd = command(null, B, ORIG_HEAD); + 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, ORIG_HEAD), + 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<ReceiveCommand> 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<ReceiveCommand> 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<String, Ref> 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<ReceiveCommand> 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<String, Ref> 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<ReceiveCommand> 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<ReceiveCommand> 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<String, Ref> 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 0000000000..8e0f38c69a --- /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<InMemoryRepository> 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 d768e0fa0b..92901f826b 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 a5cd7b5c0b..7fcee3dc82 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 3abe81cf85..43160fb115 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 <author@localhost> 1 +0000\n"); b.append("committer A. U. Thor <author@localhost> 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 <b@c> <b@c> 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 <b@c> <b@c> 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 <author@localhost> 1 +0000\n"); b.append("committer A. U. Thor <author@localhost> 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 <author@localhost> 1 +0000\n"); b.append("committer A. U. Thor <author@localhost> 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 <author@localhost> 1 +0000\n"); b.append("committer A. U. Thor <author@localhost> 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 <author@localhost> " + when + "\n"); b.append("committer A. U. Thor <author@localhost> " + 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 <author@localhost> 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 <author@localhost> 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 <author@localhost> 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 <foo 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("bad email", OBJ_COMMIT, data); + assertSkipListAccepts(OBJ_COMMIT, data); } @Test - public void testInvalidCommitInvalidAuthor2() { - final StringBuilder b = new StringBuilder(); - + public void testInvalidCommitInvalidAuthor2() + throws CorruptObjectException { + StringBuilder b = new StringBuilder(); b.append("tree "); b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); b.append('\n'); - b.append("author A. U. Thor foo> 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 <b> +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 <b>\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 <b> 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 <b> 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 <b> 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 <author@localhost> 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 { - final StringBuilder b = new StringBuilder(); + StringBuilder b = new StringBuilder(); entry(b, "100644 .a"); - final byte[] data = Constants.encodeASCII(b.toString()); + checker.checkTree(encodeASCII(b.toString())); + } + + @Test + 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 651e62c9ca..0000000000 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/T0002_TreeTest.java +++ /dev/null @@ -1,319 +0,0 @@ -/* - * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> - * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> - * 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<FileTreeEntry> files = new ArrayList<FileTreeEntry>(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 2a59f58c66..9c9edc1476 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 beda2a7b97..885c1b5b2d 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; @@ -304,6 +309,86 @@ public class RevCommitParseTest extends RepositoryTestCase { } @Test + public void testParse_incorrectUtf8Name() throws Exception { + ByteArrayOutputStream b = new ByteArrayOutputStream(); + b.write("tree 9788669ad918b6fcce64af8882fc9a81cb6aba67\n" + .getBytes(UTF_8)); + b.write("author au <a@example.com> 1218123387 +0700\n".getBytes(UTF_8)); + b.write("committer co <c@example.com> 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 <a@example.com> 1218123387 +0700\n".getBytes(UTF_8)); + b.write("committer co <c@example.com> 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 <a@example.com> 1218123387 +0700\n".getBytes(UTF_8)); + b.write("committer co <c@example.com> 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 = ""; final RevCommit c = create(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 614f49bf03..82505caf22 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; @@ -362,6 +363,44 @@ public class RevTagParseTest extends RepositoryTestCase { } @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 <t@example.com> 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 <t@example.com> 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 = ""; final RevTag c = create(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 782e414b62..c1e078d10d 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 461530896d..a83a993330 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<RefSpec> 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 aa5914fe03..94bc383db7 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<String, Ref> 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 3f5fcbbf07..4f833509d9 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 @@ -341,6 +341,41 @@ public class RefSpecTest { } @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("*:*"); assertTrue(a.isWildcard()); @@ -404,21 +439,6 @@ public class RefSpecTest { } @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 55e1e44206..5519f61ac2 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<RemoteRefUpdate> result = transport - .findRemoteRefUpdatesFor(Collections.nCopies(1, new RefSpec( - "refs/heads/master:refs/heads/x"))); + Collection<RemoteRefUpdate> 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<RemoteRefUpdate> result = transport - .findRemoteRefUpdatesFor(Collections.nCopies(1, new RefSpec( - "+refs/heads/master"))); + Collection<RemoteRefUpdate> 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<RemoteRefUpdate> result = transport - .findRemoteRefUpdatesFor(Collections.nCopies(1, new RefSpec( - "+refs/heads/*:refs/heads/test/*"))); + Collection<RemoteRefUpdate> 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<RefSpec> specs = Arrays.asList(specA, specC); - final Collection<RemoteRefUpdate> result = transport - .findRemoteRefUpdatesFor(specs); + + Collection<RemoteRefUpdate> 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<RemoteRefUpdate> result = transport - .findRemoteRefUpdatesFor(Collections.nCopies(1, new RefSpec( - "+refs/heads/a:refs/heads/a"))); + + Collection<RemoteRefUpdate> 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 2078dd337b..e55d373347 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 @@ -460,6 +460,48 @@ public class URIishTest { } @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"; URIish u = new URIish(str); 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 aca7c80fd7..c3ff7df8f2 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 d0062e1990..5edc1924f2 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<String, TreeFilter> 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.<String> of(), tw); + } + + private void assertMatches(Set<String> expect, TreeWalk tw) + throws MissingObjectException, IncorrectObjectTypeException, + IOException { + List<String> 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 7273cdbabc..aaeb79c64a 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 0d7d31b3ad..1f78e02087 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,19 +425,28 @@ 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 String base = toOSPathString("c:\\Users\\jdoe\\eclipse\\git\\project"); 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 0000000000..7542ec8910 --- /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 0000000000..fcd87cf9aa --- /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 fd26bfa7f9..a9967ae49e 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 b2a8f677f3..36041f8144 100644 --- a/org.eclipse.jgit/.settings/.api_filters +++ b/org.eclipse.jgit/.settings/.api_filters @@ -1,5 +1,45 @@ <?xml version="1.0" encoding="UTF-8" standalone="no"?> <component id="org.eclipse.jgit" version="2"> + <resource path="META-INF/MANIFEST.MF" type="org.eclipse.jgit.lib.FileTreeEntry"> + <filter id="305324134"> + <message_arguments> + <message_argument value="org.eclipse.jgit.lib.FileTreeEntry"/> + <message_argument value="org.eclipse.jgit_4.2.0"/> + </message_arguments> + </filter> + </resource> + <resource path="META-INF/MANIFEST.MF" type="org.eclipse.jgit.lib.GitlinkTreeEntry"> + <filter id="305324134"> + <message_arguments> + <message_argument value="org.eclipse.jgit.lib.GitlinkTreeEntry"/> + <message_argument value="org.eclipse.jgit_4.2.0"/> + </message_arguments> + </filter> + </resource> + <resource path="META-INF/MANIFEST.MF" type="org.eclipse.jgit.lib.SymlinkTreeEntry"> + <filter id="305324134"> + <message_arguments> + <message_argument value="org.eclipse.jgit.lib.SymlinkTreeEntry"/> + <message_argument value="org.eclipse.jgit_4.2.0"/> + </message_arguments> + </filter> + </resource> + <resource path="META-INF/MANIFEST.MF" type="org.eclipse.jgit.lib.Tree"> + <filter id="305324134"> + <message_arguments> + <message_argument value="org.eclipse.jgit.lib.Tree"/> + <message_argument value="org.eclipse.jgit_4.2.0"/> + </message_arguments> + </filter> + </resource> + <resource path="META-INF/MANIFEST.MF" type="org.eclipse.jgit.lib.TreeEntry"> + <filter id="305324134"> + <message_arguments> + <message_argument value="org.eclipse.jgit.lib.TreeEntry"/> + <message_argument value="org.eclipse.jgit_4.2.0"/> + </message_arguments> + </filter> + </resource> <resource path="src/org/eclipse/jgit/attributes/AttributesNode.java" type="org.eclipse.jgit.attributes.AttributesNode"> <filter comment="attributes weren't really usable in earlier versions" id="338792546"> <message_arguments> diff --git a/org.eclipse.jgit/BUCK b/org.eclipse.jgit/BUCK new file mode 100644 index 0000000000..73e2080576 --- /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 2a953b559d..3d3e74f5fd 100644 --- a/org.eclipse.jgit/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit/META-INF/MANIFEST.MF @@ -59,15 +59,17 @@ Export-Package: org.eclipse.jgit.annotations;version="4.2.0", org.eclipse.jgit.ignore;version="4.2.0", org.eclipse.jgit.ignore.internal;version="4.2.0";x-friends:="org.eclipse.jgit.test", org.eclipse.jgit.internal;version="4.2.0";x-friends:="org.eclipse.jgit.test,org.eclipse.jgit.http.test", + org.eclipse.jgit.internal.ketch;version="4.2.0";x-friends:="org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm", org.eclipse.jgit.internal.storage.dfs;version="4.2.0";x-friends:="org.eclipse.jgit.test,org.eclipse.jgit.http.server", org.eclipse.jgit.internal.storage.file;version="4.2.0"; x-friends:="org.eclipse.jgit.test, 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 0e9b0b59e6..992e10bad6 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/resources/org/eclipse/jgit/internal/ketch/KetchText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/ketch/KetchText.properties new file mode 100644 index 0000000000..1fbb7cb3b5 --- /dev/null +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/ketch/KetchText.properties @@ -0,0 +1,13 @@ +accepted=accepted. +cannotFetchFromLocalReplica=cannot fetch from LocalReplica +failed=failed! +invalidFollowerUri=invalid follower URI +leaderFailedToStore=leader failed to store +localReplicaRequired=LocalReplica instance is required +mismatchedTxnNamespace=mismatched txnNamespace; expected {0} found {1} +outsideTxnNamespace=ref {0} is outside of txnNamespace {1} +proposingUpdates=Proposing updates +queuedProposalFailedToApply=queued proposal failed to apply +starting=starting! +unsupportedVoterCount=unsupported voter count {0}, expected one of {1} +waitingForQueue=Waiting for queue 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 67fb342fe2..3b94f16f1a 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<DirCache> { 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<DirCache> { 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 6a945e4d39..676ae03009 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<ApplyResult> { 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 8743ea9ac7..4f918fa357 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<Ref> { } 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 b3bc319aef..2ac8729507 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<CloneCommand, Git> { } 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<CloneCommand, Git> { 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 6828ed338f..b5057ad282 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<RevCommit> { private PrintStream hookOutRedirect; + private Boolean allowEmpty; + /** * @param repo */ @@ -231,6 +234,16 @@ public class CommitCommand extends GitCommand<RevCommit> { 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<RevCommit> { // 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<RevCommit> { 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 @@ -579,6 +600,27 @@ public class CommitCommand extends GitCommand<RevCommit> { } /** + * @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. + * <p> + * 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 + * <code>true</code> 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 <code>commit</code> */ public String getMessage() { @@ -681,7 +723,7 @@ public class CommitCommand extends GitCommand<RevCommit> { */ 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 9620089b08..de512761a4 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<FetchCommand, FetchResult> { 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 3363a0fc8f..f3527fd805 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<RefSpec> refSpecs = new ArrayList<RefSpec>(1); @@ -199,19 +195,20 @@ public class LsRemoteCommand extends refSpecs.add(new RefSpec("refs/heads/*:refs/remotes/origin/*")); //$NON-NLS-1$ Collection<Ref> refs; Map<String, Ref> refmap = new HashMap<String, Ref>(); - 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/MergeCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java index d2075a70f2..bfe90a3a4f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java @@ -1,6 +1,7 @@ /* * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com> * Copyright (C) 2010-2014, Stefan Lay <stefan.lay@sap.com> + * Copyright (C) 2016, Laurent Delaigue <laurent.delaigue@obeo.fr> * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -65,8 +66,10 @@ import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Config.ConfigEnum; import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectId; 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.RefUpdate; @@ -106,6 +109,8 @@ public class MergeCommand extends GitCommand<MergeResult> { private String message; + private ProgressMonitor monitor = NullProgressMonitor.INSTANCE; + /** * The modes available for fast forward merges corresponding to the * <code>--ff</code>, <code>--no-ff</code> and <code>--ff-only</code> @@ -330,6 +335,7 @@ public class MergeCommand extends GitCommand<MergeResult> { repo.writeSquashCommitMsg(squashMessage); } Merger merger = mergeStrategy.newMerger(repo); + merger.setProgressMonitor(monitor); boolean noProblems; Map<String, org.eclipse.jgit.merge.MergeResult<?>> lowLevelResults = null; Map<String, MergeFailureReason> failingPaths = null; @@ -586,4 +592,23 @@ public class MergeCommand extends GitCommand<MergeResult> { this.message = message; return this; } + + /** + * The progress monitor associated with the diff operation. By default, this + * is set to <code>NullProgressMonitor</code> + * + * @see NullProgressMonitor + * + * @param monitor + * A progress monitor + * @return this instance + * @since 4.2 + */ + public MergeCommand setProgressMonitor(ProgressMonitor monitor) { + if (monitor == null) { + monitor = NullProgressMonitor.INSTANCE; + } + this.monitor = monitor; + return this; + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java index 2783edd5d8..549ef6cf13 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java @@ -1,6 +1,7 @@ /* * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com> * Copyright (C) 2010, Mathias Kinzler <mathias.kinzler@sap.com> + * Copyright (C) 2016, Laurent Delaigue <laurent.delaigue@obeo.fr> * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -326,6 +327,7 @@ public class PullCommand extends TransportCommand<PullCommand, PullResult> { MergeCommand merge = new MergeCommand(repo); merge.include(upstreamName, commitToMerge); merge.setStrategy(strategy); + merge.setProgressMonitor(monitor); MergeResult mergeRes = merge.call(); monitor.update(1); result = new PullResult(fetchRes, remote, mergeRes); 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 8582bbb0dc..643ec7a51f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2010, 2013 Mathias Kinzler <mathias.kinzler@sap.com> + * Copyright (C) 2016, Laurent Delaigue <laurent.delaigue@obeo.fr> * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -560,6 +561,8 @@ public class RebaseCommand extends GitCommand<RebaseResult> { 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)); @@ -609,6 +612,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> { // their non-first parents rewritten MergeCommand merge = git.merge() .setFastForward(MergeCommand.FastForwardMode.NO_FF) + .setProgressMonitor(monitor) .setCommit(false); for (int i = 1; i < commitToPick.getParentCount(); i++) merge.include(newParents.get(i)); @@ -674,6 +678,8 @@ public class RebaseCommand extends GitCommand<RebaseResult> { 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 +1079,12 @@ public class RebaseCommand extends GitCommand<RebaseResult> { 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 +1195,14 @@ public class RebaseCommand extends GitCommand<RebaseResult> { 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 8f4bc4f26c..4c91e6c17f 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<Ref> { 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 7923fd49be..f6903be05c 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<ObjectId> { 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/SubmoduleUpdateCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleUpdateCommand.java index e288d7755b..342d7f42f3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleUpdateCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleUpdateCommand.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2011, GitHub Inc. + * Copyright (C) 2016, Laurent Delaigue <laurent.delaigue@obeo.fr> * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -178,11 +179,13 @@ public class SubmoduleUpdateCommand extends if (ConfigConstants.CONFIG_KEY_MERGE.equals(update)) { MergeCommand merge = new MergeCommand(submoduleRepo); merge.include(commit); + merge.setProgressMonitor(monitor); merge.setStrategy(strategy); merge.call(); } else if (ConfigConstants.CONFIG_KEY_REBASE.equals(update)) { RebaseCommand rebase = new RebaseCommand(submoduleRepo); rebase.setUpstream(commit); + rebase.setProgressMonitor(monitor); rebase.setStrategy(strategy); rebase.call(); } else { 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 1aeb6109ec..3d2e46b26e 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<C extends GitCommand, T> 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 0000000000..b3cc1bfcf2 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/EmtpyCommitException.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2015, Christian Halstrick <christian.halstrick@sap.com> + * 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 70f80aeb7a..0fbc1f8acf 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}. * <p> @@ -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. * <p> 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 fa0339544f..ecdfe823a8 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 da55306665..c10e416082 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 4eb688170c..a1e1d15ac6 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 13885d370c..c987c964c4 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<PathEdit> 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. * <p> @@ -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. * @@ -202,6 +318,22 @@ public class DirCacheEditor extends BaseDirCacheEditor { } /** + * Configure if a file can replace a directory (or vice versa). + * <p> + * 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. * <p> * After apply is invoked the entry is added to the output table, and @@ -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 c8bc0960f4..4ebf2e0d71 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. + * <p> + * 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 c6ea093750..e4db40b889 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 index 936fd82bfd..5f67e3439b 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 <fonseca@diku.dk> - * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> - * Copyright (C) 2007, Shawn O. Pearce <spearce@spearce.org> + * 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 891479d1f4..7eb955006e 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<CopyFile> 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 9a072114a7..915066d58f 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<RepoProject> { 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<RepoProject> { * @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 796eaaebf5..7740a2bb80 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/ketch/ElectionRound.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ElectionRound.java new file mode 100644 index 0000000000..014eab2b45 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ElectionRound.java @@ -0,0 +1,141 @@ +/* + * 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.ketch; + +import static org.eclipse.jgit.internal.ketch.KetchConstants.TERM; + +import java.io.IOException; +import java.util.List; + +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.TreeFormatter; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The initial {@link Round} for a leaderless repository, used to establish a + * leader. + */ +class ElectionRound extends Round { + private static final Logger log = LoggerFactory.getLogger(ElectionRound.class); + + private long term; + + ElectionRound(KetchLeader leader, LogIndex head) { + super(leader, head); + } + + @Override + void start() throws IOException { + ObjectId id; + try (Repository git = leader.openRepository(); + ObjectInserter inserter = git.newObjectInserter()) { + id = bumpTerm(git, inserter); + inserter.flush(); + } + runAsync(id); + } + + @Override + void success() { + // Do nothing upon election, KetchLeader will copy the term. + } + + long getTerm() { + return term; + } + + private ObjectId bumpTerm(Repository git, ObjectInserter inserter) + throws IOException { + CommitBuilder b = new CommitBuilder(); + if (!ObjectId.zeroId().equals(acceptedOldIndex)) { + try (RevWalk rw = new RevWalk(git)) { + RevCommit c = rw.parseCommit(acceptedOldIndex); + b.setTreeId(c.getTree()); + b.setParentId(acceptedOldIndex); + term = parseTerm(c.getFooterLines(TERM)) + 1; + } + } else { + term = 1; + b.setTreeId(inserter.insert(new TreeFormatter())); + } + + StringBuilder msg = new StringBuilder(); + msg.append(KetchConstants.TERM.getName()) + .append(": ") //$NON-NLS-1$ + .append(term); + + String tag = leader.getSystem().newLeaderTag(); + if (tag != null && !tag.isEmpty()) { + msg.append(' ').append(tag); + } + + b.setAuthor(leader.getSystem().newCommitter()); + b.setCommitter(b.getAuthor()); + b.setMessage(msg.toString()); + + if (log.isDebugEnabled()) { + log.debug("Trying to elect myself " + b.getMessage()); //$NON-NLS-1$ + } + return inserter.insert(b); + } + + private static long parseTerm(List<String> footer) { + if (footer.isEmpty()) { + return 0; + } + + String s = footer.get(0); + int p = s.indexOf(' '); + if (p > 0) { + s = s.substring(0, p); + } + return Long.parseLong(s, 10); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchConstants.java new file mode 100644 index 0000000000..171c059db1 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchConstants.java @@ -0,0 +1,82 @@ +/* + * 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.ketch; + +import org.eclipse.jgit.revwalk.FooterKey; + +/** Frequently used constants in a Ketch system. */ +public class KetchConstants { + /** + * Default reference namespace holding {@link #ACCEPTED} and + * {@link #COMMITTED} references and the {@link #STAGE} sub-namespace. + */ + public static final String DEFAULT_TXN_NAMESPACE = "refs/txn/"; //$NON-NLS-1$ + + /** Reference name holding the RefTree accepted by a follower. */ + public static final String ACCEPTED = "accepted"; //$NON-NLS-1$ + + /** Reference name holding the RefTree known to be committed. */ + public static final String COMMITTED = "committed"; //$NON-NLS-1$ + + /** Reference subdirectory holding proposed heads. */ + public static final String STAGE = "stage/"; //$NON-NLS-1$ + + /** Footer containing the current term. */ + public static final FooterKey TERM = new FooterKey("Term"); //$NON-NLS-1$ + + /** Section for Ketch configuration ({@code ketch}). */ + public static final String CONFIG_SECTION_KETCH = "ketch"; //$NON-NLS-1$ + + /** Behavior for a replica ({@code remote.$name.ketch-type}) */ + public static final String CONFIG_KEY_TYPE = "ketch-type"; //$NON-NLS-1$ + + /** Behavior for a replica ({@code remote.$name.ketch-commit}) */ + public static final String CONFIG_KEY_COMMIT = "ketch-commit"; //$NON-NLS-1$ + + /** Behavior for a replica ({@code remote.$name.ketch-speed}) */ + public static final String CONFIG_KEY_SPEED = "ketch-speed"; //$NON-NLS-1$ + + private KetchConstants() { + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchLeader.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchLeader.java new file mode 100644 index 0000000000..3bcd6bcb24 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchLeader.java @@ -0,0 +1,624 @@ +/* + * 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.ketch; + +import static org.eclipse.jgit.internal.ketch.KetchLeader.State.CANDIDATE; +import static org.eclipse.jgit.internal.ketch.KetchLeader.State.LEADER; +import static org.eclipse.jgit.internal.ketch.KetchLeader.State.SHUTDOWN; +import static org.eclipse.jgit.internal.ketch.KetchReplica.Participation.FOLLOWER_ONLY; +import static org.eclipse.jgit.internal.ketch.Proposal.State.QUEUED; + +import java.io.IOException; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.eclipse.jgit.internal.storage.reftree.RefTree; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A leader managing consensus across remote followers. + * <p> + * A leader instance starts up in {@link State#CANDIDATE} and tries to begin a + * new term by sending an {@link ElectionRound} to all replicas. Its term starts + * if a majority of replicas have accepted this leader instance for the term. + * <p> + * Once elected by a majority the instance enters {@link State#LEADER} and runs + * proposals offered to {@link #queueProposal(Proposal)}. This continues until + * the leader is timed out for inactivity, or is deposed by a competing leader + * gaining its own majority. + * <p> + * Once timed out or deposed this {@code KetchLeader} instance should be + * discarded, and a new instance takes over. + * <p> + * Each leader instance coordinates a group of {@link KetchReplica}s. Replica + * instances are owned by the leader instance and must be discarded when the + * leader is discarded. + * <p> + * In Ketch all push requests are issued through the leader. The steps are as + * follows (see {@link KetchPreReceive} for an example): + * <ul> + * <li>Create a {@link Proposal} with the + * {@link org.eclipse.jgit.transport.ReceiveCommand}s that represent the push. + * <li>Invoke {@link #queueProposal(Proposal)} on the leader instance. + * <li>Wait for consensus with {@link Proposal#await()}. + * <li>To examine the status of the push, check {@link Proposal#getCommands()}, + * looking at + * {@link org.eclipse.jgit.internal.storage.reftree.Command#getResult()}. + * </ul> + * <p> + * The leader gains consensus by first pushing the needed objects and a + * {@link RefTree} representing the desired target repository state to the + * {@code refs/txn/accepted} branch on each of the replicas. Once a majority has + * succeeded, the leader commits the state by either pushing the + * {@code refs/txn/accepted} value to {@code refs/txn/committed} (for + * Ketch-aware replicas) or by pushing updates to {@code refs/heads/master}, + * etc. for stock Git replicas. + * <p> + * Internally, the actual transport to replicas is performed on background + * threads via the {@link KetchSystem}'s executor service. For performance, the + * {@link KetchLeader}, {@link KetchReplica} and {@link Proposal} objects share + * some state, and may invoke each other's methods on different threads. This + * access is protected by the leader's {@link #lock} object. Care must be taken + * to prevent concurrent access by correctly obtaining the leader's lock. + */ +public abstract class KetchLeader { + private static final Logger log = LoggerFactory.getLogger(KetchLeader.class); + + /** Current state of the leader instance. */ + public static enum State { + /** Newly created instance trying to elect itself leader. */ + CANDIDATE, + + /** Leader instance elected by a majority. */ + LEADER, + + /** Instance has been deposed by another with a more recent term. */ + DEPOSED, + + /** Leader has been gracefully shutdown, e.g. due to inactivity. */ + SHUTDOWN; + } + + private final KetchSystem system; + + /** Leader's knowledge of replicas for this repository. */ + private KetchReplica[] voters; + private KetchReplica[] followers; + private LocalReplica self; + + /** + * Lock protecting all data within this leader instance. + * <p> + * This lock extends into the {@link KetchReplica} instances used by the + * leader. They share the same lock instance to simplify concurrency. + */ + final Lock lock; + + private State state = CANDIDATE; + + /** Term of this leader, once elected. */ + private long term; + + /** + * Pending proposals accepted into the queue in FIFO order. + * <p> + * These proposals were preflighted and do not contain any conflicts with + * each other and their expectations matched the leader's local view of the + * agreed upon {@code refs/txn/accepted} tree. + */ + private final List<Proposal> queued; + + /** + * State of the repository's RefTree after applying all entries in + * {@link #queued}. New proposals must be consistent with this tree to be + * appended to the end of {@link #queued}. + * <p> + * Must be deep-copied with {@link RefTree#copy()} if + * {@link #roundHoldsReferenceToRefTree} is {@code true}. + */ + private RefTree refTree; + + /** + * If {@code true} {@link #refTree} must be duplicated before queuing the + * next proposal. The {@link #refTree} was passed into the constructor of a + * {@link ProposalRound}, and that external reference to the {@link RefTree} + * object is held by the proposal until it materializes the tree object in + * the object store. This field is set {@code true} when the proposal begins + * execution and set {@code false} once tree objects are persisted in the + * local repository's object store or {@link #refTree} is replaced with a + * copy to isolate it from any running rounds. + * <p> + * If proposals arrive less frequently than the {@code RefTree} is written + * out to the repository the {@link #roundHoldsReferenceToRefTree} behavior + * avoids duplicating {@link #refTree}, reducing both time and memory used. + * However if proposals arrive more frequently {@link #refTree} must be + * duplicated to prevent newly queued proposals from corrupting the + * {@link #runningRound}. + */ + volatile boolean roundHoldsReferenceToRefTree; + + /** End of the leader's log. */ + private LogIndex headIndex; + + /** Leader knows this (and all prior) states are committed. */ + private LogIndex committedIndex; + + /** + * Is the leader idle with no work pending? If {@code true} there is no work + * for the leader (normal state). This field is {@code false} when the + * leader thread is scheduled for execution, or while {@link #runningRound} + * defines a round in progress. + */ + private boolean idle; + + /** Current round the leader is preparing and waiting for a vote on. */ + private Round runningRound; + + /** + * Construct a leader for a Ketch instance. + * + * @param system + * Ketch system configuration the leader must adhere to. + */ + protected KetchLeader(KetchSystem system) { + this.system = system; + this.lock = new ReentrantLock(true /* fair */); + this.queued = new ArrayList<>(4); + this.idle = true; + } + + /** @return system configuration. */ + KetchSystem getSystem() { + return system; + } + + /** + * Configure the replicas used by this Ketch instance. + * <p> + * Replicas should be configured once at creation before any proposals are + * executed. Once elections happen, <b>reconfiguration is a complicated + * concept that is not currently supported</b>. + * + * @param replicas + * members participating with the same repository. + */ + public void setReplicas(Collection<KetchReplica> replicas) { + List<KetchReplica> v = new ArrayList<>(5); + List<KetchReplica> f = new ArrayList<>(5); + for (KetchReplica r : replicas) { + switch (r.getParticipation()) { + case FULL: + v.add(r); + break; + + case FOLLOWER_ONLY: + f.add(r); + break; + } + } + + Collection<Integer> validVoters = validVoterCounts(); + if (!validVoters.contains(Integer.valueOf(v.size()))) { + throw new IllegalArgumentException(MessageFormat.format( + KetchText.get().unsupportedVoterCount, + Integer.valueOf(v.size()), + validVoters)); + } + + LocalReplica me = findLocal(v); + if (me == null) { + throw new IllegalArgumentException( + KetchText.get().localReplicaRequired); + } + + lock.lock(); + try { + voters = v.toArray(new KetchReplica[v.size()]); + followers = f.toArray(new KetchReplica[f.size()]); + self = me; + } finally { + lock.unlock(); + } + } + + private static Collection<Integer> validVoterCounts() { + @SuppressWarnings("boxing") + Integer[] valid = { + // An odd number of voting replicas is required. + 1, 3, 5, 7, 9 }; + return Arrays.asList(valid); + } + + private static LocalReplica findLocal(Collection<KetchReplica> voters) { + for (KetchReplica r : voters) { + if (r instanceof LocalReplica) { + return (LocalReplica) r; + } + } + return null; + } + + /** + * Get an instance of the repository for use by a leader thread. + * <p> + * The caller will close the repository. + * + * @return opened repository for use by the leader thread. + * @throws IOException + * cannot reopen the repository for the leader. + */ + protected abstract Repository openRepository() throws IOException; + + /** + * Queue a reference update proposal for consensus. + * <p> + * This method does not wait for consensus to be reached. The proposal is + * checked to look for risks of conflicts, and then submitted into the queue + * for distribution as soon as possible. + * <p> + * Callers must use {@link Proposal#await()} to see if the proposal is done. + * + * @param proposal + * the proposed reference updates to queue for consideration. + * Once execution is complete the individual reference result + * fields will be populated with the outcome. + * @throws InterruptedException + * current thread was interrupted. The proposal may have been + * aborted if it was not yet queued for execution. + * @throws IOException + * unrecoverable error preventing proposals from being attempted + * by this leader. + */ + public void queueProposal(Proposal proposal) + throws InterruptedException, IOException { + try { + lock.lockInterruptibly(); + } catch (InterruptedException e) { + proposal.abort(); + throw e; + } + try { + if (refTree == null) { + initialize(); + for (Proposal p : queued) { + refTree.apply(p.getCommands()); + } + } else if (roundHoldsReferenceToRefTree) { + refTree = refTree.copy(); + roundHoldsReferenceToRefTree = false; + } + + if (!refTree.apply(proposal.getCommands())) { + // A conflict exists so abort the proposal. + proposal.abort(); + return; + } + + queued.add(proposal); + proposal.notifyState(QUEUED); + + if (idle) { + scheduleLeader(); + } + } finally { + lock.unlock(); + } + } + + private void initialize() throws IOException { + try (Repository git = openRepository(); RevWalk rw = new RevWalk(git)) { + self.initialize(git); + + ObjectId accepted = self.getTxnAccepted(); + if (!ObjectId.zeroId().equals(accepted)) { + RevCommit c = rw.parseCommit(accepted); + headIndex = LogIndex.unknown(accepted); + refTree = RefTree.read(rw.getObjectReader(), c.getTree()); + } else { + headIndex = LogIndex.unknown(ObjectId.zeroId()); + refTree = RefTree.newEmptyTree(); + } + } + } + + private void scheduleLeader() { + idle = false; + system.getExecutor().execute(new Runnable() { + @Override + public void run() { + runLeader(); + } + }); + } + + private void runLeader() { + Round round; + lock.lock(); + try { + switch (state) { + case CANDIDATE: + round = new ElectionRound(this, headIndex); + break; + + case LEADER: + round = newProposalRound(); + break; + + case DEPOSED: + case SHUTDOWN: + default: + log.warn("Leader cannot run {}", state); //$NON-NLS-1$ + // TODO(sop): Redirect proposals. + return; + } + } finally { + lock.unlock(); + } + + try { + round.start(); + } catch (IOException e) { + // TODO(sop) Depose leader if it cannot use its repository. + log.error(KetchText.get().leaderFailedToStore, e); + lock.lock(); + try { + nextRound(); + } finally { + lock.unlock(); + } + } + } + + private ProposalRound newProposalRound() { + List<Proposal> todo = new ArrayList<>(queued); + queued.clear(); + roundHoldsReferenceToRefTree = true; + return new ProposalRound(this, headIndex, todo, refTree); + } + + /** @return term of this leader's reign. */ + long getTerm() { + return term; + } + + /** @return end of the leader's log. */ + LogIndex getHead() { + return headIndex; + } + + /** + * @return state leader knows it has committed across a quorum of replicas. + */ + LogIndex getCommitted() { + return committedIndex; + } + + boolean isIdle() { + return idle; + } + + void runAsync(Round round) { + lock.lock(); + try { + // End of the log is this round. Once transport begins it is + // reasonable to assume at least one replica will eventually get + // this, and there is reasonable probability it commits. + headIndex = round.acceptedNewIndex; + runningRound = round; + + for (KetchReplica replica : voters) { + replica.pushTxnAcceptedAsync(round); + } + for (KetchReplica replica : followers) { + replica.pushTxnAcceptedAsync(round); + } + } finally { + lock.unlock(); + } + } + + /** + * Asynchronous signal from a replica after completion. + * <p> + * Must be called while {@link #lock} is held by the replica. + * + * @param replica + * replica posting a completion event. + */ + void onReplicaUpdate(KetchReplica replica) { + if (log.isDebugEnabled()) { + log.debug("Replica {} finished:\n{}", //$NON-NLS-1$ + replica.describeForLog(), snapshot()); + } + + if (replica.getParticipation() == FOLLOWER_ONLY) { + // Followers cannot vote, so votes haven't changed. + return; + } else if (runningRound == null) { + // No round running, no need to tally votes. + return; + } + + assert headIndex.equals(runningRound.acceptedNewIndex); + int matching = 0; + for (KetchReplica r : voters) { + if (r.hasAccepted(headIndex)) { + matching++; + } + } + + int quorum = voters.length / 2 + 1; + boolean success = matching >= quorum; + if (!success) { + return; + } + + switch (state) { + case CANDIDATE: + term = ((ElectionRound) runningRound).getTerm(); + state = LEADER; + if (log.isDebugEnabled()) { + log.debug("Won election, running term " + term); //$NON-NLS-1$ + } + + //$FALL-THROUGH$ + case LEADER: + committedIndex = headIndex; + if (log.isDebugEnabled()) { + log.debug("Committed {} in term {}", //$NON-NLS-1$ + committedIndex.describeForLog(), + Long.valueOf(term)); + } + nextRound(); + commitAsync(replica); + notifySuccess(runningRound); + if (log.isDebugEnabled()) { + log.debug("Leader state:\n{}", snapshot()); //$NON-NLS-1$ + } + break; + + default: + log.debug("Leader ignoring replica while in {}", state); //$NON-NLS-1$ + break; + } + } + + private void notifySuccess(Round round) { + // Drop the leader lock while notifying Proposal listeners. + lock.unlock(); + try { + round.success(); + } finally { + lock.lock(); + } + } + + private void commitAsync(KetchReplica caller) { + for (KetchReplica r : voters) { + if (r == caller) { + continue; + } + if (r.shouldPushUnbatchedCommit(committedIndex, isIdle())) { + r.pushCommitAsync(committedIndex); + } + } + for (KetchReplica r : followers) { + if (r == caller) { + continue; + } + if (r.shouldPushUnbatchedCommit(committedIndex, isIdle())) { + r.pushCommitAsync(committedIndex); + } + } + } + + /** Schedule the next round; invoked while {@link #lock} is held. */ + void nextRound() { + runningRound = null; + + if (queued.isEmpty()) { + idle = true; + } else { + // Caller holds lock. Reschedule leader on a new thread so + // the call stack can unwind and lock is not held unexpectedly + // during prepare for the next round. + scheduleLeader(); + } + } + + /** @return snapshot this leader. */ + public LeaderSnapshot snapshot() { + lock.lock(); + try { + LeaderSnapshot s = new LeaderSnapshot(); + s.state = state; + s.term = term; + s.headIndex = headIndex; + s.committedIndex = committedIndex; + s.idle = isIdle(); + for (KetchReplica r : voters) { + s.replicas.add(r.snapshot()); + } + for (KetchReplica r : followers) { + s.replicas.add(r.snapshot()); + } + return s; + } finally { + lock.unlock(); + } + } + + /** Gracefully shutdown this leader and cancel outstanding operations. */ + public void shutdown() { + lock.lock(); + try { + if (state != SHUTDOWN) { + state = SHUTDOWN; + for (KetchReplica r : voters) { + r.shutdown(); + } + for (KetchReplica r : followers) { + r.shutdown(); + } + } + } finally { + lock.unlock(); + } + } + + @Override + public String toString() { + return snapshot().toString(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchLeaderCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchLeaderCache.java new file mode 100644 index 0000000000..ba033c1a42 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchLeaderCache.java @@ -0,0 +1,126 @@ +/* + * 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.ketch; + +import java.net.URISyntaxException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.eclipse.jgit.internal.storage.dfs.DfsRepository; +import org.eclipse.jgit.lib.Repository; + +/** + * A cache of live leader instances, keyed by repository. + * <p> + * Ketch only assigns a leader to a repository when needed. If + * {@link #get(Repository)} is called for a repository that does not have a + * leader, the leader is created and added to the cache. + */ +public class KetchLeaderCache { + private final KetchSystem system; + private final ConcurrentMap<String, KetchLeader> leaders; + private final Lock startLock; + + /** + * Initialize a new leader cache. + * + * @param system + * system configuration for the leaders + */ + public KetchLeaderCache(KetchSystem system) { + this.system = system; + leaders = new ConcurrentHashMap<>(); + startLock = new ReentrantLock(true /* fair */); + } + + /** + * Lookup the leader instance for a given repository. + * + * @param repo + * repository to get the leader for. + * @return the leader instance for the repository. + * @throws URISyntaxException + * remote configuration contains an invalid URL. + */ + public KetchLeader get(Repository repo) + throws URISyntaxException { + String key = computeKey(repo); + KetchLeader leader = leaders.get(key); + if (leader != null) { + return leader; + } + return startLeader(key, repo); + } + + private KetchLeader startLeader(String key, Repository repo) + throws URISyntaxException { + startLock.lock(); + try { + KetchLeader leader = leaders.get(key); + if (leader != null) { + return leader; + } + leader = system.createLeader(repo); + leaders.put(key, leader); + return leader; + } finally { + startLock.unlock(); + } + } + + private static String computeKey(Repository repo) { + if (repo instanceof DfsRepository) { + DfsRepository dfs = (DfsRepository) repo; + return dfs.getDescription().getRepositoryName(); + } + + if (repo.getDirectory() != null) { + return repo.getDirectory().toURI().toString(); + } + + throw new IllegalArgumentException(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchPreReceive.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchPreReceive.java new file mode 100644 index 0000000000..1b4307f3fb --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchPreReceive.java @@ -0,0 +1,155 @@ +/* + * 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.ketch; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.eclipse.jgit.internal.ketch.Proposal.State.EXECUTED; +import static org.eclipse.jgit.internal.ketch.Proposal.State.QUEUED; +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.util.Collection; + +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.transport.PreReceiveHook; +import org.eclipse.jgit.transport.ProgressSpinner; +import org.eclipse.jgit.transport.ReceiveCommand; +import org.eclipse.jgit.transport.ReceivePack; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * PreReceiveHook for handling push traffic in a Ketch system. + * <p> + * Install an instance on {@link ReceivePack} to capture the commands and other + * connection state and relay them through the {@link KetchLeader}, allowing the + * leader to gain consensus about the new reference state. + */ +public class KetchPreReceive implements PreReceiveHook { + private static final Logger log = LoggerFactory.getLogger(KetchPreReceive.class); + + private final KetchLeader leader; + + /** + * Construct a hook executing updates through a {@link KetchLeader}. + * + * @param leader + * leader for this repository. + */ + public KetchPreReceive(KetchLeader leader) { + this.leader = leader; + } + + @Override + public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> cmds) { + cmds = ReceiveCommand.filter(cmds, NOT_ATTEMPTED); + if (cmds.isEmpty()) { + return; + } + + try { + Proposal proposal = new Proposal(rp.getRevWalk(), cmds) + .setPushCertificate(rp.getPushCertificate()) + .setAuthor(rp.getRefLogIdent()) + .setMessage("push"); //$NON-NLS-1$ + leader.queueProposal(proposal); + if (proposal.isDone()) { + // This failed fast, e.g. conflict or bad precondition. + return; + } + + ProgressSpinner spinner = new ProgressSpinner( + rp.getMessageOutputStream()); + if (proposal.getState() == QUEUED) { + waitForQueue(proposal, spinner); + } + if (!proposal.isDone()) { + waitForPropose(proposal, spinner); + } + } catch (IOException | InterruptedException e) { + String msg = JGitText.get().transactionAborted; + for (ReceiveCommand cmd : cmds) { + if (cmd.getResult() == NOT_ATTEMPTED) { + cmd.setResult(REJECTED_OTHER_REASON, msg); + } + } + log.error(msg, e); + } + } + + private void waitForQueue(Proposal proposal, ProgressSpinner spinner) + throws InterruptedException { + spinner.beginTask(KetchText.get().waitingForQueue, 1, SECONDS); + while (!proposal.awaitStateChange(QUEUED, 250, MILLISECONDS)) { + spinner.update(); + } + switch (proposal.getState()) { + case RUNNING: + default: + spinner.endTask(KetchText.get().starting); + break; + + case EXECUTED: + spinner.endTask(KetchText.get().accepted); + break; + + case ABORTED: + spinner.endTask(KetchText.get().failed); + break; + } + } + + private void waitForPropose(Proposal proposal, ProgressSpinner spinner) + throws InterruptedException { + spinner.beginTask(KetchText.get().proposingUpdates, 2, SECONDS); + while (!proposal.await(250, MILLISECONDS)) { + spinner.update(); + } + spinner.endTask(proposal.getState() == EXECUTED + ? KetchText.get().accepted + : KetchText.get().failed); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchReplica.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchReplica.java new file mode 100644 index 0000000000..a30bbb260a --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchReplica.java @@ -0,0 +1,755 @@ +/* + * 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.ketch; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.eclipse.jgit.internal.ketch.KetchReplica.CommitSpeed.BATCHED; +import static org.eclipse.jgit.internal.ketch.KetchReplica.CommitSpeed.FAST; +import static org.eclipse.jgit.internal.ketch.KetchReplica.State.CURRENT; +import static org.eclipse.jgit.internal.ketch.KetchReplica.State.LAGGING; +import static org.eclipse.jgit.internal.ketch.KetchReplica.State.OFFLINE; +import static org.eclipse.jgit.internal.ketch.KetchReplica.State.UNKNOWN; +import static org.eclipse.jgit.lib.Constants.HEAD; +import static org.eclipse.jgit.lib.FileMode.TYPE_GITLINK; +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.Type.CREATE; + +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; + +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.internal.storage.reftree.RefTree; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.ReceiveCommand; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.util.SystemReader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A Ketch replica, either {@link LocalReplica} or {@link RemoteGitReplica}. + * <p> + * Replicas can be either a stock Git replica, or a Ketch-aware replica. + * <p> + * A stock Git replica has no special knowledge of Ketch and simply stores + * objects and references. Ketch communicates with the stock Git replica using + * the Git push wire protocol. The {@link KetchLeader} commits an agreed upon + * state by pushing all references to the Git replica, for example + * {@code "refs/heads/master"} is pushed during commit. Stock Git replicas use + * {@link CommitMethod#ALL_REFS} to record the final state. + * <p> + * Ketch-aware replicas understand the {@code RefTree} sent during the proposal + * and during commit are able to update their own reference space to match the + * state represented by the {@code RefTree}. Ketch-aware replicas typically use + * a {@link org.eclipse.jgit.internal.storage.reftree.RefTreeDatabase} and + * {@link CommitMethod#TXN_COMMITTED} to record the final state. + * <p> + * KetchReplica instances are tightly coupled with a single {@link KetchLeader}. + * Some state may be accessed by the leader thread and uses the leader's own + * {@link KetchLeader#lock} to protect shared data. + */ +public abstract class KetchReplica { + static final Logger log = LoggerFactory.getLogger(KetchReplica.class); + private static final byte[] PEEL = { ' ', '^' }; + + /** Participation of a replica in establishing consensus. */ + public enum Participation { + /** Replica can vote. */ + FULL, + + /** Replica does not vote, but tracks leader. */ + FOLLOWER_ONLY; + } + + /** How this replica wants to receive Ketch commit operations. */ + public enum CommitMethod { + /** All references are pushed to the peer as standard Git. */ + ALL_REFS, + + /** Only {@code refs/txn/committed} is written/updated. */ + TXN_COMMITTED; + } + + /** Delay before committing to a replica. */ + public enum CommitSpeed { + /** + * Send the commit immediately, even if it could be batched with the + * next proposal. + */ + FAST, + + /** + * If the next proposal is available, batch the commit with it, + * otherwise just send the commit. This generates less network use, but + * may provide slower consistency on the replica. + */ + BATCHED; + } + + /** Current state of a replica. */ + public enum State { + /** Leader has not yet contacted the replica. */ + UNKNOWN, + + /** Replica is behind the consensus. */ + LAGGING, + + /** Replica matches the consensus. */ + CURRENT, + + /** Replica has a different (or unknown) history. */ + DIVERGENT, + + /** Replica's history contains the leader's history. */ + AHEAD, + + /** Replica can not be contacted. */ + OFFLINE; + } + + private final KetchLeader leader; + private final String replicaName; + private final Participation participation; + private final CommitMethod commitMethod; + private final CommitSpeed commitSpeed; + private final long minRetryMillis; + private final long maxRetryMillis; + private final Map<ObjectId, List<ReceiveCommand>> staged; + private final Map<String, ReceiveCommand> running; + private final Map<String, ReceiveCommand> waiting; + private final List<ReplicaPushRequest> queued; + + /** + * Value known for {@code "refs/txn/accepted"}. + * <p> + * Raft literature refers to this as {@code matchIndex}. + */ + private ObjectId txnAccepted; + + /** + * Value known for {@code "refs/txn/committed"}. + * <p> + * Raft literature refers to this as {@code commitIndex}. In traditional + * Raft this is a state variable inside the follower implementation, but + * Ketch keeps it in the leader. + */ + private ObjectId txnCommitted; + + /** What is happening with this replica. */ + private State state = UNKNOWN; + private String error; + + /** Scheduled retry due to communication failure. */ + private Future<?> retryFuture; + private long lastRetryMillis; + private long retryAtMillis; + + /** + * Configure a replica representation. + * + * @param leader + * instance this replica follows. + * @param name + * unique-ish name identifying this replica for debugging. + * @param cfg + * how Ketch should treat the replica. + */ + protected KetchReplica(KetchLeader leader, String name, ReplicaConfig cfg) { + this.leader = leader; + this.replicaName = name; + this.participation = cfg.getParticipation(); + this.commitMethod = cfg.getCommitMethod(); + this.commitSpeed = cfg.getCommitSpeed(); + this.minRetryMillis = cfg.getMinRetry(MILLISECONDS); + this.maxRetryMillis = cfg.getMaxRetry(MILLISECONDS); + this.staged = new HashMap<>(); + this.running = new HashMap<>(); + this.waiting = new HashMap<>(); + this.queued = new ArrayList<>(4); + } + + /** @return system configuration. */ + public KetchSystem getSystem() { + return getLeader().getSystem(); + } + + /** @return leader instance this replica follows. */ + public KetchLeader getLeader() { + return leader; + } + + /** @return unique-ish name for debugging. */ + public String getName() { + return replicaName; + } + + /** @return description of this replica for error/debug logging purposes. */ + protected String describeForLog() { + return getName(); + } + + /** @return how the replica participates in this Ketch system. */ + public Participation getParticipation() { + return participation; + } + + /** @return how Ketch will commit to the repository. */ + public CommitMethod getCommitMethod() { + return commitMethod; + } + + /** @return when Ketch will commit to the repository. */ + public CommitSpeed getCommitSpeed() { + return commitSpeed; + } + + /** + * Called by leader to perform graceful shutdown. + * <p> + * Default implementation cancels any scheduled retry. Subclasses may add + * additional logic before or after calling {@code super.shutdown()}. + * <p> + * Called with {@link KetchLeader#lock} held by caller. + */ + protected void shutdown() { + Future<?> f = retryFuture; + if (f != null) { + retryFuture = null; + f.cancel(true); + } + } + + ReplicaSnapshot snapshot() { + ReplicaSnapshot s = new ReplicaSnapshot(this); + s.accepted = txnAccepted; + s.committed = txnCommitted; + s.state = state; + s.error = error; + s.retryAtMillis = waitingForRetry() ? retryAtMillis : 0; + return s; + } + + /** + * Update the leader's view of the replica after a poll. + * <p> + * Called with {@link KetchLeader#lock} held by caller. + * + * @param refs + * map of refs from the replica. + */ + void initialize(Map<String, Ref> refs) { + if (txnAccepted == null) { + txnAccepted = getId(refs.get(getSystem().getTxnAccepted())); + } + if (txnCommitted == null) { + txnCommitted = getId(refs.get(getSystem().getTxnCommitted())); + } + } + + ObjectId getTxnAccepted() { + return txnAccepted; + } + + boolean hasAccepted(LogIndex id) { + return equals(txnAccepted, id); + } + + private static boolean equals(@Nullable ObjectId a, LogIndex b) { + return a != null && b != null && AnyObjectId.equals(a, b); + } + + /** + * Schedule a proposal round with the replica. + * <p> + * Called with {@link KetchLeader#lock} held by caller. + * + * @param round + * current round being run by the leader. + */ + void pushTxnAcceptedAsync(Round round) { + List<ReceiveCommand> cmds = new ArrayList<>(); + if (commitSpeed == BATCHED) { + LogIndex committedIndex = leader.getCommitted(); + if (equals(txnAccepted, committedIndex) + && !equals(txnCommitted, committedIndex)) { + prepareTxnCommitted(cmds, committedIndex); + } + } + + // TODO(sop) Lagging replicas should build accept on the fly. + if (round.stageCommands != null) { + for (ReceiveCommand cmd : round.stageCommands) { + // TODO(sop): Do not send certain object graphs to replica. + cmds.add(copy(cmd)); + } + } + cmds.add(new ReceiveCommand( + round.acceptedOldIndex, round.acceptedNewIndex, + getSystem().getTxnAccepted())); + pushAsync(new ReplicaPushRequest(this, cmds)); + } + + private static ReceiveCommand copy(ReceiveCommand c) { + return new ReceiveCommand(c.getOldId(), c.getNewId(), c.getRefName()); + } + + boolean shouldPushUnbatchedCommit(LogIndex committed, boolean leaderIdle) { + return (leaderIdle || commitSpeed == FAST) && hasAccepted(committed); + } + + void pushCommitAsync(LogIndex committed) { + List<ReceiveCommand> cmds = new ArrayList<>(); + prepareTxnCommitted(cmds, committed); + pushAsync(new ReplicaPushRequest(this, cmds)); + } + + private void prepareTxnCommitted(List<ReceiveCommand> cmds, + ObjectId committed) { + removeStaged(cmds, committed); + cmds.add(new ReceiveCommand( + txnCommitted, committed, + getSystem().getTxnCommitted())); + } + + private void removeStaged(List<ReceiveCommand> cmds, ObjectId committed) { + List<ReceiveCommand> a = staged.remove(committed); + if (a != null) { + delete(cmds, a); + } + if (staged.isEmpty() || !(committed instanceof LogIndex)) { + return; + } + + LogIndex committedIndex = (LogIndex) committed; + Iterator<Map.Entry<ObjectId, List<ReceiveCommand>>> itr = staged + .entrySet().iterator(); + while (itr.hasNext()) { + Map.Entry<ObjectId, List<ReceiveCommand>> e = itr.next(); + if (e.getKey() instanceof LogIndex) { + LogIndex stagedIndex = (LogIndex) e.getKey(); + if (stagedIndex.isBefore(committedIndex)) { + delete(cmds, e.getValue()); + itr.remove(); + } + } + } + } + + private static void delete(List<ReceiveCommand> cmds, + List<ReceiveCommand> createCmds) { + for (ReceiveCommand cmd : createCmds) { + ObjectId id = cmd.getNewId(); + String name = cmd.getRefName(); + cmds.add(new ReceiveCommand(id, ObjectId.zeroId(), name)); + } + } + + /** + * Determine the next push for this replica (if any) and start it. + * <p> + * If the replica has successfully accepted the committed state of the + * leader, this method will push all references to the replica using the + * configured {@link CommitMethod}. + * <p> + * If the replica is {@link State#LAGGING} this method will begin catch up + * by sending a more recent {@code refs/txn/accepted}. + * <p> + * Must be invoked with {@link KetchLeader#lock} held by caller. + */ + private void runNextPushRequest() { + LogIndex committed = leader.getCommitted(); + if (!equals(txnCommitted, committed) + && shouldPushUnbatchedCommit(committed, leader.isIdle())) { + pushCommitAsync(committed); + } + + if (queued.isEmpty() || !running.isEmpty() || waitingForRetry()) { + return; + } + + // Collapse all queued requests into a single request. + Map<String, ReceiveCommand> cmdMap = new HashMap<>(); + for (ReplicaPushRequest req : queued) { + for (ReceiveCommand cmd : req.getCommands()) { + String name = cmd.getRefName(); + ReceiveCommand old = cmdMap.remove(name); + if (old != null) { + cmd = new ReceiveCommand( + old.getOldId(), cmd.getNewId(), + name); + } + cmdMap.put(name, cmd); + } + } + queued.clear(); + waiting.clear(); + + List<ReceiveCommand> next = new ArrayList<>(cmdMap.values()); + for (ReceiveCommand cmd : next) { + running.put(cmd.getRefName(), cmd); + } + startPush(new ReplicaPushRequest(this, next)); + } + + private void pushAsync(ReplicaPushRequest req) { + if (defer(req)) { + // TODO(sop) Collapse during long retry outage. + for (ReceiveCommand cmd : req.getCommands()) { + waiting.put(cmd.getRefName(), cmd); + } + queued.add(req); + } else { + for (ReceiveCommand cmd : req.getCommands()) { + running.put(cmd.getRefName(), cmd); + } + startPush(req); + } + } + + private boolean defer(ReplicaPushRequest req) { + if (waitingForRetry()) { + // Prior communication failure; everything is deferred. + return true; + } + + for (ReceiveCommand nextCmd : req.getCommands()) { + ReceiveCommand priorCmd = waiting.get(nextCmd.getRefName()); + if (priorCmd == null) { + priorCmd = running.get(nextCmd.getRefName()); + } + if (priorCmd != null) { + // Another request pending on same ref; that must go first. + // Verify priorCmd.newId == nextCmd.oldId? + return true; + } + } + return false; + } + + private boolean waitingForRetry() { + Future<?> f = retryFuture; + return f != null && !f.isDone(); + } + + private void retryLater(ReplicaPushRequest req) { + Collection<ReceiveCommand> cmds = req.getCommands(); + for (ReceiveCommand cmd : cmds) { + cmd.setResult(NOT_ATTEMPTED, null); + if (!waiting.containsKey(cmd.getRefName())) { + waiting.put(cmd.getRefName(), cmd); + } + } + queued.add(0, new ReplicaPushRequest(this, cmds)); + + if (!waitingForRetry()) { + long delay = KetchSystem.delay( + lastRetryMillis, + minRetryMillis, maxRetryMillis); + if (log.isDebugEnabled()) { + log.debug("Retrying {} after {} ms", //$NON-NLS-1$ + describeForLog(), Long.valueOf(delay)); + } + lastRetryMillis = delay; + retryAtMillis = SystemReader.getInstance().getCurrentTime() + delay; + retryFuture = getSystem().getExecutor() + .schedule(new WeakRetryPush(this), delay, MILLISECONDS); + } + } + + /** Weakly holds a retrying replica, allowing it to garbage collect. */ + static class WeakRetryPush extends WeakReference<KetchReplica> + implements Callable<Void> { + WeakRetryPush(KetchReplica r) { + super(r); + } + + @Override + public Void call() throws Exception { + KetchReplica r = get(); + if (r != null) { + r.doRetryPush(); + } + return null; + } + } + + private void doRetryPush() { + leader.lock.lock(); + try { + retryFuture = null; + runNextPushRequest(); + } finally { + leader.lock.unlock(); + } + } + + /** + * Begin executing a single push. + * <p> + * This method must move processing onto another thread. + * Called with {@link KetchLeader#lock} held by caller. + * + * @param req + * the request to send to the replica. + */ + protected abstract void startPush(ReplicaPushRequest req); + + /** + * Callback from {@link ReplicaPushRequest} upon success or failure. + * <p> + * Acquires the {@link KetchLeader#lock} and updates the leader's internal + * knowledge about this replica to reflect what has been learned during a + * push to the replica. In some cases of divergence this method may take + * some time to determine how the replica has diverged; to reduce contention + * this is evaluated before acquiring the leader lock. + * + * @param repo + * local repository instance used by the push thread. + * @param req + * push request just attempted. + */ + void afterPush(@Nullable Repository repo, ReplicaPushRequest req) { + ReceiveCommand acceptCmd = null; + ReceiveCommand commitCmd = null; + List<ReceiveCommand> stages = null; + + for (ReceiveCommand cmd : req.getCommands()) { + String name = cmd.getRefName(); + if (name.equals(getSystem().getTxnAccepted())) { + acceptCmd = cmd; + } else if (name.equals(getSystem().getTxnCommitted())) { + commitCmd = cmd; + } else if (cmd.getResult() == OK && cmd.getType() == CREATE + && name.startsWith(getSystem().getTxnStage())) { + if (stages == null) { + stages = new ArrayList<>(); + } + stages.add(cmd); + } + } + + State newState = null; + ObjectId acceptId = readId(req, acceptCmd); + if (repo != null && acceptCmd != null && acceptCmd.getResult() != OK + && req.getException() == null) { + try (LagCheck lag = new LagCheck(this, repo)) { + newState = lag.check(acceptId, acceptCmd); + acceptId = lag.getRemoteId(); + } + } + + leader.lock.lock(); + try { + for (ReceiveCommand cmd : req.getCommands()) { + running.remove(cmd.getRefName()); + } + + Throwable err = req.getException(); + if (err != null) { + state = OFFLINE; + error = err.toString(); + retryLater(req); + leader.onReplicaUpdate(this); + return; + } + + lastRetryMillis = 0; + error = null; + updateView(req, acceptId, commitCmd); + + if (acceptCmd != null && acceptCmd.getResult() == OK) { + state = hasAccepted(leader.getHead()) ? CURRENT : LAGGING; + if (stages != null) { + staged.put(acceptCmd.getNewId(), stages); + } + } else if (newState != null) { + state = newState; + } + + leader.onReplicaUpdate(this); + runNextPushRequest(); + } finally { + leader.lock.unlock(); + } + } + + private void updateView(ReplicaPushRequest req, @Nullable ObjectId acceptId, + ReceiveCommand commitCmd) { + if (acceptId != null) { + txnAccepted = acceptId; + } + + ObjectId committed = readId(req, commitCmd); + if (committed != null) { + txnCommitted = committed; + } else if (acceptId != null && txnCommitted == null) { + // Initialize during first conversation. + Map<String, Ref> adv = req.getRefs(); + if (adv != null) { + Ref refs = adv.get(getSystem().getTxnCommitted()); + txnCommitted = getId(refs); + } + } + } + + @Nullable + private static ObjectId readId(ReplicaPushRequest req, + @Nullable ReceiveCommand cmd) { + if (cmd == null) { + // Ref was not in the command list, do not trust advertisement. + return null; + + } else if (cmd.getResult() == OK) { + // Currently at newId. + return cmd.getNewId(); + } + + Map<String, Ref> refs = req.getRefs(); + return refs != null ? getId(refs.get(cmd.getRefName())) : null; + } + + /** + * Fetch objects from the remote using the calling thread. + * <p> + * Called without {@link KetchLeader#lock}. + * + * @param repo + * local repository to fetch objects into. + * @param req + * the request to fetch from a replica. + * @throws IOException + * communication with the replica was not possible. + */ + protected abstract void blockingFetch(Repository repo, + ReplicaFetchRequest req) throws IOException; + + /** + * Build a list of commands to commit {@link CommitMethod#ALL_REFS}. + * + * @param git + * local leader repository to read committed state from. + * @param current + * all known references in the replica's repository. Typically + * this comes from a push advertisement. + * @param committed + * state being pushed to {@code refs/txn/committed}. + * @return commands to update during commit. + * @throws IOException + * cannot read the committed state. + */ + protected Collection<ReceiveCommand> prepareCommit(Repository git, + Map<String, Ref> current, ObjectId committed) throws IOException { + List<ReceiveCommand> delta = new ArrayList<>(); + Map<String, Ref> remote = new HashMap<>(current); + try (RevWalk rw = new RevWalk(git); + TreeWalk tw = new TreeWalk(rw.getObjectReader())) { + tw.setRecursive(true); + tw.addTree(rw.parseCommit(committed).getTree()); + while (tw.next()) { + if (tw.getRawMode(0) != TYPE_GITLINK + || tw.isPathSuffix(PEEL, 2)) { + // Symbolic references cannot be pushed. + // Caching peeled values is handled remotely. + continue; + } + + // TODO(sop) Do not send certain ref names to replica. + String name = RefTree.refName(tw.getPathString()); + Ref oldRef = remote.remove(name); + ObjectId oldId = getId(oldRef); + ObjectId newId = tw.getObjectId(0); + if (!AnyObjectId.equals(oldId, newId)) { + delta.add(new ReceiveCommand(oldId, newId, name)); + } + } + } + + // Delete any extra references not in the committed state. + for (Ref ref : remote.values()) { + if (canDelete(ref)) { + delta.add(new ReceiveCommand( + ref.getObjectId(), ObjectId.zeroId(), + ref.getName())); + } + } + return delta; + } + + boolean canDelete(Ref ref) { + String name = ref.getName(); + if (HEAD.equals(name)) { + return false; + } + if (name.startsWith(getSystem().getTxnNamespace())) { + return false; + } + // TODO(sop) Do not delete precious names from replica. + return true; + } + + @NonNull + static ObjectId getId(@Nullable Ref ref) { + if (ref != null) { + ObjectId id = ref.getObjectId(); + if (id != null) { + return id; + } + } + return ObjectId.zeroId(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchSystem.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchSystem.java new file mode 100644 index 0000000000..71e872e3fa --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchSystem.java @@ -0,0 +1,301 @@ +/* + * 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.ketch; + +import static org.eclipse.jgit.internal.ketch.KetchConstants.ACCEPTED; +import static org.eclipse.jgit.internal.ketch.KetchConstants.COMMITTED; +import static org.eclipse.jgit.internal.ketch.KetchConstants.CONFIG_KEY_TYPE; +import static org.eclipse.jgit.internal.ketch.KetchConstants.CONFIG_SECTION_KETCH; +import static org.eclipse.jgit.internal.ketch.KetchConstants.DEFAULT_TXN_NAMESPACE; +import static org.eclipse.jgit.internal.ketch.KetchConstants.STAGE; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_NAME; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_REMOTE; + +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.RemoteConfig; +import org.eclipse.jgit.transport.URIish; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Ketch system-wide configuration. + * <p> + * This class provides useful defaults for testing and small proof of concepts. + * Full scale installations are expected to subclass and override methods to + * provide consistent configuration across all managed repositories. + * <p> + * Servers should configure their own {@link ScheduledExecutorService}. + */ +public class KetchSystem { + private static final Random RNG = new Random(); + + /** @return default executor, one thread per available processor. */ + public static ScheduledExecutorService defaultExecutor() { + return DefaultExecutorHolder.I; + } + + private final ScheduledExecutorService executor; + private final String txnNamespace; + private final String txnAccepted; + private final String txnCommitted; + private final String txnStage; + + /** Create a default system with a thread pool of 1 thread per CPU. */ + public KetchSystem() { + this(defaultExecutor(), DEFAULT_TXN_NAMESPACE); + } + + /** + * Create a Ketch system with the provided executor service. + * + * @param executor + * thread pool to run background operations. + * @param txnNamespace + * reference namespace for the RefTree graph and associated + * transaction state. Must begin with {@code "refs/"} and end + * with {@code '/'}, for example {@code "refs/txn/"}. + */ + public KetchSystem(ScheduledExecutorService executor, String txnNamespace) { + this.executor = executor; + this.txnNamespace = txnNamespace; + this.txnAccepted = txnNamespace + ACCEPTED; + this.txnCommitted = txnNamespace + COMMITTED; + this.txnStage = txnNamespace + STAGE; + } + + /** @return executor to perform background operations. */ + public ScheduledExecutorService getExecutor() { + return executor; + } + + /** + * Get the namespace used for the RefTree graph and transaction management. + * + * @return reference namespace such as {@code "refs/txn/"}. + */ + public String getTxnNamespace() { + return txnNamespace; + } + + /** @return name of the accepted RefTree graph. */ + public String getTxnAccepted() { + return txnAccepted; + } + + /** @return name of the committed RefTree graph. */ + public String getTxnCommitted() { + return txnCommitted; + } + + /** @return prefix for staged objects, e.g. {@code "refs/txn/stage/"}. */ + public String getTxnStage() { + return txnStage; + } + + /** @return identity line for the committer header of a RefTreeGraph. */ + public PersonIdent newCommitter() { + String name = "ketch"; //$NON-NLS-1$ + String email = "ketch@system"; //$NON-NLS-1$ + return new PersonIdent(name, email); + } + + /** + * Construct a random tag to identify a candidate during leader election. + * <p> + * Multiple processes trying to elect themselves leaders at exactly the same + * time (rounded to seconds) using the same {@link #newCommitter()} identity + * strings, for the same term, may generate the same ObjectId for the + * election commit and falsely assume they have both won. + * <p> + * Candidates add this tag to their election ballot commit to disambiguate + * the election. The tag only needs to be unique for a given triplet of + * {@link #newCommitter()}, system time (rounded to seconds), and term. If + * every replica in the system uses a unique {@code newCommitter} (such as + * including the host name after the {@code "@"} in the email address) the + * tag could be the empty string. + * <p> + * The default implementation generates a few bytes of random data. + * + * @return unique tag; null or empty string if {@code newCommitter()} is + * sufficiently unique to identify the leader. + */ + @Nullable + public String newLeaderTag() { + int n = RNG.nextInt(1 << (6 * 4)); + return String.format("%06x", Integer.valueOf(n)); //$NON-NLS-1$ + } + + /** + * Construct the KetchLeader instance of a repository. + * + * @param repo + * local repository stored by the leader. + * @return leader instance. + * @throws URISyntaxException + * a follower configuration contains an unsupported URI. + */ + public KetchLeader createLeader(final Repository repo) + throws URISyntaxException { + KetchLeader leader = new KetchLeader(this) { + @Override + protected Repository openRepository() { + repo.incrementOpen(); + return repo; + } + }; + leader.setReplicas(createReplicas(leader, repo)); + return leader; + } + + /** + * Get the collection of replicas for a repository. + * <p> + * The collection of replicas must include the local repository. + * + * @param leader + * the leader driving these replicas. + * @param repo + * repository to get the replicas of. + * @return collection of replicas for the specified repository. + * @throws URISyntaxException + * a configured URI is invalid. + */ + protected List<KetchReplica> createReplicas(KetchLeader leader, + Repository repo) throws URISyntaxException { + List<KetchReplica> replicas = new ArrayList<>(); + Config cfg = repo.getConfig(); + String localName = getLocalName(cfg); + for (String name : cfg.getSubsections(CONFIG_KEY_REMOTE)) { + if (!hasParticipation(cfg, name)) { + continue; + } + + ReplicaConfig kc = ReplicaConfig.newFromConfig(cfg, name); + if (name.equals(localName)) { + replicas.add(new LocalReplica(leader, name, kc)); + continue; + } + + RemoteConfig rc = new RemoteConfig(cfg, name); + List<URIish> uris = rc.getPushURIs(); + if (uris.isEmpty()) { + uris = rc.getURIs(); + } + for (URIish uri : uris) { + String n = uris.size() == 1 ? name : uri.getHost(); + replicas.add(new RemoteGitReplica(leader, n, uri, kc, rc)); + } + } + return replicas; + } + + private static boolean hasParticipation(Config cfg, String name) { + return cfg.getString(CONFIG_KEY_REMOTE, name, CONFIG_KEY_TYPE) != null; + } + + private static String getLocalName(Config cfg) { + return cfg.getString(CONFIG_SECTION_KETCH, null, CONFIG_KEY_NAME); + } + + static class DefaultExecutorHolder { + private static final Logger log = LoggerFactory.getLogger(KetchSystem.class); + static final ScheduledExecutorService I = create(); + + private static ScheduledExecutorService create() { + int cores = Runtime.getRuntime().availableProcessors(); + int threads = Math.max(5, cores); + log.info("Using {} threads", Integer.valueOf(threads)); //$NON-NLS-1$ + return Executors.newScheduledThreadPool( + threads, + new ThreadFactory() { + private final AtomicInteger threadCnt = new AtomicInteger(); + + @Override + public Thread newThread(Runnable r) { + int id = threadCnt.incrementAndGet(); + Thread thr = new Thread(r); + thr.setName("KetchExecutor-" + id); //$NON-NLS-1$ + return thr; + } + }); + } + + private DefaultExecutorHolder() { + } + } + + /** + * Compute a delay in a {@code min..max} interval with random jitter. + * + * @param last + * amount of delay waited before the last attempt. This is used + * to seed the next delay interval. Should be 0 if there was no + * prior delay. + * @param min + * shortest amount of allowable delay between attempts. + * @param max + * longest amount of allowable delay between attempts. + * @return new amount of delay to wait before the next attempt. + */ + static long delay(long last, long min, long max) { + long r = Math.max(0, last * 3 - min); + if (r > 0) { + int c = (int) Math.min(r + 1, Integer.MAX_VALUE); + r = RNG.nextInt(c); + } + return Math.max(Math.min(min + r, max), min); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchText.java new file mode 100644 index 0000000000..b6c3bc92c5 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchText.java @@ -0,0 +1,70 @@ +/* + * 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.ketch; + +import org.eclipse.jgit.nls.NLS; +import org.eclipse.jgit.nls.TranslationBundle; + +/** Translation bundle for the Ketch implementation. */ +public class KetchText extends TranslationBundle { + /** @return instance of this translation bundle. */ + public static KetchText get() { + return NLS.getBundleFor(KetchText.class); + } + + // @formatter:off + /***/ public String accepted; + /***/ public String cannotFetchFromLocalReplica; + /***/ public String failed; + /***/ public String invalidFollowerUri; + /***/ public String leaderFailedToStore; + /***/ public String localReplicaRequired; + /***/ public String mismatchedTxnNamespace; + /***/ public String outsideTxnNamespace; + /***/ public String proposingUpdates; + /***/ public String queuedProposalFailedToApply; + /***/ public String starting; + /***/ public String unsupportedVoterCount; + /***/ public String waitingForQueue; +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LagCheck.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LagCheck.java new file mode 100644 index 0000000000..35327ea0b3 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LagCheck.java @@ -0,0 +1,183 @@ +/* + * 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.ketch; + +import static org.eclipse.jgit.internal.ketch.KetchReplica.State.AHEAD; +import static org.eclipse.jgit.internal.ketch.KetchReplica.State.DIVERGENT; +import static org.eclipse.jgit.internal.ketch.KetchReplica.State.LAGGING; +import static org.eclipse.jgit.internal.ketch.KetchReplica.State.UNKNOWN; +import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT; + +import java.io.IOException; +import java.util.Collections; +import java.util.Map; + +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.ObjectId; +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; + +/** + * A helper to check if a {@link KetchReplica} is ahead or behind the leader. + */ +class LagCheck implements AutoCloseable { + private final KetchReplica replica; + private final Repository repo; + private RevWalk rw; + private ObjectId remoteId; + + LagCheck(KetchReplica replica, Repository repo) { + this.replica = replica; + this.repo = repo; + initRevWalk(); + } + + private void initRevWalk() { + if (rw != null) { + rw.close(); + } + + rw = new RevWalk(repo); + rw.setRetainBody(false); + } + + public void close() { + if (rw != null) { + rw.close(); + rw = null; + } + } + + ObjectId getRemoteId() { + return remoteId; + } + + KetchReplica.State check(ObjectId acceptId, ReceiveCommand acceptCmd) { + remoteId = acceptId; + if (remoteId == null) { + // Nothing advertised by the replica, value is unknown. + return UNKNOWN; + } + + if (AnyObjectId.equals(remoteId, ObjectId.zeroId())) { + // Replica does not have the txnAccepted reference. + return LAGGING; + } + + try { + RevCommit remote; + try { + remote = parseRemoteCommit(acceptCmd.getRefName()); + } catch (RefGoneException gone) { + // Replica does not have the txnAccepted reference. + return LAGGING; + } catch (MissingObjectException notFound) { + // Local repository does not know this commit so it cannot + // be including the replica's log. + return DIVERGENT; + } + + RevCommit head = rw.parseCommit(acceptCmd.getNewId()); + if (rw.isMergedInto(remote, head)) { + return LAGGING; + } + + // TODO(sop) Check term to see if my leader was deposed. + if (rw.isMergedInto(head, remote)) { + return AHEAD; + } else { + return DIVERGENT; + } + } catch (IOException err) { + KetchReplica.log.error(String.format( + "Cannot compare %s", //$NON-NLS-1$ + acceptCmd.getRefName()), err); + return UNKNOWN; + } + } + + private RevCommit parseRemoteCommit(String refName) + throws IOException, MissingObjectException, RefGoneException { + try { + return rw.parseCommit(remoteId); + } catch (MissingObjectException notLocal) { + // Fall through and try to acquire the object by fetching it. + } + + ReplicaFetchRequest fetch = new ReplicaFetchRequest( + Collections.singleton(refName), + Collections.<ObjectId> emptySet()); + try { + replica.blockingFetch(repo, fetch); + } catch (IOException fetchErr) { + KetchReplica.log.error(String.format( + "Cannot fetch %s (%s) from %s", //$NON-NLS-1$ + remoteId.abbreviate(8).name(), refName, + replica.describeForLog()), fetchErr); + throw new MissingObjectException(remoteId, OBJ_COMMIT); + } + + Map<String, Ref> adv = fetch.getRefs(); + if (adv == null) { + throw new MissingObjectException(remoteId, OBJ_COMMIT); + } + + Ref ref = adv.get(refName); + if (ref == null || ref.getObjectId() == null) { + throw new RefGoneException(); + } + + initRevWalk(); + remoteId = ref.getObjectId(); + return rw.parseCommit(remoteId); + } + + private static class RefGoneException extends Exception { + private static final long serialVersionUID = 1L; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LeaderSnapshot.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LeaderSnapshot.java new file mode 100644 index 0000000000..28a49df97a --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LeaderSnapshot.java @@ -0,0 +1,164 @@ +/* + * 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.ketch; + +import static org.eclipse.jgit.internal.ketch.KetchReplica.State.OFFLINE; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.lib.ObjectId; + +/** A snapshot of a leader and its view of the world. */ +public class LeaderSnapshot { + final List<ReplicaSnapshot> replicas = new ArrayList<>(); + KetchLeader.State state; + long term; + LogIndex headIndex; + LogIndex committedIndex; + boolean idle; + + LeaderSnapshot() { + } + + /** @return unmodifiable view of configured replicas. */ + public Collection<ReplicaSnapshot> getReplicas() { + return Collections.unmodifiableList(replicas); + } + + /** @return current state of the leader. */ + public KetchLeader.State getState() { + return state; + } + + /** + * @return {@code true} if the leader is not running a round to reach + * consensus, and has no rounds queued. + */ + public boolean isIdle() { + return idle; + } + + /** + * @return term of this leader. Valid only if {@link #getState()} is + * currently {@link KetchLeader.State#LEADER}. + */ + public long getTerm() { + return term; + } + + /** + * @return end of the leader's log; null if leader hasn't started up enough + * to begin its own election. + */ + @Nullable + public LogIndex getHead() { + return headIndex; + } + + /** + * @return state the leader knows is committed on a majority of participant + * replicas. Null until the leader instance has committed a log + * index within its own term. + */ + @Nullable + public LogIndex getCommitted() { + return committedIndex; + } + + @Override + public String toString() { + StringBuilder s = new StringBuilder(); + s.append(isIdle() ? "IDLE" : "RUNNING"); //$NON-NLS-1$ //$NON-NLS-2$ + s.append(" state ").append(getState()); //$NON-NLS-1$ + if (getTerm() > 0) { + s.append(" term ").append(getTerm()); //$NON-NLS-1$ + } + s.append('\n'); + s.append(String.format( + "%-10s %12s %12s\n", //$NON-NLS-1$ + "Replica", "Accepted", "Committed")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + s.append("------------------------------------\n"); //$NON-NLS-1$ + debug(s, "(leader)", getHead(), getCommitted()); //$NON-NLS-1$ + s.append('\n'); + for (ReplicaSnapshot r : getReplicas()) { + debug(s, r); + s.append('\n'); + } + s.append('\n'); + return s.toString(); + } + + private static void debug(StringBuilder b, ReplicaSnapshot s) { + KetchReplica replica = s.getReplica(); + debug(b, replica.getName(), s.getAccepted(), s.getCommitted()); + b.append(String.format(" %-8s %s", //$NON-NLS-1$ + replica.getParticipation(), s.getState())); + if (s.getState() == OFFLINE) { + String err = s.getErrorMessage(); + if (err != null) { + b.append(" (").append(err).append(')'); //$NON-NLS-1$ + } + } + } + + private static void debug(StringBuilder s, String name, + ObjectId accepted, ObjectId committed) { + s.append(String.format( + "%-10s %-12s %-12s", //$NON-NLS-1$ + name, str(accepted), str(committed))); + } + + static String str(ObjectId c) { + if (c instanceof LogIndex) { + return ((LogIndex) c).describeForLog(); + } else if (c != null) { + return c.abbreviate(8).name(); + } + return "-"; //$NON-NLS-1$ + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LocalReplica.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LocalReplica.java new file mode 100644 index 0000000000..e297bca45e --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LocalReplica.java @@ -0,0 +1,229 @@ +/* + * 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.ketch; + +import static org.eclipse.jgit.internal.ketch.KetchReplica.CommitMethod.ALL_REFS; +import static org.eclipse.jgit.internal.ketch.KetchReplica.CommitMethod.TXN_COMMITTED; +import static org.eclipse.jgit.lib.RefDatabase.ALL; +import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK; +import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON; + +import java.io.IOException; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import org.eclipse.jgit.internal.storage.reftree.RefTreeDatabase; +import org.eclipse.jgit.lib.BatchRefUpdate; +import org.eclipse.jgit.lib.NullProgressMonitor; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefDatabase; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.ReceiveCommand; + +/** Ketch replica running on the same system as the {@link KetchLeader}. */ +public class LocalReplica extends KetchReplica { + /** + * Configure a local replica. + * + * @param leader + * instance this replica follows. + * @param name + * unique-ish name identifying this replica for debugging. + * @param cfg + * how Ketch should treat the local system. + */ + public LocalReplica(KetchLeader leader, String name, ReplicaConfig cfg) { + super(leader, name, cfg); + } + + @Override + protected String describeForLog() { + return String.format("%s (leader)", getName()); //$NON-NLS-1$ + } + + /** + * Initializes local replica by reading accepted and committed references. + * <p> + * Loads accepted and committed references from the reference database of + * the local replica and stores their current ObjectIds in memory. + * + * @param repo + * repository to initialize state from. + * @throws IOException + * cannot read repository state. + */ + void initialize(Repository repo) throws IOException { + RefDatabase refdb = repo.getRefDatabase(); + if (refdb instanceof RefTreeDatabase) { + RefTreeDatabase treeDb = (RefTreeDatabase) refdb; + String txnNamespace = getSystem().getTxnNamespace(); + if (!txnNamespace.equals(treeDb.getTxnNamespace())) { + throw new IOException(MessageFormat.format( + KetchText.get().mismatchedTxnNamespace, + txnNamespace, treeDb.getTxnNamespace())); + } + refdb = treeDb.getBootstrap(); + } + initialize(refdb.exactRef( + getSystem().getTxnAccepted(), + getSystem().getTxnCommitted())); + } + + @Override + protected void startPush(final ReplicaPushRequest req) { + getSystem().getExecutor().execute(new Runnable() { + @Override + public void run() { + try (Repository git = getLeader().openRepository()) { + try { + update(git, req); + req.done(git); + } catch (Throwable err) { + req.setException(git, err); + } + } catch (IOException err) { + req.setException(null, err); + } + } + }); + } + + @Override + protected void blockingFetch(Repository repo, ReplicaFetchRequest req) + throws IOException { + throw new IOException(KetchText.get().cannotFetchFromLocalReplica); + } + + private void update(Repository git, ReplicaPushRequest req) + throws IOException { + RefDatabase refdb = git.getRefDatabase(); + CommitMethod method = getCommitMethod(); + + // Local replica probably uses RefTreeDatabase, the request should + // be only for the txnNamespace, so drop to the bootstrap layer. + if (refdb instanceof RefTreeDatabase) { + if (!isOnlyTxnNamespace(req.getCommands())) { + return; + } + + refdb = ((RefTreeDatabase) refdb).getBootstrap(); + method = TXN_COMMITTED; + } + + BatchRefUpdate batch = refdb.newBatchUpdate(); + batch.setRefLogIdent(getSystem().newCommitter()); + batch.setRefLogMessage("ketch", false); //$NON-NLS-1$ + batch.setAllowNonFastForwards(true); + + // RefDirectory updates multiple references sequentially. + // Run everything else first, then accepted (if present), + // then committed (if present). This ensures an earlier + // failure will not update these critical references. + ReceiveCommand accepted = null; + ReceiveCommand committed = null; + for (ReceiveCommand cmd : req.getCommands()) { + String name = cmd.getRefName(); + if (name.equals(getSystem().getTxnAccepted())) { + accepted = cmd; + } else if (name.equals(getSystem().getTxnCommitted())) { + committed = cmd; + } else { + batch.addCommand(cmd); + } + } + if (committed != null && method == ALL_REFS) { + Map<String, Ref> refs = refdb.getRefs(ALL); + batch.addCommand(prepareCommit(git, refs, committed.getNewId())); + } + if (accepted != null) { + batch.addCommand(accepted); + } + if (committed != null) { + batch.addCommand(committed); + } + + try (RevWalk rw = new RevWalk(git)) { + batch.execute(rw, NullProgressMonitor.INSTANCE); + } + + // KetchReplica only cares about accepted and committed in + // advertisement. If they failed, store the current values + // back in the ReplicaPushRequest. + List<String> failed = new ArrayList<>(2); + checkFailed(failed, accepted); + checkFailed(failed, committed); + if (!failed.isEmpty()) { + String[] arr = failed.toArray(new String[failed.size()]); + req.setRefs(refdb.exactRef(arr)); + } + } + + private static void checkFailed(List<String> failed, ReceiveCommand cmd) { + if (cmd != null && cmd.getResult() != OK) { + failed.add(cmd.getRefName()); + } + } + + private boolean isOnlyTxnNamespace(Collection<ReceiveCommand> cmdList) { + // Be paranoid and reject non txnNamespace names, this + // is a programming error in Ketch that should not occur. + + String txnNamespace = getSystem().getTxnNamespace(); + for (ReceiveCommand cmd : cmdList) { + if (!cmd.getRefName().startsWith(txnNamespace)) { + cmd.setResult(REJECTED_OTHER_REASON, + MessageFormat.format( + KetchText.get().outsideTxnNamespace, + cmd.getRefName(), txnNamespace)); + ReceiveCommand.abort(cmdList); + return false; + } + } + return true; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LogIndex.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LogIndex.java new file mode 100644 index 0000000000..350c8ed62e --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LogIndex.java @@ -0,0 +1,119 @@ +/* + * 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.ketch; + +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.ObjectId; + +/** + * An ObjectId for a commit extended with incrementing log index. + * <p> + * For any two LogIndex instances, {@code A} is an ancestor of {@code C} + * reachable through parent edges in the graph if {@code A.index < C.index}. + * LogIndex provides a performance optimization for Ketch, the same information + * can be obtained from {@link org.eclipse.jgit.revwalk.RevWalk}. + * <p> + * Index values are only valid within a single {@link KetchLeader} instance + * after it has won an election. By restricting scope to a single leader new + * leaders do not need to traverse the entire history to determine the next + * {@code index} for new proposals. This differs from Raft, where leader + * election uses the log index and the term number to determine which replica + * holds a sufficiently up-to-date log. Since Ketch uses Git objects for storage + * of its replicated log, it keeps the term number as Raft does but uses + * standard Git operations to imply the log index. + * <p> + * {@link Round#runAsync(AnyObjectId)} bumps the index as each new round is + * constructed. + */ +public class LogIndex extends ObjectId { + static LogIndex unknown(AnyObjectId id) { + return new LogIndex(id, 0); + } + + private final long index; + + private LogIndex(AnyObjectId id, long index) { + super(id); + this.index = index; + } + + LogIndex nextIndex(AnyObjectId id) { + return new LogIndex(id, index + 1); + } + + /** @return index provided by the current leader instance. */ + public long getIndex() { + return index; + } + + /** + * Check if this log position committed before another log position. + * <p> + * Only valid for log positions in memory for the current leader. + * + * @param c + * other (more recent) log position. + * @return true if this log position was before {@code c} or equal to c and + * therefore any agreement of {@code c} implies agreement on this + * log position. + */ + boolean isBefore(LogIndex c) { + return index <= c.index; + } + + /** + * @return string suitable for debug logging containing the log index and + * abbreviated ObjectId. + */ + @SuppressWarnings("boxing") + public String describeForLog() { + return String.format("%5d/%s", index, abbreviate(6).name()); //$NON-NLS-1$ + } + + @SuppressWarnings("boxing") + @Override + public String toString() { + return String.format("LogId[%5d/%s]", index, name()); //$NON-NLS-1$ + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Proposal.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Proposal.java new file mode 100644 index 0000000000..0876eb5dbd --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Proposal.java @@ -0,0 +1,391 @@ +/* + * 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.ketch; + +import static org.eclipse.jgit.internal.ketch.Proposal.State.ABORTED; +import static org.eclipse.jgit.internal.ketch.Proposal.State.EXECUTED; +import static org.eclipse.jgit.internal.ketch.Proposal.State.NEW; +import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED; +import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.internal.storage.reftree.Command; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.PushCertificate; +import org.eclipse.jgit.transport.ReceiveCommand; + +/** + * A proposal to be applied in a Ketch system. + * <p> + * Pushing to a Ketch leader results in the leader making a proposal. The + * proposal includes the list of reference updates. The leader attempts to send + * the proposal to a quorum of replicas by pushing the proposal to a "staging" + * area under the {@code refs/txn/stage/} namespace. If the proposal succeeds + * then the changes are durable and the leader can commit the proposal. + * <p> + * Proposals are executed by {@link KetchLeader#queueProposal(Proposal)}, which + * runs them asynchronously in the background. Proposals are thread-safe futures + * allowing callers to {@link #await()} for results or be notified by callback + * using {@link #addListener(Runnable)}. + */ +public class Proposal { + /** Current state of the proposal. */ + public enum State { + /** Proposal has not yet been given to a {@link KetchLeader}. */ + NEW(false), + + /** + * Proposal was validated and has entered the queue, but a round + * containing this proposal has not started yet. + */ + QUEUED(false), + + /** Round containing the proposal has begun and is in progress. */ + RUNNING(false), + + /** + * Proposal was executed through a round. Individual results from + * {@link Proposal#getCommands()}, {@link Command#getResult()} explain + * the success or failure outcome. + */ + EXECUTED(true), + + /** Proposal was aborted and did not reach consensus. */ + ABORTED(true); + + private final boolean done; + + private State(boolean done) { + this.done = done; + } + + /** @return true if this is a terminal state. */ + public boolean isDone() { + return done; + } + } + + private final List<Command> commands; + private PersonIdent author; + private String message; + private PushCertificate pushCert; + private final List<Runnable> listeners = new CopyOnWriteArrayList<>(); + private final AtomicReference<State> state = new AtomicReference<>(NEW); + + /** + * Create a proposal from a list of Ketch commands. + * + * @param cmds + * prepared list of commands. + */ + public Proposal(List<Command> cmds) { + commands = Collections.unmodifiableList(new ArrayList<>(cmds)); + } + + /** + * Create a proposal from a collection of received commands. + * + * @param rw + * walker to assist in preparing commands. + * @param cmds + * list of pending commands. + * @throws MissingObjectException + * newId of a command is not found locally. + * @throws IOException + * local objects cannot be accessed. + */ + public Proposal(RevWalk rw, Collection<ReceiveCommand> cmds) + throws MissingObjectException, IOException { + commands = asCommandList(rw, cmds); + } + + private static List<Command> asCommandList(RevWalk rw, + Collection<ReceiveCommand> cmds) + throws MissingObjectException, IOException { + List<Command> commands = new ArrayList<>(cmds.size()); + for (ReceiveCommand cmd : cmds) { + commands.add(new Command(rw, cmd)); + } + return Collections.unmodifiableList(commands); + } + + /** @return commands from this proposal. */ + public Collection<Command> getCommands() { + return commands; + } + + /** @return optional author of the proposal. */ + @Nullable + public PersonIdent getAuthor() { + return author; + } + + /** + * Set the author for the proposal. + * + * @param who + * optional identity of the author of the proposal. + * @return {@code this} + */ + public Proposal setAuthor(@Nullable PersonIdent who) { + author = who; + return this; + } + + /** @return optional message for the commit log of the RefTree. */ + @Nullable + public String getMessage() { + return message; + } + + /** + * Set the message to appear in the commit log of the RefTree. + * + * @param msg + * message text for the commit. + * @return {@code this} + */ + public Proposal setMessage(@Nullable String msg) { + message = msg != null && !msg.isEmpty() ? msg : null; + return this; + } + + /** @return optional certificate signing the references. */ + @Nullable + public PushCertificate getPushCertificate() { + return pushCert; + } + + /** + * Set the push certificate signing the references. + * + * @param cert + * certificate, may be null. + * @return {@code this} + */ + public Proposal setPushCertificate(@Nullable PushCertificate cert) { + pushCert = cert; + return this; + } + + /** + * Add a callback to be invoked when the proposal is done. + * <p> + * A proposal is done when it has entered either {@link State#EXECUTED} or + * {@link State#ABORTED} state. If the proposal is already done + * {@code callback.run()} is immediately invoked on the caller's thread. + * + * @param callback + * method to run after the proposal is done. The callback may be + * run on a Ketch system thread and should be completed quickly. + */ + public void addListener(Runnable callback) { + boolean runNow = false; + synchronized (state) { + if (state.get().isDone()) { + runNow = true; + } else { + listeners.add(callback); + } + } + if (runNow) { + callback.run(); + } + } + + /** Set command result as OK. */ + void success() { + for (Command c : commands) { + if (c.getResult() == NOT_ATTEMPTED) { + c.setResult(OK); + } + } + notifyState(EXECUTED); + } + + /** Mark commands as "transaction aborted". */ + void abort() { + Command.abort(commands, null); + notifyState(ABORTED); + } + + /** @return read the current state of the proposal. */ + public State getState() { + return state.get(); + } + + /** + * @return {@code true} if the proposal was attempted. A true value does not + * mean consensus was reached, only that the proposal was considered + * and will not be making any more progress beyond its current + * state. + */ + public boolean isDone() { + return state.get().isDone(); + } + + /** + * Wait for the proposal to be attempted and {@link #isDone()} to be true. + * + * @throws InterruptedException + * caller was interrupted before proposal executed. + */ + public void await() throws InterruptedException { + synchronized (state) { + while (!state.get().isDone()) { + state.wait(); + } + } + } + + /** + * Wait for the proposal to be attempted and {@link #isDone()} to be true. + * + * @param wait + * how long to wait. + * @param unit + * unit describing the wait time. + * @return true if the proposal is done; false if the method timed out. + * @throws InterruptedException + * caller was interrupted before proposal executed. + */ + public boolean await(long wait, TimeUnit unit) throws InterruptedException { + synchronized (state) { + if (state.get().isDone()) { + return true; + } + state.wait(unit.toMillis(wait)); + return state.get().isDone(); + } + } + + /** + * Wait for the proposal to exit a state. + * + * @param notIn + * state the proposal should not be in to return. + * @param wait + * how long to wait. + * @param unit + * unit describing the wait time. + * @return true if the proposal exited the state; false on time out. + * @throws InterruptedException + * caller was interrupted before proposal executed. + */ + public boolean awaitStateChange(State notIn, long wait, TimeUnit unit) + throws InterruptedException { + synchronized (state) { + if (state.get() != notIn) { + return true; + } + state.wait(unit.toMillis(wait)); + return state.get() != notIn; + } + } + + void notifyState(State s) { + synchronized (state) { + state.set(s); + state.notifyAll(); + } + if (s.isDone()) { + for (Runnable callback : listeners) { + callback.run(); + } + listeners.clear(); + } + } + + @Override + public String toString() { + StringBuilder s = new StringBuilder(); + s.append("Ketch Proposal {\n"); //$NON-NLS-1$ + s.append(" ").append(state.get()).append('\n'); //$NON-NLS-1$ + if (author != null) { + s.append(" author ").append(author).append('\n'); //$NON-NLS-1$ + } + if (message != null) { + s.append(" message ").append(message).append('\n'); //$NON-NLS-1$ + } + for (Command c : commands) { + s.append(" "); //$NON-NLS-1$ + format(s, c.getOldRef(), "CREATE"); //$NON-NLS-1$ + s.append(' '); + format(s, c.getNewRef(), "DELETE"); //$NON-NLS-1$ + s.append(' ').append(c.getRefName()); + if (c.getResult() != ReceiveCommand.Result.NOT_ATTEMPTED) { + s.append(' ').append(c.getResult()); // $NON-NLS-1$ + } + s.append('\n'); + } + s.append('}'); + return s.toString(); + } + + private static void format(StringBuilder s, @Nullable Ref r, String n) { + if (r == null) { + s.append(n); + } else if (r.isSymbolic()) { + s.append(r.getTarget().getName()); + } else { + ObjectId id = r.getObjectId(); + if (id != null) { + s.append(id.abbreviate(8).name()); + } + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ProposalRound.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ProposalRound.java new file mode 100644 index 0000000000..d34477ab26 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ProposalRound.java @@ -0,0 +1,299 @@ +/* + * 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.ketch; + +import static org.eclipse.jgit.internal.ketch.Proposal.State.RUNNING; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.internal.storage.reftree.Command; +import org.eclipse.jgit.internal.storage.reftree.RefTree; +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.PersonIdent; +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; + +/** A {@link Round} that aggregates and sends user {@link Proposal}s. */ +class ProposalRound extends Round { + private final List<Proposal> todo; + private RefTree queuedTree; + + ProposalRound(KetchLeader leader, LogIndex head, List<Proposal> todo, + @Nullable RefTree tree) { + super(leader, head); + this.todo = todo; + + if (tree != null && canCombine(todo)) { + this.queuedTree = tree; + } else { + leader.roundHoldsReferenceToRefTree = false; + } + } + + private static boolean canCombine(List<Proposal> todo) { + Proposal first = todo.get(0); + for (int i = 1; i < todo.size(); i++) { + if (!canCombine(first, todo.get(i))) { + return false; + } + } + return true; + } + + private static boolean canCombine(Proposal a, Proposal b) { + String aMsg = nullToEmpty(a.getMessage()); + String bMsg = nullToEmpty(b.getMessage()); + return aMsg.equals(bMsg) && canCombine(a.getAuthor(), b.getAuthor()); + } + + private static String nullToEmpty(@Nullable String str) { + return str != null ? str : ""; //$NON-NLS-1$ + } + + private static boolean canCombine(@Nullable PersonIdent a, + @Nullable PersonIdent b) { + if (a != null && b != null) { + // Same name and email address. Combine timestamp as the two + // proposals are running concurrently and appear together or + // not at all from the point of view of an outside reader. + return a.getName().equals(b.getName()) + && a.getEmailAddress().equals(b.getEmailAddress()); + } + + // If a and b are null, both will be the system identity. + return a == null && b == null; + } + + void start() throws IOException { + for (Proposal p : todo) { + p.notifyState(RUNNING); + } + try { + ObjectId id; + try (Repository git = leader.openRepository()) { + id = insertProposals(git); + } + runAsync(id); + } catch (NoOp e) { + for (Proposal p : todo) { + p.success(); + } + leader.lock.lock(); + try { + leader.nextRound(); + } finally { + leader.lock.unlock(); + } + } catch (IOException e) { + abort(); + throw e; + } + } + + private ObjectId insertProposals(Repository git) + throws IOException, NoOp { + ObjectId id; + try (ObjectInserter inserter = git.newObjectInserter()) { + // TODO(sop) Process signed push certificates. + + if (queuedTree != null) { + id = insertSingleProposal(git, inserter); + } else { + id = insertMultiProposal(git, inserter); + } + + stageCommands = makeStageList(git, inserter); + inserter.flush(); + } + return id; + } + + private ObjectId insertSingleProposal(Repository git, + ObjectInserter inserter) throws IOException, NoOp { + // Fast path: tree is passed in with all proposals applied. + ObjectId treeId = queuedTree.writeTree(inserter); + queuedTree = null; + leader.roundHoldsReferenceToRefTree = false; + + if (!ObjectId.zeroId().equals(acceptedOldIndex)) { + try (RevWalk rw = new RevWalk(git)) { + RevCommit c = rw.parseCommit(acceptedOldIndex); + if (treeId.equals(c.getTree())) { + throw new NoOp(); + } + } + } + + Proposal p = todo.get(0); + CommitBuilder b = new CommitBuilder(); + b.setTreeId(treeId); + if (!ObjectId.zeroId().equals(acceptedOldIndex)) { + b.setParentId(acceptedOldIndex); + } + b.setCommitter(leader.getSystem().newCommitter()); + b.setAuthor(p.getAuthor() != null ? p.getAuthor() : b.getCommitter()); + b.setMessage(message(p)); + return inserter.insert(b); + } + + private ObjectId insertMultiProposal(Repository git, + ObjectInserter inserter) throws IOException, NoOp { + // The tree was not passed in, or there are multiple proposals + // each needing their own commit. Reset the tree and replay each + // proposal in order as individual commits. + ObjectId lastIndex = acceptedOldIndex; + ObjectId oldTreeId; + RefTree tree; + if (ObjectId.zeroId().equals(lastIndex)) { + oldTreeId = ObjectId.zeroId(); + tree = RefTree.newEmptyTree(); + } else { + try (RevWalk rw = new RevWalk(git)) { + RevCommit c = rw.parseCommit(lastIndex); + oldTreeId = c.getTree(); + tree = RefTree.read(rw.getObjectReader(), c.getTree()); + } + } + + PersonIdent committer = leader.getSystem().newCommitter(); + for (Proposal p : todo) { + if (!tree.apply(p.getCommands())) { + // This should not occur, previously during queuing the + // commands were successfully applied to the pending tree. + // Abort the entire round. + throw new IOException( + KetchText.get().queuedProposalFailedToApply); + } + + ObjectId treeId = tree.writeTree(inserter); + if (treeId.equals(oldTreeId)) { + continue; + } + + CommitBuilder b = new CommitBuilder(); + b.setTreeId(treeId); + if (!ObjectId.zeroId().equals(lastIndex)) { + b.setParentId(lastIndex); + } + b.setAuthor(p.getAuthor() != null ? p.getAuthor() : committer); + b.setCommitter(committer); + b.setMessage(message(p)); + lastIndex = inserter.insert(b); + } + if (lastIndex.equals(acceptedOldIndex)) { + throw new NoOp(); + } + return lastIndex; + } + + private String message(Proposal p) { + StringBuilder m = new StringBuilder(); + String msg = p.getMessage(); + if (msg != null && !msg.isEmpty()) { + m.append(msg); + while (m.length() < 2 || m.charAt(m.length() - 2) != '\n' + || m.charAt(m.length() - 1) != '\n') { + m.append('\n'); + } + } + m.append(KetchConstants.TERM.getName()) + .append(": ") //$NON-NLS-1$ + .append(leader.getTerm()); + return m.toString(); + } + + void abort() { + for (Proposal p : todo) { + p.abort(); + } + } + + void success() { + for (Proposal p : todo) { + p.success(); + } + } + + private List<ReceiveCommand> makeStageList(Repository git, + ObjectInserter inserter) throws IOException { + // For each branch, collapse consecutive updates to only most recent, + // avoiding sending multiple objects in a rapid fast-forward chain, or + // rewritten content. + Map<String, ObjectId> byRef = new HashMap<>(); + for (Proposal p : todo) { + for (Command c : p.getCommands()) { + Ref n = c.getNewRef(); + if (n != null && !n.isSymbolic()) { + byRef.put(n.getName(), n.getObjectId()); + } + } + } + if (byRef.isEmpty()) { + return Collections.emptyList(); + } + + Set<ObjectId> newObjs = new HashSet<>(byRef.values()); + StageBuilder b = new StageBuilder( + leader.getSystem().getTxnStage(), + acceptedNewIndex); + return b.makeStageList(newObjs, git, inserter); + } + + + private static class NoOp extends Exception { + private static final long serialVersionUID = 1L; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/RemoteGitReplica.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/RemoteGitReplica.java new file mode 100644 index 0000000000..6f4a178673 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/RemoteGitReplica.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.ketch; + +import static org.eclipse.jgit.internal.ketch.KetchReplica.CommitMethod.ALL_REFS; +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.OK; +import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_NODELETE; +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.lib.Ref.Storage.NETWORK; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.errors.NotSupportedException; +import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.NullProgressMonitor; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdRef; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.FetchConnection; +import org.eclipse.jgit.transport.PushConnection; +import org.eclipse.jgit.transport.ReceiveCommand; +import org.eclipse.jgit.transport.RemoteConfig; +import org.eclipse.jgit.transport.RemoteRefUpdate; +import org.eclipse.jgit.transport.Transport; +import org.eclipse.jgit.transport.URIish; + +/** + * Representation of a Git repository on a remote replica system. + * <p> + * {@link KetchLeader} will contact the replica using the Git wire protocol. + * <p> + * The remote replica may be fully Ketch-aware, or a standard Git server. + */ +public class RemoteGitReplica extends KetchReplica { + private final URIish uri; + private final RemoteConfig remoteConfig; + + /** + * Configure a new remote. + * + * @param leader + * instance this replica follows. + * @param name + * unique-ish name identifying this remote for debugging. + * @param uri + * URI to connect to the follower's repository. + * @param cfg + * how Ketch should treat the remote system. + * @param rc + * optional remote configuration describing how to contact the + * peer repository. + */ + public RemoteGitReplica(KetchLeader leader, String name, URIish uri, + ReplicaConfig cfg, @Nullable RemoteConfig rc) { + super(leader, name, cfg); + this.uri = uri; + this.remoteConfig = rc; + } + + /** @return URI to contact the remote peer repository. */ + public URIish getURI() { + return uri; + } + + /** @return optional configuration describing how to contact the peer. */ + @Nullable + protected RemoteConfig getRemoteConfig() { + return remoteConfig; + } + + @Override + protected String describeForLog() { + return String.format("%s @ %s", getName(), getURI()); //$NON-NLS-1$ + } + + @Override + protected void startPush(final ReplicaPushRequest req) { + getSystem().getExecutor().execute(new Runnable() { + @Override + public void run() { + try (Repository git = getLeader().openRepository()) { + try { + push(git, req); + req.done(git); + } catch (Throwable err) { + req.setException(git, err); + } + } catch (IOException err) { + req.setException(null, err); + } + } + }); + } + + private void push(Repository repo, ReplicaPushRequest req) + throws NotSupportedException, TransportException, IOException { + Map<String, Ref> adv; + List<RemoteCommand> cmds = asUpdateList(req.getCommands()); + try (Transport transport = Transport.open(repo, uri)) { + RemoteConfig rc = getRemoteConfig(); + if (rc != null) { + transport.applyConfig(rc); + } + transport.setPushAtomic(true); + adv = push(repo, transport, cmds); + } + for (RemoteCommand c : cmds) { + c.copyStatusToResult(); + } + req.setRefs(adv); + } + + private Map<String, Ref> push(Repository git, Transport transport, + List<RemoteCommand> cmds) throws IOException { + Map<String, RemoteRefUpdate> updates = asUpdateMap(cmds); + try (PushConnection connection = transport.openPush()) { + Map<String, Ref> adv = connection.getRefsMap(); + RemoteRefUpdate accepted = updates.get(getSystem().getTxnAccepted()); + if (accepted != null && !isExpectedValue(adv, accepted)) { + abort(cmds); + return adv; + } + + RemoteRefUpdate committed = updates.get(getSystem().getTxnCommitted()); + if (committed != null && !isExpectedValue(adv, committed)) { + abort(cmds); + return adv; + } + if (committed != null && getCommitMethod() == ALL_REFS) { + prepareCommit(git, cmds, updates, adv, + committed.getNewObjectId()); + } + + connection.push(NullProgressMonitor.INSTANCE, updates); + return adv; + } + } + + private static boolean isExpectedValue(Map<String, Ref> adv, + RemoteRefUpdate u) { + Ref r = adv.get(u.getRemoteName()); + if (!AnyObjectId.equals(getId(r), u.getExpectedOldObjectId())) { + ((RemoteCommand) u).cmd.setResult(LOCK_FAILURE); + return false; + } + return true; + } + + private void prepareCommit(Repository git, List<RemoteCommand> cmds, + Map<String, RemoteRefUpdate> updates, Map<String, Ref> adv, + ObjectId committed) throws IOException { + for (ReceiveCommand cmd : prepareCommit(git, adv, committed)) { + RemoteCommand c = new RemoteCommand(cmd); + cmds.add(c); + updates.put(c.getRemoteName(), c); + } + } + + private static List<RemoteCommand> asUpdateList( + Collection<ReceiveCommand> cmds) { + try { + List<RemoteCommand> toPush = new ArrayList<>(cmds.size()); + for (ReceiveCommand cmd : cmds) { + toPush.add(new RemoteCommand(cmd)); + } + return toPush; + } catch (IOException e) { + // Cannot occur as no IO was required to build the command. + throw new IllegalStateException(e); + } + } + + private static Map<String, RemoteRefUpdate> asUpdateMap( + List<RemoteCommand> cmds) { + Map<String, RemoteRefUpdate> m = new LinkedHashMap<>(); + for (RemoteCommand cmd : cmds) { + m.put(cmd.getRemoteName(), cmd); + } + return m; + } + + private static void abort(List<RemoteCommand> cmds) { + List<ReceiveCommand> tmp = new ArrayList<>(cmds.size()); + for (RemoteCommand cmd : cmds) { + tmp.add(cmd.cmd); + } + ReceiveCommand.abort(tmp); + } + + protected void blockingFetch(Repository repo, ReplicaFetchRequest req) + throws NotSupportedException, TransportException { + try (Transport transport = Transport.open(repo, uri)) { + RemoteConfig rc = getRemoteConfig(); + if (rc != null) { + transport.applyConfig(rc); + } + fetch(transport, req); + } + } + + private void fetch(Transport transport, ReplicaFetchRequest req) + throws NotSupportedException, TransportException { + try (FetchConnection conn = transport.openFetch()) { + Map<String, Ref> remoteRefs = conn.getRefsMap(); + req.setRefs(remoteRefs); + + List<Ref> want = new ArrayList<>(); + for (String name : req.getWantRefs()) { + Ref ref = remoteRefs.get(name); + if (ref != null && ref.getObjectId() != null) { + want.add(ref); + } + } + for (ObjectId id : req.getWantObjects()) { + want.add(new ObjectIdRef.Unpeeled(NETWORK, id.name(), id)); + } + + conn.fetch(NullProgressMonitor.INSTANCE, want, + Collections.<ObjectId> emptySet()); + } + } + + static class RemoteCommand extends RemoteRefUpdate { + final ReceiveCommand cmd; + + RemoteCommand(ReceiveCommand cmd) throws IOException { + super(null, null, + cmd.getNewId(), cmd.getRefName(), + true /* force update */, + null /* no local tracking ref */, + cmd.getOldId()); + this.cmd = cmd; + } + + void copyStatusToResult() { + if (cmd.getResult() == NOT_ATTEMPTED) { + switch (getStatus()) { + case OK: + case UP_TO_DATE: + case NON_EXISTING: + cmd.setResult(OK); + break; + + case REJECTED_NODELETE: + cmd.setResult(REJECTED_NODELETE); + break; + + case REJECTED_NONFASTFORWARD: + cmd.setResult(REJECTED_NONFASTFORWARD); + break; + + case REJECTED_OTHER_REASON: + cmd.setResult(REJECTED_OTHER_REASON, getMessage()); + break; + + default: + cmd.setResult(REJECTED_OTHER_REASON, getStatus().name()); + break; + } + } + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaConfig.java new file mode 100644 index 0000000000..e16e63aa7e --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaConfig.java @@ -0,0 +1,233 @@ +/* + * 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.ketch; + +import static java.util.concurrent.TimeUnit.DAYS; +import static java.util.concurrent.TimeUnit.HOURS; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.MINUTES; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.eclipse.jgit.internal.ketch.KetchConstants.CONFIG_KEY_COMMIT; +import static org.eclipse.jgit.internal.ketch.KetchConstants.CONFIG_KEY_SPEED; +import static org.eclipse.jgit.internal.ketch.KetchConstants.CONFIG_KEY_TYPE; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_REMOTE; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jgit.internal.ketch.KetchReplica.CommitMethod; +import org.eclipse.jgit.internal.ketch.KetchReplica.CommitSpeed; +import org.eclipse.jgit.internal.ketch.KetchReplica.Participation; +import org.eclipse.jgit.lib.Config; + +/** Configures a {@link KetchReplica}. */ +public class ReplicaConfig { + /** + * Read a configuration from a config block. + * + * @param cfg + * configuration to read. + * @param name + * of the replica being configured. + * @return replica configuration for {@code name}. + */ + public static ReplicaConfig newFromConfig(Config cfg, String name) { + return new ReplicaConfig().fromConfig(cfg, name); + } + + private Participation participation = Participation.FULL; + private CommitMethod commitMethod = CommitMethod.ALL_REFS; + private CommitSpeed commitSpeed = CommitSpeed.BATCHED; + private long minRetry = SECONDS.toMillis(5); + private long maxRetry = MINUTES.toMillis(1); + + /** @return participation of the replica in the system. */ + public Participation getParticipation() { + return participation; + } + + /** @return how Ketch should apply committed changes. */ + public CommitMethod getCommitMethod() { + return commitMethod; + } + + /** @return how quickly should Ketch commit. */ + public CommitSpeed getCommitSpeed() { + return commitSpeed; + } + + /** + * Returns the minimum wait delay before retrying a failure. + * + * @param unit + * to get retry delay in. + * @return minimum delay before retrying a failure. + */ + public long getMinRetry(TimeUnit unit) { + return unit.convert(minRetry, MILLISECONDS); + } + + /** + * Returns the maximum wait delay before retrying a failure. + * + * @param unit + * to get retry delay in. + * @return maximum delay before retrying a failure. + */ + public long getMaxRetry(TimeUnit unit) { + return unit.convert(maxRetry, MILLISECONDS); + } + + /** + * Update the configuration from a config block. + * + * @param cfg + * configuration to read. + * @param name + * of the replica being configured. + * @return {@code this} + */ + public ReplicaConfig fromConfig(Config cfg, String name) { + participation = cfg.getEnum( + CONFIG_KEY_REMOTE, name, CONFIG_KEY_TYPE, + participation); + commitMethod = cfg.getEnum( + CONFIG_KEY_REMOTE, name, CONFIG_KEY_COMMIT, + commitMethod); + commitSpeed = cfg.getEnum( + CONFIG_KEY_REMOTE, name, CONFIG_KEY_SPEED, + commitSpeed); + minRetry = getMillis(cfg, name, "ketch-minRetry", minRetry); //$NON-NLS-1$ + maxRetry = getMillis(cfg, name, "ketch-maxRetry", maxRetry); //$NON-NLS-1$ + return this; + } + + private static long getMillis(Config cfg, String name, String key, + long defaultValue) { + String valStr = cfg.getString(CONFIG_KEY_REMOTE, name, key); + if (valStr == null) { + return defaultValue; + } + + valStr = valStr.trim(); + if (valStr.isEmpty()) { + return defaultValue; + } + + Matcher m = UnitMap.PATTERN.matcher(valStr); + if (!m.matches()) { + return defaultValue; + } + + String digits = m.group(1); + String unitName = m.group(2).trim(); + TimeUnit unit = UnitMap.UNITS.get(unitName); + if (unit == null) { + return defaultValue; + } + + try { + if (digits.indexOf('.') == -1) { + return unit.toMillis(Long.parseLong(digits)); + } + + double val = Double.parseDouble(digits); + return (long) (val * unit.toMillis(1)); + } catch (NumberFormatException nfe) { + return defaultValue; + } + } + + static class UnitMap { + static final Pattern PATTERN = Pattern + .compile("^([1-9][0-9]*(?:\\.[0-9]*)?)\\s*(.*)$"); //$NON-NLS-1$ + + static final Map<String, TimeUnit> UNITS; + + static { + Map<String, TimeUnit> m = new HashMap<>(); + TimeUnit u = MILLISECONDS; + m.put("", u); //$NON-NLS-1$ + m.put("ms", u); //$NON-NLS-1$ + m.put("millis", u); //$NON-NLS-1$ + m.put("millisecond", u); //$NON-NLS-1$ + m.put("milliseconds", u); //$NON-NLS-1$ + + u = SECONDS; + m.put("s", u); //$NON-NLS-1$ + m.put("sec", u); //$NON-NLS-1$ + m.put("secs", u); //$NON-NLS-1$ + m.put("second", u); //$NON-NLS-1$ + m.put("seconds", u); //$NON-NLS-1$ + + u = MINUTES; + m.put("m", u); //$NON-NLS-1$ + m.put("min", u); //$NON-NLS-1$ + m.put("mins", u); //$NON-NLS-1$ + m.put("minute", u); //$NON-NLS-1$ + m.put("minutes", u); //$NON-NLS-1$ + + u = HOURS; + m.put("h", u); //$NON-NLS-1$ + m.put("hr", u); //$NON-NLS-1$ + m.put("hrs", u); //$NON-NLS-1$ + m.put("hour", u); //$NON-NLS-1$ + m.put("hours", u); //$NON-NLS-1$ + + u = DAYS; + m.put("d", u); //$NON-NLS-1$ + m.put("day", u); //$NON-NLS-1$ + m.put("days", u); //$NON-NLS-1$ + + UNITS = Collections.unmodifiableMap(m); + } + + private UnitMap() { + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaFetchRequest.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaFetchRequest.java new file mode 100644 index 0000000000..201d9e9743 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaFetchRequest.java @@ -0,0 +1,96 @@ +/* + * 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.ketch; + +import java.util.Map; +import java.util.Set; + +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; + +/** A fetch request to obtain objects from a replica, and its result. */ +public class ReplicaFetchRequest { + private final Set<String> wantRefs; + private final Set<ObjectId> wantObjects; + private Map<String, Ref> refs; + + /** + * Construct a new fetch request for a replica. + * + * @param wantRefs + * named references to be fetched. + * @param wantObjects + * specific objects to be fetched. + */ + public ReplicaFetchRequest(Set<String> wantRefs, + Set<ObjectId> wantObjects) { + this.wantRefs = wantRefs; + this.wantObjects = wantObjects; + } + + /** @return references to be fetched. */ + public Set<String> getWantRefs() { + return wantRefs; + } + + /** @return objects to be fetched. */ + public Set<ObjectId> getWantObjects() { + return wantObjects; + } + + /** @return remote references, usually from the advertisement. */ + @Nullable + public Map<String, Ref> getRefs() { + return refs; + } + + /** + * @param refs + * references observed from the replica. + */ + public void setRefs(Map<String, Ref> refs) { + this.refs = refs; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaPushRequest.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaPushRequest.java new file mode 100644 index 0000000000..691b1424f4 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaPushRequest.java @@ -0,0 +1,168 @@ +/* + * 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.ketch; + +import java.util.Collection; +import java.util.Map; + +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.ReceiveCommand; + +/** + * A push request sending objects to a replica, and its result. + * <p> + * Implementors of {@link KetchReplica} must populate the command result fields, + * {@link #setRefs(Map)}, and call one of + * {@link #setException(Repository, Throwable)} or {@link #done(Repository)} to + * finish processing. + */ +public class ReplicaPushRequest { + private final KetchReplica replica; + private final Collection<ReceiveCommand> commands; + private Map<String, Ref> refs; + private Throwable exception; + private boolean notified; + + /** + * Construct a new push request for a replica. + * + * @param replica + * the replica being pushed to. + * @param commands + * commands to be executed. + */ + public ReplicaPushRequest(KetchReplica replica, + Collection<ReceiveCommand> commands) { + this.replica = replica; + this.commands = commands; + } + + /** @return commands to be executed, and their results. */ + public Collection<ReceiveCommand> getCommands() { + return commands; + } + + /** @return remote references, usually from the advertisement. */ + @Nullable + public Map<String, Ref> getRefs() { + return refs; + } + + /** + * @param refs + * references observed from the replica. + */ + public void setRefs(Map<String, Ref> refs) { + this.refs = refs; + } + + /** @return exception thrown, if any. */ + @Nullable + public Throwable getException() { + return exception; + } + + /** + * Mark the request as crashing with a communication error. + * <p> + * This method may take significant time acquiring the leader lock and + * updating the Ketch state machine with the failure. + * + * @param repo + * local repository reference used by the push attempt. + * @param err + * exception thrown during communication. + */ + public void setException(@Nullable Repository repo, Throwable err) { + if (KetchReplica.log.isErrorEnabled()) { + KetchReplica.log.error(describe("failed"), err); //$NON-NLS-1$ + } + if (!notified) { + notified = true; + exception = err; + replica.afterPush(repo, this); + } + } + + /** + * Mark the request as completed without exception. + * <p> + * This method may take significant time acquiring the leader lock and + * updating the Ketch state machine with results from this replica. + * + * @param repo + * local repository reference used by the push attempt. + */ + public void done(Repository repo) { + if (KetchReplica.log.isDebugEnabled()) { + KetchReplica.log.debug(describe("completed")); //$NON-NLS-1$ + } + if (!notified) { + notified = true; + replica.afterPush(repo, this); + } + } + + private String describe(String heading) { + StringBuilder b = new StringBuilder(); + b.append("push to "); //$NON-NLS-1$ + b.append(replica.describeForLog()); + b.append(' ').append(heading).append(":\n"); //$NON-NLS-1$ + for (ReceiveCommand cmd : commands) { + b.append(String.format( + " %-12s %-12s %s %s", //$NON-NLS-1$ + LeaderSnapshot.str(cmd.getOldId()), + LeaderSnapshot.str(cmd.getNewId()), + cmd.getRefName(), + cmd.getResult())); + if (cmd.getMessage() != null) { + b.append(' ').append(cmd.getMessage()); + } + b.append('\n'); + } + return b.toString(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaSnapshot.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaSnapshot.java new file mode 100644 index 0000000000..8c3de027d2 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaSnapshot.java @@ -0,0 +1,109 @@ +/* + * 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.ketch; + +import java.util.Date; + +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.lib.ObjectId; + +/** + * A snapshot of a replica. + * + * @see LeaderSnapshot + */ +public class ReplicaSnapshot { + final KetchReplica replica; + ObjectId accepted; + ObjectId committed; + KetchReplica.State state; + String error; + long retryAtMillis; + + ReplicaSnapshot(KetchReplica replica) { + this.replica = replica; + } + + /** @return the replica this snapshot describes the state of. */ + public KetchReplica getReplica() { + return replica; + } + + /** @return current state of the replica. */ + public KetchReplica.State getState() { + return state; + } + + /** @return last known Git commit at {@code refs/txn/accepted}. */ + @Nullable + public ObjectId getAccepted() { + return accepted; + } + + /** @return last known Git commit at {@code refs/txn/committed}. */ + @Nullable + public ObjectId getCommitted() { + return committed; + } + + /** + * @return if {@link #getState()} == {@link KetchReplica.State#OFFLINE} an + * optional human-readable message from the transport system + * explaining the failure. + */ + @Nullable + public String getErrorMessage() { + return error; + } + + /** + * @return time (usually in the future) when the leader will retry + * communication with the offline or lagging replica; null if no + * retry is scheduled or necessary. + */ + @Nullable + public Date getRetryAt() { + return retryAtMillis > 0 ? new Date(retryAtMillis) : null; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Round.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Round.java new file mode 100644 index 0000000000..1335b85cca --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Round.java @@ -0,0 +1,116 @@ +/* + * 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.ketch; + +import java.io.IOException; +import java.util.List; + +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.transport.ReceiveCommand; + +/** + * One round-trip to all replicas proposing a log entry. + * <p> + * In Raft a log entry represents a state transition at a specific index in the + * replicated log. The leader can only append log entries to the log. + * <p> + * In Ketch a log entry is recorded under the {@code refs/txn} namespace. This + * occurs when: + * <ul> + * <li>a replica wants to establish itself as a new leader by proposing a new + * term (see {@link ElectionRound}) + * <li>an established leader wants to gain consensus on new {@link Proposal}s + * (see {@link ProposalRound}) + * </ul> + */ +abstract class Round { + final KetchLeader leader; + final LogIndex acceptedOldIndex; + LogIndex acceptedNewIndex; + List<ReceiveCommand> stageCommands; + + Round(KetchLeader leader, LogIndex head) { + this.leader = leader; + this.acceptedOldIndex = head; + } + + /** + * Creates a commit for {@code refs/txn/accepted} and calls + * {@link #runAsync(AnyObjectId)} to begin execution of the round across + * the system. + * <p> + * If references are being updated (such as in a {@link ProposalRound}) the + * RefTree may be modified. + * <p> + * Invoked without {@link KetchLeader#lock} to build objects. + * + * @throws IOException + * the round cannot build new objects within the leader's + * repository. The leader may be unable to execute. + */ + abstract void start() throws IOException; + + /** + * Asynchronously distribute the round's new value for + * {@code refs/txn/accepted} to all replicas. + * <p> + * Invoked by {@link #start()} after new commits have been created for the + * log. The method passes {@code newId} to {@link KetchLeader} to be + * distributed to all known replicas. + * + * @param newId + * new value for {@code refs/txn/accepted}. + */ + void runAsync(AnyObjectId newId) { + acceptedNewIndex = acceptedOldIndex.nextIndex(newId); + leader.runAsync(this); + } + + /** + * Notify the round it was accepted by a majority of the system. + * <p> + * Invoked by the leader with {@link KetchLeader#lock} held by the caller. + */ + abstract void success(); +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/StageBuilder.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/StageBuilder.java new file mode 100644 index 0000000000..61871a494f --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/StageBuilder.java @@ -0,0 +1,270 @@ +/* + * 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.ketch; + +import static org.eclipse.jgit.lib.FileMode.TYPE_GITLINK; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevObject; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.ReceiveCommand; +import org.eclipse.jgit.treewalk.EmptyTreeIterator; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.filter.TreeFilter; + +/** Constructs a set of commands to stage content during a proposal. */ +public class StageBuilder { + /** + * Acceptable number of references to send in a single stage transaction. + * <p> + * If the number of unique objects exceeds this amount the builder will + * attempt to decrease the reference count by chaining commits.. + */ + private static final int SMALL_BATCH_SIZE = 5; + + /** + * Acceptable number of commits to chain together using parent pointers. + * <p> + * When staging many unique commits the {@link StageBuilder} batches + * together unrelated commits as parents of a temporary commit. After the + * proposal completes the temporary commit is discarded and can be garbage + * collected by all replicas. + */ + private static final int TEMP_PARENT_BATCH_SIZE = 128; + + private static final byte[] PEEL = { ' ', '^' }; + + private final String txnStage; + private final String txnId; + + /** + * Construct a stage builder for a transaction. + * + * @param txnStageNamespace + * namespace for transaction references to build + * {@code "txnStageNamespace/txnId.n"} style names. + * @param txnId + * identifier used to name temporary staging refs. + */ + public StageBuilder(String txnStageNamespace, ObjectId txnId) { + this.txnStage = txnStageNamespace; + this.txnId = txnId.name(); + } + + /** + * Compare two RefTrees and return commands to stage new objects. + * <p> + * This method ignores the lineage between the two RefTrees and does a + * straight diff on the two trees. New objects will be staged. The diff + * strategy is useful to catch-up a lagging replica, without sending every + * intermediate step. This may mean the replica does not have the same + * object set as other replicas if there are rewinds or branch deletes. + * + * @param git + * source repository to read {@code oldTree} and {@code newTree} + * from. + * @param oldTree + * accepted RefTree on the replica ({@code refs/txn/accepted}). + * Use {@link ObjectId#zeroId()} if the remote does not have any + * ref tree, e.g. a new replica catching up. + * @param newTree + * RefTree being sent to the replica. The trees will be compared. + * @return list of commands to create {@code "refs/txn/stage/..."} + * references on replicas anchoring new objects into the repository + * while a transaction gains consensus. + * @throws IOException + * {@code git} cannot be accessed to compare {@code oldTree} and + * {@code newTree} to build the object set. + */ + public List<ReceiveCommand> makeStageList(Repository git, ObjectId oldTree, + ObjectId newTree) throws IOException { + try (RevWalk rw = new RevWalk(git); + TreeWalk tw = new TreeWalk(rw.getObjectReader()); + ObjectInserter ins = git.newObjectInserter()) { + if (AnyObjectId.equals(oldTree, ObjectId.zeroId())) { + tw.addTree(new EmptyTreeIterator()); + } else { + tw.addTree(rw.parseTree(oldTree)); + } + tw.addTree(rw.parseTree(newTree)); + tw.setFilter(TreeFilter.ANY_DIFF); + tw.setRecursive(true); + + Set<ObjectId> newObjs = new HashSet<>(); + while (tw.next()) { + if (tw.getRawMode(1) == TYPE_GITLINK + && !tw.isPathSuffix(PEEL, 2)) { + newObjs.add(tw.getObjectId(1)); + } + } + + List<ReceiveCommand> cmds = makeStageList(newObjs, git, ins); + ins.flush(); + return cmds; + } + } + + /** + * Construct a set of commands to stage objects on a replica. + * + * @param newObjs + * objects to send to a replica. + * @param git + * local repository to read source objects from. Required to + * perform minification of {@code newObjs}. + * @param inserter + * inserter to write temporary commit objects during minification + * if many new branches are created by {@code newObjs}. + * @return list of commands to create {@code "refs/txn/stage/..."} + * references on replicas anchoring {@code newObjs} into the + * repository while a transaction gains consensus. + * @throws IOException + * {@code git} cannot be accessed to perform minification of + * {@code newObjs}. + */ + public List<ReceiveCommand> makeStageList(Set<ObjectId> newObjs, + @Nullable Repository git, @Nullable ObjectInserter inserter) + throws IOException { + if (git == null || newObjs.size() <= SMALL_BATCH_SIZE) { + // Without a source repository can only construct unique set. + List<ReceiveCommand> cmds = new ArrayList<>(newObjs.size()); + for (ObjectId id : newObjs) { + stage(cmds, id); + } + return cmds; + } + + List<ReceiveCommand> cmds = new ArrayList<>(); + List<RevCommit> commits = new ArrayList<>(); + reduceObjects(cmds, commits, git, newObjs); + + if (inserter == null || commits.size() <= 1 + || (cmds.size() + commits.size()) <= SMALL_BATCH_SIZE) { + // Without an inserter to aggregate commits, or for a small set of + // commits just send one stage ref per commit. + for (RevCommit c : commits) { + stage(cmds, c.copy()); + } + return cmds; + } + + // 'commits' is sorted most recent to least recent commit. + // Group batches of commits and build a chain. + // TODO(sop) Cluster by restricted graphs to support filtering. + ObjectId tip = null; + for (int end = commits.size(); end > 0;) { + int start = Math.max(0, end - TEMP_PARENT_BATCH_SIZE); + List<RevCommit> batch = commits.subList(start, end); + List<ObjectId> parents = new ArrayList<>(1 + batch.size()); + if (tip != null) { + parents.add(tip); + } + parents.addAll(batch); + + CommitBuilder b = new CommitBuilder(); + b.setTreeId(batch.get(0).getTree()); + b.setParentIds(parents); + b.setAuthor(tmpAuthor(batch)); + b.setCommitter(b.getAuthor()); + tip = inserter.insert(b); + end = start; + } + stage(cmds, tip); + return cmds; + } + + private static PersonIdent tmpAuthor(List<RevCommit> commits) { + // Construct a predictable author using most recent commit time. + int t = 0; + for (int i = 0; i < commits.size();) { + t = Math.max(t, commits.get(i).getCommitTime()); + } + String name = "Ketch Stage"; //$NON-NLS-1$ + String email = "tmp@tmp"; //$NON-NLS-1$ + return new PersonIdent(name, email, t * 1000L, 0); + } + + private void reduceObjects(List<ReceiveCommand> cmds, + List<RevCommit> commits, Repository git, + Set<ObjectId> newObjs) throws IOException { + try (RevWalk rw = new RevWalk(git)) { + rw.setRetainBody(false); + + for (ObjectId id : newObjs) { + RevObject obj = rw.parseAny(id); + if (obj instanceof RevCommit) { + rw.markStart((RevCommit) obj); + } else { + stage(cmds, id); + } + } + + for (RevCommit c; (c = rw.next()) != null;) { + commits.add(c); + rw.markUninteresting(c); + } + } + } + + private void stage(List<ReceiveCommand> cmds, ObjectId id) { + int estLen = txnStage.length() + txnId.length() + 5; + StringBuilder n = new StringBuilder(estLen); + n.append(txnStage).append(txnId).append('.'); + n.append(Integer.toHexString(cmds.size())); + cmds.add(new ReceiveCommand(ObjectId.zeroId(), id, n.toString())); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/package-info.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/package-info.java new file mode 100644 index 0000000000..dfe03752ca --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/package-info.java @@ -0,0 +1,4 @@ +/** + * Distributed consensus system built on Git. + */ +package org.eclipse.jgit.internal.ketch; 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 faf27e32bb..33be3b15a8 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<DfsPackDescription> newPackDesc; private final List<PackStatistics> newPackStats; - private final List<PackWriter.ObjectIdSet> newPackObj; + private final List<ObjectIdSet> newPackObj; private DfsReader ctx; @@ -95,14 +94,11 @@ public class DfsGarbageCollector { private long coalesceGarbageLimit = 50 << 20; - private Map<String, Ref> refsBefore; - private List<DfsPackFile> packsBefore; private Set<ObjectId> allHeads; - private Set<ObjectId> nonHeads; - + private Set<ObjectId> txnHeads; private Set<ObjectId> tagTargets; /** @@ -117,7 +113,7 @@ public class DfsGarbageCollector { objdb = repo.getObjectDatabase(); newPackDesc = new ArrayList<DfsPackDescription>(4); newPackStats = new ArrayList<PackStatistics>(4); - newPackObj = new ArrayList<PackWriter.ObjectIdSet>(4); + newPackObj = new ArrayList<ObjectIdSet>(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<Ref> refsBefore = getAllRefs(); packsBefore = packsToRebuild(); if (packsBefore.isEmpty()) return true; allHeads = new HashSet<ObjectId>(); nonHeads = new HashSet<ObjectId>(); + txnHeads = new HashSet<ObjectId>(); tagTargets = new HashSet<ObjectId>(); - 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; @@ -235,6 +235,18 @@ public class DfsGarbageCollector { } } + private Collection<Ref> getAllRefs() throws IOException { + Collection<Ref> refs = refdb.getRefs(RefDatabase.ALL).values(); + List<Ref> addl = refdb.getAdditionalRefs(); + if (!addl.isEmpty()) { + List<Ref> all = new ArrayList<>(refs.size() + addl.size()); + all.addAll(refs); + all.addAll(addl); + return all; + } + return refs; + } + private List<DfsPackFile> packsToRebuild() throws IOException { DfsPackFile[] packs = objdb.getPacks(); List<DfsPackFile> out = new ArrayList<DfsPackFile>(packs.length); @@ -277,18 +289,17 @@ public class DfsGarbageCollector { try (PackWriter pw = newPackWriter()) { pw.setTagTargets(tagTargets); - pw.preparePack(pm, allHeads, Collections.<ObjectId> 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 +307,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 +352,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 +413,10 @@ public class DfsGarbageCollector { } } - final ObjectIdOwnerMap<ObjectIdOwnerMap.Entry> 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 5f491ff2fd..3641560ee9 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 @@ -91,6 +91,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. * <p> * Packs created by compacting multiple packs together aren't nearly as 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 7073763a7a..11aef7feaf 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<DfsPackFile> srcPacks; - private final List<PackWriter.ObjectIdSet> exclude; + private final List<ObjectIdSet> exclude; private final List<DfsPackDescription> newPacks; @@ -113,7 +114,7 @@ public class DfsPackCompactor { repo = repository; autoAddSize = 5 * 1024 * 1024; // 5 MiB srcPacks = new ArrayList<DfsPackFile>(); - exclude = new ArrayList<PackWriter.ObjectIdSet>(4); + exclude = new ArrayList<ObjectIdSet>(4); newPacks = new ArrayList<DfsPackDescription>(1); newStats = new ArrayList<PackStatistics>(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 a1035a1284..e5469f6b83 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 @@ -262,6 +262,11 @@ public abstract class DfsRefDatabase extends RefDatabase { } @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 0d5fd0f859..ef8845084b 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 1c664b4097..5e246b47b4 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<ObjectId, ObjectId> 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<ReceiveCommand> 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 490cbcaa81..62d2d6969f 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 4c40538b6a..49f9335aed 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<String, Ref> lastPackedRefs; + private Collection<Ref> 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<String, Ref> newRefs; + Collection<Ref> newRefs; if (lastPackedRefs == null || lastPackedRefs.isEmpty()) newRefs = getAllRefs(); else { - newRefs = new HashMap<String, Ref>(); - for (Iterator<Map.Entry<String, Ref>> i = getAllRefs().entrySet() - .iterator(); i.hasNext();) { - Entry<String, Ref> newEntry = i.next(); - Ref old = lastPackedRefs.get(newEntry.getKey()); - if (!equals(newEntry.getValue(), old)) - newRefs.put(newEntry.getKey(), newEntry.getValue()); + Map<String, Ref> 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<PackFile> toBeDeleted = repo.getObjectDatabase().getPacks(); long time = System.currentTimeMillis(); - Map<String, Ref> refsBefore = getAllRefs(); + Collection<Ref> refsBefore = getAllRefs(); Set<ObjectId> allHeads = new HashSet<ObjectId>(); Set<ObjectId> nonHeads = new HashSet<ObjectId>(); + Set<ObjectId> txnHeads = new HashSet<ObjectId>(); Set<ObjectId> tagTargets = new HashSet<ObjectId>(); Set<ObjectId> 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<ObjectIdSet> excluded = new LinkedList<ObjectIdSet>(); 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) { @@ -616,17 +629,23 @@ public class GC { } /** - * Returns a map of all refs and additional refs (e.g. FETCH_HEAD, + * Returns a collection of all refs and additional refs (e.g. FETCH_HEAD, * MERGE_HEAD, ...) * - * @return a map where names of refs point to ref objects + * @return a collection of refs pointing to live objects. * @throws IOException */ - private Map<String, Ref> getAllRefs() throws IOException { - Map<String, Ref> ret = repo.getRefDatabase().getRefs(ALL); - for (Ref ref : repo.getRefDatabase().getAdditionalRefs()) - ret.put(ref.getName(), ref); - return ret; + private Collection<Ref> getAllRefs() throws IOException { + RefDatabase refdb = repo.getRefDatabase(); + Collection<Ref> refs = refdb.getRefs(RefDatabase.ALL).values(); + List<Ref> addl = refdb.getAdditionalRefs(); + if (!addl.isEmpty()) { + List<Ref> all = new ArrayList<>(refs.size() + addl.size()); + all.addAll(refs); + all.addAll(addl); + return all; + } + return refs; } /** @@ -681,8 +700,8 @@ public class GC { } } - private PackFile writePack(Set<? extends ObjectId> want, - Set<? extends ObjectId> have, Set<ObjectId> tagTargets, + private PackFile writePack(@NonNull Set<? extends ObjectId> want, + @NonNull Set<? extends ObjectId> have, Set<ObjectId> tagTargets, List<ObjectIdSet> excludeObjects) throws IOException { File tmpPack = null; Map<PackExt, File> tmpExts = new TreeMap<PackExt, File>( @@ -788,39 +807,33 @@ public class GC { break; } tmpPack.setReadOnly(); - boolean delete = true; - try { - FileUtils.rename(tmpPack, realPack); - delete = false; - for (Map.Entry<PackExt, File> 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<PackExt, File> 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 +1011,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 0000000000..1e2617c0e3 --- /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<Entry> 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<Entry> load() { + ObjectIdOwnerMap<Entry> 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 e23ca741b8..ce9677a62d 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 bd1d488d94..ea80528518 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 1c076ee099..2e6c245ea1 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 0040aea713..f36bd4d70c 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. * </p> */ -public abstract class PackIndex implements Iterable<PackIndex.MutableEntry> { +public abstract class PackIndex + implements Iterable<PackIndex.MutableEntry>, ObjectIdSet { /** * Open an existing pack <code>.idx</code> file for reading. * <p> @@ -166,6 +168,11 @@ public abstract class PackIndex implements Iterable<PackIndex.MutableEntry> { 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 69f7e97071..2c8e5f9d11 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 ba4a63d7fe..4b803a5144 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 19b6b080da..525f9aecc7 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<ObjectId> NONE = Collections.emptySet(); private static final Map<WeakReference<PackWriter>, Boolean> instances = new ConcurrentHashMap<WeakReference<PackWriter>, 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<RevObject> objectsSource) + public void preparePack(@NonNull Iterator<RevObject> 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<? extends ObjectId> want, - Set<? extends ObjectId> have) throws IOException { + @NonNull Set<? extends ObjectId> want, + @NonNull Set<? extends ObjectId> 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<? extends ObjectId> interestingObjects, - final Set<? extends ObjectId> uninterestingObjects) + @NonNull ObjectWalk walk, + @NonNull Set<? extends ObjectId> interestingObjects, + @NonNull Set<? extends ObjectId> 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<? extends ObjectId> want, - Set<? extends ObjectId> have) - throws MissingObjectException, IOException, - IncorrectObjectTypeException { + private void findObjectsToPack(@NonNull ProgressMonitor countingMonitor, + @NonNull ObjectWalk walker, @NonNull Set<? extends ObjectId> want, + @NonNull Set<? extends ObjectId> 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<ObjectId>(want)); stats.uninterestingObjects = Collections.unmodifiableSet(new HashSet<ObjectId>(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 0000000000..12ef8734c4 --- /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 0000000000..dd08375f21 --- /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}. + * <p> + * 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. + * <p> + * RefTree Commands may wrap a {@code ReceiveCommand} to allow callers to + * process an existing ReceiveCommand against a RefTree. + * <p> + * 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. + * <p> + * 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<Command> 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. + * <p> + * 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 0000000000..85690c8ca5 --- /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. + * <p> + * 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}. + * <p> + * Normal references are stored as {@link FileMode#GITLINK} tree entries. The + * ObjectId in the tree entry is the ObjectId the reference refers to. + * <p> + * Symbolic references are stored as {@link FileMode#SYMLINK} entries, with the + * blob storing the name of the target reference. + * <p> + * Annotated tags also store the peeled object using a {@code GITLINK} entry + * with the suffix <code>" ^"</code> (space carrot), for example + * {@code "tags/v1.0"} stores the annotated tag object, while + * <code>"tags/v1.0 ^"</code> stores the commit the tag annotates. + * <p> + * {@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<ObjectId, String> pendingBlobs; + + private RefTree(DirCache dc) { + this.contents = dc; + } + + /** + * Read one reference. + * <p> + * 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()}. + * <p> + * 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. + * <p> + * The batch is applied atomically, either all commands apply at once, or + * they all reject and the RefTree is left unmodified. + * <p> + * 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<Command> 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 0000000000..a55a9f51e7 --- /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<Command> 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<Command> 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<Command> 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<Command> 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 0000000000..df93ce88af --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeDatabase.java @@ -0,0 +1,372 @@ +/* + * 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.Ref.Storage.LOOSE; +import static org.eclipse.jgit.lib.Ref.Storage.PACKED; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +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}. + * <p> + * 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 { + String[] needle = new String[SEARCH_PATH.length]; + for (int i = 0; i < SEARCH_PATH.length; i++) { + needle[i] = SEARCH_PATH[i] + name; + } + return firstExactRef(needle); + } + + @Override + public Ref exactRef(String name) throws IOException { + if (!repo.isBare() && name.indexOf('/') < 0 && !HEAD.equals(name)) { + // Pass through names like MERGE_HEAD, ORIG_HEAD, FETCH_HEAD. + return bootstrap.exactRef(name); + } else 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<String, Ref> 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.<Ref> 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<Ref> getAdditionalRefs() throws IOException { + Collection<Ref> txnRefs; + if (txnNamespace != null) { + txnRefs = bootstrap.getRefs(txnNamespace).values(); + } else { + Ref r = bootstrap.exactRef(txnCommitted); + if (r != null && r.getObjectId() != null) { + txnRefs = Collections.singleton(r); + } else { + txnRefs = Collections.emptyList(); + } + } + + List<Ref> otherRefs = bootstrap.getAdditionalRefs(); + List<Ref> all = new ArrayList<>(txnRefs.size() + otherRefs.size()); + all.addAll(txnRefs); + all.addAll(otherRefs); + return all; + } + + @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 (!repo.isBare() && name.indexOf('/') < 0 && !HEAD.equals(name)) { + return bootstrap.newUpdate(name, detach); + } + 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; + } + + if (name.indexOf('/') < 0 && !HEAD.equals(name)) { + return true; + } + + 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 0000000000..c53d6deb21 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeNames.java @@ -0,0 +1,89 @@ +/* + * 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 org.eclipse.jgit.lib.RefDatabase; + +/** Magic reference name logic for RefTrees. */ +public class RefTreeNames { + /** + * Suffix used on a {@link RefTreeDatabase#getTxnNamespace()} for user data. + * <p> + * 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; + } + + 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 0000000000..5fd7ecdd79 --- /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<Command> 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 0000000000..8829c1156a --- /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 0000000000..d383abf316 --- /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<Ref> all; + final RefList<Ref> sym; + + Result(ObjectId id, RefList<Ref> all, RefList<Ref> 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<Ref> all = new RefList.Builder<>(); + RefList.Builder<Ref> 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<Ref> 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<Ref> all, RefList.Builder<Ref> 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<Ref> 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<Ref> 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 45dd7ee1ac..670f9a9e14 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<B extends BaseRepositoryBuilder, R extends Re int pathStart = 8; int lineEnd = RawParseUtils.nextLF(content, pathStart); - if (content[lineEnd - 1] == '\n') + while (content[lineEnd - 1] == '\n' || + (content[lineEnd - 1] == '\r' && SystemReader.getInstance().isWindows())) lineEnd--; if (lineEnd == pathStart) throw new IOException(MessageFormat.format( diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BlobBasedConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BlobBasedConfig.java index cbb2f5b856..7d52991df0 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BlobBasedConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BlobBasedConfig.java @@ -79,7 +79,15 @@ public class BlobBasedConfig extends Config { public BlobBasedConfig(Config base, final byte[] blob) throws ConfigInvalidException { super(base); - fromText(RawParseUtils.decode(blob)); + final String decoded; + if (blob.length >= 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 6811417ee0..0000000000 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileTreeEntry.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> - * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org> - * 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 a7a67a8812..0b5efd77d4 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,31 +136,135 @@ 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<ErrorType> 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<ErrorType> 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. * <p> * Some broken Git libraries generated leading zeros in the mode part of * 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. + * <p> + * 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. * <p> @@ -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 95b16d9176..442261cbd5 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 <V> * type of subclass of ObjectId that will be stored in the map. */ -public class ObjectIdOwnerMap<V extends ObjectIdOwnerMap.Entry> implements - Iterable<V> { +public class ObjectIdOwnerMap<V extends ObjectIdOwnerMap.Entry> + implements Iterable<V>, 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 f481c772dc..c286f5e463 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 index c7e41bce04..0b5848463c 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 <robin.rosenberg@dewire.com> - * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org> + * 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. + * <p> + * 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 48aa109e7c..faed64bfe7 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 <V> * type of subclass of ObjectId that will be stored in the map. */ -public class ObjectIdSubclassMap<V extends ObjectId> implements Iterable<V> { +public class ObjectIdSubclassMap<V extends ObjectId> + implements Iterable<V>, 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 f119c44fe2..a78a90fe58 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. * <p> @@ -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 <code>ref^{}</code> (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 986666f2f4..c0c3862c8b 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 { * <p> * 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 747fa62b50..3a02b22813 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 49a970d03a..f8266133a6 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 43b1510f94..eeab921a7a 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}. * <p> @@ -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 43bd489dc0..0000000000 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Tree.java +++ /dev/null @@ -1,601 +0,0 @@ -/* - * Copyright (C) 2007, Robin Rosenberg <me@lathund.dewire.com> - * Copyright (C) 2007-2008, Robin Rosenberg <robin.rosenberg@dewire.com> - * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> - * 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<s.length ? (byte)'/' : 0; - p = binarySearch(contents, s, xlast, offset, slash); - if (p >= 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. - * - * <p> - * 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<s.length ? (byte)'/' : slast; - p = binarySearch(contents, s, xlast, offset, slash); - if (p >= 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 a1ffa68056..0000000000 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TreeEntry.java +++ /dev/null @@ -1,256 +0,0 @@ -/* - * Copyright (C) 2007-2008, Robin Rosenberg <robin.rosenberg@dewire.com> - * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org> - * 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 191f3d8366..82cbf368c7 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<String> commits = new ArrayList<String>(); List<String> others = new ArrayList<String>(); 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<String> listings = new ArrayList<String>(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/Merger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/Merger.java index 983bf5c91c..bee2d03523 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/Merger.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/Merger.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2008-2013, Google Inc. + * Copyright (C) 2016, Laurent Delaigue <laurent.delaigue@obeo.fr> * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -51,9 +52,11 @@ import org.eclipse.jgit.errors.NoMergeBaseException; import org.eclipse.jgit.errors.NoMergeBaseException.MergeBaseFailureReason; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.AnyObjectId; +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.ProgressMonitor; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevObject; @@ -88,6 +91,13 @@ public abstract class Merger { protected RevTree[] sourceTrees; /** + * A progress monitor. + * + * @since 4.2 + */ + protected ProgressMonitor monitor = NullProgressMonitor.INSTANCE; + + /** * Create a new merge instance for a repository. * * @param local @@ -290,4 +300,20 @@ public abstract class Merger { * @return resulting tree, if {@link #merge(AnyObjectId[])} returned true. */ public abstract ObjectId getResultTreeId(); + + /** + * Set a progress monitor. + * + * @param monitor + * Monitor to use, can be null to indicate no progress reporting + * is desired. + * @since 4.2 + */ + public void setProgressMonitor(ProgressMonitor monitor) { + if (monitor == null) { + this.monitor = NullProgressMonitor.INSTANCE; + } else { + this.monitor = monitor; + } + } } 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 6a2d44bca9..362328a963 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 c23e4e3288..e67ada6022 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; } @@ -488,18 +494,49 @@ public class RevCommit extends RevObject { /** * Determine the encoding of the commit message buffer. * <p> + * 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. + * <p> + * 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. + * <p> * Locates the "encoding" header (if present) and then returns the proper * character set to apply to this buffer to evaluate its contents as * character data. * <p> - * 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. * <p> @@ -529,7 +566,7 @@ public class RevCommit extends RevObject { final int msgB = RawParseUtils.commitMessage(raw, 0); final ArrayList<FooterLine> r = new ArrayList<FooterLine>(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 bf2785e0d7..81a54bf7ea 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. * <p> 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 7f9cec734d..aa36aeb1be 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 cf13582db5..754cf361a9 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 0834c359aa..963de35d41 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 776a9f695a..a20e652553 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<ObjectId>(); clientShallowCommits = new HashSet<ObjectId>(); - 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/BundleFetchConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BundleFetchConnection.java index e53c04b535..8038fa4d31 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BundleFetchConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BundleFetchConnection.java @@ -161,16 +161,23 @@ class BundleFetchConnection extends BaseFetchConnection { } private String readLine(final byte[] hdrbuf) throws IOException { - bin.mark(hdrbuf.length); - final int cnt = bin.read(hdrbuf); - int lf = 0; - while (lf < cnt && hdrbuf[lf] != '\n') - lf++; - bin.reset(); - IO.skipFully(bin, lf); - if (lf < cnt && hdrbuf[lf] == '\n') - IO.skipFully(bin, 1); - return RawParseUtils.decode(Constants.CHARSET, hdrbuf, 0, lf); + StringBuilder line = new StringBuilder(); + boolean done = false; + while (!done) { + bin.mark(hdrbuf.length); + final int cnt = bin.read(hdrbuf); + int lf = 0; + while (lf < cnt && hdrbuf[lf] != '\n') + lf++; + bin.reset(); + IO.skipFully(bin, lf); + if (lf < cnt && hdrbuf[lf] == '\n') { + IO.skipFully(bin, 1); + done = true; + } + line.append(RawParseUtils.decode(Constants.CHARSET, hdrbuf, 0, lf)); + } + return line.toString(); } public boolean didFetchTestConnectivity() { 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 3e0ee2f645..3941d3c552 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 0ff9fcea74..da288ec31e 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 { * <p> * If additional messages were produced by the remote peer, these should * still be retained in the connection instance for {@link #getMessages()}. + * <p> + * {@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 464d0f9ee5..4800f6826f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/CredentialsProvider.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/CredentialsProvider.java @@ -81,6 +81,20 @@ public abstract class CredentialsProvider { } /** + * @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. * * An interactive provider may try to open a dialog box, or prompt for input diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Daemon.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Daemon.java index d9e0b937e8..2593ba556d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Daemon.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Daemon.java @@ -256,6 +256,16 @@ public class Daemon { } /** + * Get the factory used to construct per-request ReceivePack. + * + * @return the factory. + * @since 4.2 + */ + public ReceivePackFactory<DaemonClient> getReceivePackFactory() { + return receivePackFactory; + } + + /** * Set the factory to construct and configure per-request ReceivePack. * * @param factory 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 9aae1c37aa..c4b3f83048 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<String, Ref> 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 85109a5bf0..1dfe5d9797 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 74909998ce..4037545e9d 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 6e5fc9f009..b96fe885e1 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 0000000000..ac048a14a9 --- /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}. + * <p> + * 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 4fd192dbb2..5cea88215a 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<String, RemoteRefUpdate> result = new HashMap<String, RemoteRefUpdate>(); 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 5702b6d7b9..2b21c4a8fe 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<ReceiveCommand>) commands, want); } + /** + * Set unprocessed commands as failed due to transaction aborted. + * <p> + * 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<ReceiveCommand> 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 66ffc3abe9..0e803bdaf7 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 cf388e2718..fe9f2a3155 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 f0c513427a..72c9c8b93e 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<TransferConfig> KEY = new SectionParser<TransferConfig>() { 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<ObjectChecker.ErrorType> 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<ObjectChecker.ErrorType> 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<String, ObjectChecker.ErrorType> 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 6af153cbc9..9e6d1f68f5 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. + * <p> + * {@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 2f9dfa1d6b..3ee2feb140 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 1c6b8b7363..17edfdc4fb 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<ObjectId> inWorkQueue = new HashSet<ObjectId>(); 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 5e71889574..58136355eb 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. * <p> @@ -692,6 +678,14 @@ public abstract class AbstractTreeIterator { } /** + * @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 */ public int getNameLength() { 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 8dbf80e6a8..ec4a84eff3 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 350f563964..d2195a874c 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 06dc0bf6d0..5cd713da78 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,13 +661,30 @@ 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. + * <p> + * 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. * <p> * Entering into (or exiting out of) a subtree causes the current tree @@ -861,10 +874,13 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { * Test if the supplied path matches the current entry's path. * <p> * 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 <code>startsWith</code> or some other type of string * match function. + * <p> + * 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 94beeeb56f..0d617ee7f9 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> ENTRY_CMP = new Comparator<Entry>() { - 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 bdfde0bfcd..7601956c43 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 35fc99e54e..e14096e598 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 727ea79cc9..aa101f73f9 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 0000000000..6be7ddbe12 --- /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. + * <p> + * 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. + * <p> + * Returns {@code 0} if the names are identical and a conflict exists + * between {@code aPath} and {@code bPath}, as they share the same name. + * <p> + * 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. + * <p> + * 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 45c339fb48..f2955f7e6b 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<String, Charset> encodingAliases; static { - encodingAliases = new HashMap<String, Charset>(); - 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); @@ -671,35 +675,60 @@ public final class RawParseUtils { } /** + * Parse the "encoding " header as a string. + * <p> + * 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. * <p> * Locates the "encoding " header (if present) by first calling * {@link #encoding(byte[], int)} and then returns the proper character set * to apply to this buffer to evaluate its contents as character data. * <p> - * 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 24b8b53330..8d39a22ac2 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 0000000000..3481fa1f8a --- /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 0000000000..557dff2319 --- /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 |