diff options
295 files changed, 16694 insertions, 2412 deletions
@@ -3,8 +3,33 @@ build --repository_cache=~/.gerritcodereview/bazel-cache/repository build --experimental_strict_action_env build --action_env=PATH build --disk_cache=~/.gerritcodereview/bazel-cache/cas -build --java_toolchain //tools:error_prone_warnings_toolchain + +# Builds using remote_jdk11, executes using remote_jdk11 or local_jdk +build --java_language_version=11 +build --java_runtime_version=remotejdk_11 +build --tool_java_language_version=11 +build --tool_java_runtime_version=remotejdk_11 + +# Builds and executes on RBE using remotejdk_11 +build:remote --java_language_version=11 +build:remote --java_runtime_version=remotejdk_11 +build:remote --tool_java_language_version=11 +build:remote --tool_java_runtime_version=remotejdk_11 + +# Builds using remote_jdk17, executes using remote_jdk11 or local_jdk +build:java17 --java_language_version=17 +build:java17 --java_runtime_version=remotejdk_17 +build:java17 --tool_java_language_version=17 +build:java17 --tool_java_runtime_version=remotejdk_17 + +# Builds and executes on RBE using remotejdk_17 +build:remote17 --java_language_version=17 +build:remote17 --java_runtime_version=remotejdk_17 +build:remote17 --tool_java_language_version=17 +build:remote17 --tool_java_runtime_version=remotejdk_17 test --build_tests_only test --test_output=errors +import %workspace%/tools/remote-bazelrc + diff --git a/.bazelversion b/.bazelversion index fcdb2e109f..0062ac9718 100644 --- a/.bazelversion +++ b/.bazelversion @@ -1 +1 @@ -4.0.0 +5.0.0 @@ -12,6 +12,7 @@ Roberto Tyley <roberto.tyley@guardian.co.uk> roberto <roberto.tyl Saša Živkov <sasa.zivkov@sap.com> Sasa Zivkov <sasa.zivkov@sap.com> Saša Živkov <sasa.zivkov@sap.com> Saša Živkov <zivkov@gmail.com> Saša Živkov <sasa.zivkov@sap.com> Sasa Zivkov <zivkov@gmail.com> +Sebastian Schuberth <sschuberth@gmail.com> Sebastian Schuberth <sebastian.schuberth@bosch.io> Shawn Pearce <spearce@spearce.org> Shawn O. Pearce <sop@google.com> Shawn Pearce <spearce@spearce.org> Shawn Pearce <sop@google.com> Shawn Pearce <spearce@spearce.org> Shawn O. Pearce <spearce@spearce.org> diff --git a/DEPENDENCIES b/DEPENDENCIES index ebbe4805aa..be7ab17cb4 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -1,69 +1,70 @@ maven/mavencentral/args4j/args4j/2.33, MIT, approved, CQ11068 -maven/mavencentral/com.google.code.gson/gson/2.8.7, Apache-2.0, approved, CQ23496 -maven/mavencentral/com.googlecode.javaewah/JavaEWAH/1.1.12, Apache-2.0, approved, CQ11658 +maven/mavencentral/com.google.code.gson/gson/2.8.9, Apache-2.0, approved, CQ23496 +maven/mavencentral/com.googlecode.javaewah/JavaEWAH/1.1.13, Apache-2.0, approved, CQ11658 maven/mavencentral/com.jcraft/jsch/0.1.55, BSD-3-Clause, approved, CQ19435 -maven/mavencentral/com.jcraft/jzlib/1.1.1, BSD-2-Clause, approved, CQ6218 +maven/mavencentral/com.jcraft/jzlib/1.1.3, BSD-2-Clause, approved, CQ6218 maven/mavencentral/commons-codec/commons-codec/1.11, Apache-2.0 AND BSD-3-Clause, approved, CQ15971 maven/mavencentral/commons-logging/commons-logging/1.2, Apache-2.0, approved, CQ10162 -maven/mavencentral/javax.servlet/javax.servlet-api/3.1.0, Apache-2.0 AND (CDDL-1.1 OR GPL-2.0 WITH Classpath-exception-2.0), approved, CQ7248 -maven/mavencentral/junit/junit/4.13, , approved, CQ22796 -maven/mavencentral/log4j/log4j/1.2.15, Apache-2.0, approved, CQ7837 +maven/mavencentral/javax.servlet/javax.servlet-api/4.0.0, (CDDL-1.1 OR GPL-2.0-only WITH Classpath-exception-2.0) AND Apache-2.0, approved, CQ16125 +maven/mavencentral/junit/junit/4.13.2, EPL-2.0, approved, CQ23636 maven/mavencentral/net.bytebuddy/byte-buddy-agent/1.9.0, Apache-2.0, approved, clearlydefined maven/mavencentral/net.bytebuddy/byte-buddy/1.9.0, Apache-2.0, approved, clearlydefined maven/mavencentral/net.i2p.crypto/eddsa/0.3.0, CC0-1.0, approved, CQ22537 +maven/mavencentral/net.java.dev.jna/jna-platform/5.8.0, Apache-2.0 OR LGPL-2.1-or-later, approved, CQ23218 +maven/mavencentral/net.java.dev.jna/jna/5.8.0, Apache-2.0 OR LGPL-2.1-or-later, approved, CQ23217 maven/mavencentral/net.sf.jopt-simple/jopt-simple/4.6, MIT, approved, clearlydefined -maven/mavencentral/org.apache.ant/ant-launcher/1.10.10, Apache-2.0 AND W3C AND LicenseRef-Public-Domain, approved, CQ15560 -maven/mavencentral/org.apache.ant/ant/1.10.10, Apache-2.0 AND W3C AND LicenseRef-Public-Domain, approved, CQ15560 -maven/mavencentral/org.apache.commons/commons-compress/1.20, Apache-2.0 AND BSD-3-Clause AND LicenseRef-Public-Domain, approved, CQ21771 +maven/mavencentral/org.apache.ant/ant-launcher/1.10.12, Apache-2.0 AND W3C AND LicenseRef-Public-Domain, approved, CQ15560 +maven/mavencentral/org.apache.ant/ant/1.10.12, Apache-2.0 AND W3C AND LicenseRef-Public-Domain, approved, CQ15560 +maven/mavencentral/org.apache.commons/commons-compress/1.21, Apache-2.0 AND BSD-3-Clause AND bzip2-1.0.6 AND LicenseRef-Public-Domain, approved, CQ23710 maven/mavencentral/org.apache.commons/commons-math3/3.2, Apache-2.0, approved, clearlydefined maven/mavencentral/org.apache.httpcomponents/httpclient/4.5.13, Apache-2.0 AND LicenseRef-Public-Domain, approved, CQ23527 maven/mavencentral/org.apache.httpcomponents/httpcore/4.4.14, Apache-2.0, approved, CQ23528 -maven/mavencentral/org.apache.sshd/sshd-common/2.7.0, Apache-2.0 and ISC, approved, CQ23469 -maven/mavencentral/org.apache.sshd/sshd-core/2.7.0, Apache-2.0, approved, CQ23469 -maven/mavencentral/org.apache.sshd/sshd-osgi/2.7.0, Apache-2.0 and ISC, approved, CQ23469 -maven/mavencentral/org.apache.sshd/sshd-sftp/2.7.0, Apache-2.0, approved, CQ23470 +maven/mavencentral/org.apache.sshd/sshd-common/2.8.0, Apache-2.0 AND ISC, approved, #2349 +maven/mavencentral/org.apache.sshd/sshd-core/2.8.0, Apache-2.0, approved, #2331 +maven/mavencentral/org.apache.sshd/sshd-osgi/2.8.0, Apache-2.0, approved, CQ23892 +maven/mavencentral/org.apache.sshd/sshd-sftp/2.8.0, Apache-2.0, approved, CQ23893 maven/mavencentral/org.assertj/assertj-core/3.20.2, Apache-2.0, approved, clearlydefined -maven/mavencentral/org.bouncycastle/bcpg-jdk15on/1.69, MIT and Apache-2.0, approved, CQ23472 -maven/mavencentral/org.bouncycastle/bcpkix-jdk15on/1.69, MIT, approved, CQ23473 -maven/mavencentral/org.bouncycastle/bcprov-jdk15on/1.69, MIT, approved, CQ23471 -maven/mavencentral/org.bouncycastle/bcutil-jdk15on/1.69, MIT, approved, CQ23474 -maven/mavencentral/org.eclipse.jetty/jetty-http/9.4.43.v20210629, , approved, eclipse -maven/mavencentral/org.eclipse.jetty/jetty-io/9.4.43.v20210629, , approved, eclipse -maven/mavencentral/org.eclipse.jetty/jetty-security/9.4.43.v20210629, , approved, eclipse -maven/mavencentral/org.eclipse.jetty/jetty-server/9.4.43.v20210629, , approved, eclipse -maven/mavencentral/org.eclipse.jetty/jetty-servlet/9.4.43.v20210629, , approved, eclipse -maven/mavencentral/org.eclipse.jetty/jetty-util-ajax/9.4.43.v20210629, , approved, eclipse -maven/mavencentral/org.eclipse.jetty/jetty-util/9.4.43.v20210629, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ant.test/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ant/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.archive/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.gpg.bc/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.http.apache/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.http.server/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.http.test/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.junit.http/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.junit.ssh/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.junit/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs.server.test/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs.server/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs.test/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.pgm.test/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.pgm/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ssh.apache.test/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ssh.apache/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ssh.jsch/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.test/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ui/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit/5.13.0-SNAPSHOT, , approved, eclipse -maven/mavencentral/org.hamcrest/hamcrest-core/1.3, BSD-2-Clause, approved, CQ7063 -maven/mavencentral/org.hamcrest/hamcrest/2.2, BSD-2-Clause, approved, clearlydefined -maven/mavencentral/org.mockito/mockito-core/2.23.0, MIT, approved, CQ17976 +maven/mavencentral/org.bouncycastle/bcpg-jdk15on/1.70, Apache-2.0, approved, #1713 +maven/mavencentral/org.bouncycastle/bcpkix-jdk15on/1.70, MIT, approved, clearlydefined +maven/mavencentral/org.bouncycastle/bcprov-jdk15on/1.70, MIT, approved, #1712 +maven/mavencentral/org.bouncycastle/bcutil-jdk15on/1.70, MIT, approved, clearlydefined +maven/mavencentral/org.eclipse.jetty.toolchain/jetty-servlet-api/4.0.6, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-http/10.0.6, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-io/10.0.6, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-security/10.0.6, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-server/10.0.6, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-servlet/10.0.6, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jetty/jetty-util/10.0.6, EPL-2.0 OR Apache-2.0, approved, rt.jetty +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ant.test/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ant/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.archive/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.gpg.bc/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.http.apache/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.http.server/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.http.test/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.junit.http/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.junit.ssh/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.junit/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs.server.test/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs.server/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs.test/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.pgm.test/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.pgm/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ssh.apache.agent/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ssh.apache.test/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ssh.apache/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ssh.jsch/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.test/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ui/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit/6.2.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit +maven/mavencentral/org.hamcrest/hamcrest-core/1.3, BSD-2-Clause, approved, CQ11429 +maven/mavencentral/org.mockito/mockito-core/2.23.0, Apache-2.0 AND MIT, approved, #958 maven/mavencentral/org.objenesis/objenesis/2.6, Apache-2.0, approved, CQ15478 -maven/mavencentral/org.openjdk.jmh/jmh-core/1.32, GPL-2.0, approved, CQ23499 -maven/mavencentral/org.openjdk.jmh/jmh-generator-annprocess/1.32, GPL-2.0, approved, CQ23500 -maven/mavencentral/org.osgi/org.osgi.core/4.3.1, Apache-2.0, approved, CQ10111 -maven/mavencentral/org.slf4j/jcl-over-slf4j/1.7.30, Apache-2.0, approved, CQ12843 +maven/mavencentral/org.openjdk.jmh/jmh-core/1.32, GPL-2.0-only with Classpath-exception-2.0, approved, #959 +maven/mavencentral/org.openjdk.jmh/jmh-generator-annprocess/1.32, GPL-2.0-only with Classpath-exception-2.0, approved, #962 +maven/mavencentral/org.osgi/org.osgi.core/6.0.0, Apache-2.0, approved, #1794 +maven/mavencentral/org.slf4j/jcl-over-slf4j/1.7.32, Apache-2.0, approved, CQ12843 maven/mavencentral/org.slf4j/slf4j-api/1.7.30, MIT, approved, CQ13368 -maven/mavencentral/org.slf4j/slf4j-log4j12/1.7.30, MIT, approved, CQ7665 +maven/mavencentral/org.slf4j/slf4j-simple/1.7.30, MIT, approved, CQ7952 maven/mavencentral/org.tukaani/xz/1.9, LicenseRef-Public-Domain, approved, CQ23498 @@ -1,23 +1,6 @@ workspace(name = "jgit") load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") - -http_archive( - name = "bazel_skylib", - sha256 = "2ea8a5ed2b448baf4a6855d3ce049c4c452a6470b1efd1504fdb7c1c134d220a", - strip_prefix = "bazel-skylib-0.8.0", - urls = ["https://github.com/bazelbuild/bazel-skylib/archive/0.8.0.tar.gz"], -) - -# Check Bazel version when invoked by Bazel directly -load("//tools:bazelisk_version.bzl", "bazelisk_version") - -bazelisk_version(name = "bazelisk_version") - -load("@bazelisk_version//:check.bzl", "check_bazel_version") - -check_bazel_version() - load("//tools:bazlets.bzl", "load_bazlets") load_bazlets(commit = "f30a992da9fc855dce819875afb59f9dd6f860cd") @@ -28,32 +11,18 @@ load( ) http_archive( - name = "openjdk15_linux_archive", - build_file_content = """ -java_runtime(name = 'runtime', srcs = glob(['**']), visibility = ['//visibility:public']) -exports_files(["WORKSPACE"], visibility = ["//visibility:public"]) -""", - sha256 = "0a38f1138c15a4f243b75eb82f8ef40855afcc402e3c2a6de97ce8235011b1ad", - strip_prefix = "zulu15.27.17-ca-jdk15.0.0-linux_x64", + name = "rbe_jdk11", + sha256 = "766796de71916118e528b9f4334c29c9c9b4e926227bf3264dee555e6a4306c8", + strip_prefix = "rbe_autoconfig-2.0.0", urls = [ - "https://mirror.bazel.build/cdn.azul.com/zulu/bin/zulu15.27.17-ca-jdk15.0.0-linux_x64.tar.gz", - "https://cdn.azul.com/zulu/bin/zulu15.27.17-ca-jdk15.0.0-linux_x64.tar.gz", + "https://gerrit-bazel.storage.googleapis.com/rbe_autoconfig/v2.0.0.tar.gz", + "https://github.com/davido/rbe_autoconfig/archive/v2.0.0.tar.gz", ], ) -http_archive( - name = "openjdk15_darwin_archive", - build_file_content = """ -java_runtime(name = 'runtime', srcs = glob(['**']), visibility = ['//visibility:public']) -exports_files(["WORKSPACE"], visibility = ["//visibility:public"]) -""", - sha256 = "f80b2e0512d9d8a92be24497334c974bfecc8c898fc215ce0e76594f00437482", - strip_prefix = "zulu15.27.17-ca-jdk15.0.0-macosx_x64", - urls = [ - "https://mirror.bazel.build/cdn.azul.com/zulu/bin/zulu15.27.17-ca-jdk15.0.0-macosx_x64.tar.gz", - "https://cdn.azul.com/zulu/bin/zulu15.27.17-ca-jdk15.0.0-macosx_x64.tar.gz", - ], -) +register_toolchains("//tools:error_prone_warnings_toolchain_java11_definition") + +register_toolchains("//tools:error_prone_warnings_toolchain_java17_definition") JMH_VERS = "1.32" @@ -99,8 +68,8 @@ maven_jar( maven_jar( name = "jzlib", - artifact = "com.jcraft:jzlib:1.1.1", - sha1 = "a1551373315ffc2f96130a0e5704f74e151777ba", + artifact = "com.jcraft:jzlib:1.1.3", + sha1 = "c01428efa717624f7aabf4df319939dda9646b2d", ) maven_jar( @@ -123,14 +92,14 @@ maven_jar( maven_jar( name = "sshd-osgi", - artifact = "org.apache.sshd:sshd-osgi:2.7.0", - sha1 = "a101aad0f79ad424498098f7e91c39d3d92177c1", + artifact = "org.apache.sshd:sshd-osgi:2.8.0", + sha1 = "b2a59b73c045f40d5722b9160d4f909a646d86c9", ) maven_jar( name = "sshd-sftp", - artifact = "org.apache.sshd:sshd-sftp:2.7.0", - sha1 = "0c9eff7145e20b338c1dd6aca36ba93ed7c0147c", + artifact = "org.apache.sshd:sshd-sftp:2.8.0", + sha1 = "d3cd9bc8d335b3ed1a86d2965deb4d202de27442", ) maven_jar( @@ -171,8 +140,8 @@ maven_jar( maven_jar( name = "servlet-api", - artifact = "javax.servlet:javax.servlet-api:3.1.0", - sha1 = "3cd63d075497751784b2fa84be59432f4905bf7c", + artifact = "javax.servlet:javax.servlet-api:4.0.0", + sha1 = "60200affc2fe0165136ed3690faf00b66aed581a", ) maven_jar( @@ -239,8 +208,8 @@ maven_jar( maven_jar( name = "gson", - artifact = "com.google.code.gson:gson:2.8.8", - sha1 = "431fc3cbc0ff81abdbfde070062741089c3ba874", + artifact = "com.google.code.gson:gson:2.8.9", + sha1 = "8a432c1d6825781e21a02db2e2c33c5fde2833b9", ) JETTY_VER = "10.0.6" @@ -294,32 +263,32 @@ maven_jar( src_sha1 = "f35f5525a5d30dc1237b85457d758d578e3ce8d0", ) -BOUNCYCASTLE_VER = "1.69" +BOUNCYCASTLE_VER = "1.70" maven_jar( name = "bcpg", artifact = "org.bouncycastle:bcpg-jdk15on:" + BOUNCYCASTLE_VER, - sha1 = "d99a08c3f651b26e8eb668e941b0bbd2c09ece08", - src_sha1 = "de1fc261b44a8eb60583413a31ffc98ce3dce38b", + sha1 = "062f72ec06f31a6c31a3f3355fce0384b21126d7", + src_sha1 = "9dee73ad926752ee3b421a7dc4531287166ccf36", ) maven_jar( name = "bcprov", artifact = "org.bouncycastle:bcprov-jdk15on:" + BOUNCYCASTLE_VER, - sha1 = "91e1628251cf3ca90093ce9d0fe67e5b7dab3850", - src_sha1 = "67dc6476845f6b29cb520b5df61db65ae56718e4", + sha1 = "4636a0d01f74acaf28082fb62b317f1080118371", + src_sha1 = "6245e15dd47e5fc33cff275df61662e0a8e5958f", ) maven_jar( name = "bcutil", artifact = "org.bouncycastle:bcutil-jdk15on:" + BOUNCYCASTLE_VER, - sha1 = "c3edf93d346e97f64f041e448e7455c39c7eff64", - src_sha1 = "deeb3fbbf373e05e2a20941f9a8ce90e9aeab3d2", + sha1 = "54280e7195a7430d7911ded93fc01e07300b9526", + src_sha1 = "4af4a6c92b8ea07885b27d8536b81b855497f4eb", ) maven_jar( name = "bcpkix", artifact = "org.bouncycastle:bcpkix-jdk15on:" + BOUNCYCASTLE_VER, - sha1 = "45c36fb72fafb0b688c6af795e6cc803f6f79ee5", - src_sha1 = "8bc5214401459bd91eea816b516079da38374e90", + sha1 = "f81e5af49571a9d5a109a88f239a73ce87055417", + src_sha1 = "42f9de53a91b20bc06e88482c57fd97e5a84250d", ) @@ -97,7 +97,7 @@ java_library( java_library( name = "jna", visibility = [ - "//org.eclipse.jgit.ssh.apache.agent:__pkg__", + "//org.eclipse.jgit.ssh.apache.agent:__pkg__", ], exports = ["@jna//jar"], ) @@ -105,7 +105,7 @@ java_library( java_library( name = "jna-platform", visibility = [ - "//org.eclipse.jgit.ssh.apache.agent:__pkg__", + "//org.eclipse.jgit.ssh.apache.agent:__pkg__", ], exports = ["@jna-platform//jar"], ) @@ -277,7 +277,6 @@ java_library( java_library( name = "slf4j-simple", - testonly = 1, visibility = ["//visibility:public"], exports = ["@slf4j-simple//jar"], ) diff --git a/org.eclipse.jgit.ant/pom.xml b/org.eclipse.jgit.ant/pom.xml index c5d92fc607..adbee5c74b 100644 --- a/org.eclipse.jgit.ant/pom.xml +++ b/org.eclipse.jgit.ant/pom.xml @@ -38,6 +38,7 @@ <dependency> <groupId>org.apache.ant</groupId> <artifactId>ant</artifactId> + <version>1.10.12</version> </dependency> </dependencies> diff --git a/org.eclipse.jgit.benchmarks/src/org/eclipse/jgit/benchmarks/GetRefsBenchmark.java b/org.eclipse.jgit.benchmarks/src/org/eclipse/jgit/benchmarks/GetRefsBenchmark.java new file mode 100644 index 0000000000..62627e66f6 --- /dev/null +++ b/org.eclipse.jgit.benchmarks/src/org/eclipse/jgit/benchmarks/GetRefsBenchmark.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2021, Luca Milanesio <luca.milanesio@gmail.com> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.benchmarks; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription; +import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; +import org.eclipse.jgit.internal.storage.file.FileRepository; +import org.eclipse.jgit.lib.*; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.ReceiveCommand; +import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.FileUtils; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +import static org.eclipse.jgit.transport.ReceiveCommand.Type.CREATE; +import static org.eclipse.jgit.transport.ReceiveCommand.Type.UPDATE; + +@State(Scope.Thread) +public class GetRefsBenchmark { + + ThreadLocalRandom branchIndex = ThreadLocalRandom.current(); + + @State(Scope.Benchmark) + public static class BenchmarkState { + + @Param({ "true", "false" }) + boolean useRefTable; + + @Param({ "100", "2500", "10000", "50000" }) + int numBranches; + + @Param({ "true", "false" }) + boolean trustFolderStat; + + List<String> branches = new ArrayList<>(numBranches); + + Path testDir; + + Repository repo; + + @Setup + @SuppressWarnings("boxing") + public void setupBenchmark() throws IOException, GitAPIException { + String firstBranch = "firstbranch"; + testDir = Files.createDirectory(Paths.get("testrepos")); + String repoName = "branches-" + numBranches + "-trustFolderStat-" + + trustFolderStat + "-" + refDatabaseType(); + Path workDir = testDir.resolve(repoName); + Path repoPath = workDir.resolve(".git"); + Git git = Git.init().setDirectory(workDir.toFile()).call(); + RevCommit firstCommit = git.commit().setMessage("First commit") + .call(); + git.branchCreate().setName(firstBranch).call(); + + StoredConfig cfg = git.getRepository().getConfig(); + if (useRefTable) { + ((FileRepository) git.getRepository()).convertRefStorage( + ConfigConstants.CONFIG_REF_STORAGE_REFTABLE, false, + false); + } else { + cfg.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null, + ConfigConstants.CONFIG_KEY_TRUSTFOLDERSTAT, + trustFolderStat); + } + cfg.setInt(ConfigConstants.CONFIG_RECEIVE_SECTION, null, + "maxCommandBytes", Integer.MAX_VALUE); + cfg.save(); + + repo = RepositoryCache.open(RepositoryCache.FileKey + .lenient(repoPath.toFile(), FS.DETECTED)); + + System.out.println("Preparing test"); + System.out.println("- repository: \t\t" + repoPath); + System.out.println("- refDatabase: \t\t" + refDatabaseType()); + System.out.println("- trustFolderStat: \t" + trustFolderStat); + System.out.println("- branches: \t\t" + numBranches); + + BatchRefUpdate u = repo.getRefDatabase().newBatchUpdate(); + + branches = IntStream.range(0, numBranches) + .mapToObj(i -> "branch/" + i % 100 + "/" + i) + .collect(Collectors.toList()); + for (String branch : branches) { + u.addCommand(new ReceiveCommand(ObjectId.zeroId(), + firstCommit.toObjectId(), Constants.R_HEADS + branch, + CREATE)); + } + + System.out.println(); + System.out.print( + String.format("Creating %d branches ... ", numBranches)); + + try (RevWalk rw = new RevWalk(repo)) { + u.execute(rw, new TextProgressMonitor()); + } + System.out.println("DONE"); + } + + private String refDatabaseType() { + return useRefTable ? "reftable" : "refdir"; + } + + @TearDown + public void teardown() throws IOException { + repo.close(); + FileUtils.delete(testDir.toFile(), + FileUtils.RECURSIVE | FileUtils.RETRY); + } + } + + @Benchmark + @BenchmarkMode({ Mode.AverageTime }) + @OutputTimeUnit(TimeUnit.MICROSECONDS) + @Warmup(iterations = 2, time = 100, timeUnit = TimeUnit.MILLISECONDS) + @Measurement(iterations = 2, time = 10, timeUnit = TimeUnit.SECONDS) + public void testGetExactRef(Blackhole blackhole, BenchmarkState state) + throws IOException { + String branchName = state.branches + .get(branchIndex.nextInt(state.numBranches)); + blackhole.consume(state.repo.exactRef(branchName)); + } + + @Benchmark + @BenchmarkMode({ Mode.AverageTime }) + @OutputTimeUnit(TimeUnit.MICROSECONDS) + @Warmup(iterations = 2, time = 100, timeUnit = TimeUnit.MILLISECONDS) + @Measurement(iterations = 2, time = 10, timeUnit = TimeUnit.SECONDS) + public void testGetRefsByPrefix(Blackhole blackhole, BenchmarkState state) + throws IOException { + String branchPrefix = "refs/heads/branch/" + branchIndex.nextInt(100) + + "/"; + blackhole.consume( + state.repo.getRefDatabase().getRefsByPrefix(branchPrefix)); + } + + public static void main(String[] args) throws RunnerException { + Options opt = new OptionsBuilder() + .include(GetRefsBenchmark.class.getSimpleName()) + // .addProfiler(StackProfiler.class) + // .addProfiler(GCProfiler.class) + .forks(1).jvmArgs("-ea").build(); + new Runner(opt).run(); + } +} diff --git a/org.eclipse.jgit.gpg.bc.test/BUILD b/org.eclipse.jgit.gpg.bc.test/BUILD index 35b125f21e..9e5813cb39 100644 --- a/org.eclipse.jgit.gpg.bc.test/BUILD +++ b/org.eclipse.jgit.gpg.bc.test/BUILD @@ -10,8 +10,8 @@ load( junit_tests( name = "bc", srcs = glob(["tst/**/*.java"]), - resource_jars = [":tst_rsrc"], tags = ["bc"], + runtime_deps = [":tst_rsrc"], deps = [ "//lib:bcpg", "//lib:bcprov", diff --git a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/GitSmartHttpTools.java b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/GitSmartHttpTools.java index 012f9a3dc0..f1155dcf57 100644 --- a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/GitSmartHttpTools.java +++ b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/GitSmartHttpTools.java @@ -158,11 +158,11 @@ public class GitSmartHttpTools { } if (isInfoRefs(req)) { - sendInfoRefsError(req, res, textForGit); + sendInfoRefsError(req, res, textForGit, httpStatus); } else if (isUploadPack(req)) { - sendUploadPackError(req, res, textForGit); + sendUploadPackError(req, res, textForGit, httpStatus); } else if (isReceivePack(req)) { - sendReceivePackError(req, res, textForGit); + sendReceivePackError(req, res, textForGit, httpStatus); } else { if (httpStatus < 400) ServletUtils.consumeRequestBody(req); @@ -171,29 +171,32 @@ public class GitSmartHttpTools { } private static void sendInfoRefsError(HttpServletRequest req, - HttpServletResponse res, String textForGit) throws IOException { + HttpServletResponse res, String textForGit, int httpStatus) + throws IOException { ByteArrayOutputStream buf = new ByteArrayOutputStream(128); PacketLineOut pck = new PacketLineOut(buf); String svc = req.getParameter("service"); pck.writeString("# service=" + svc + "\n"); pck.end(); pck.writeString("ERR " + textForGit); - send(req, res, infoRefsResultType(svc), buf.toByteArray()); + send(req, res, infoRefsResultType(svc), buf.toByteArray(), httpStatus); } private static void sendUploadPackError(HttpServletRequest req, - HttpServletResponse res, String textForGit) throws IOException { + HttpServletResponse res, String textForGit, int httpStatus) + throws IOException { // Do not use sideband. Sideband is acceptable only while packfile is // being sent. Other places, like acknowledgement section, do not // support sideband. Use an error packet. ByteArrayOutputStream buf = new ByteArrayOutputStream(128); PacketLineOut pckOut = new PacketLineOut(buf); writePacket(pckOut, textForGit); - send(req, res, UPLOAD_PACK_RESULT_TYPE, buf.toByteArray()); + send(req, res, UPLOAD_PACK_RESULT_TYPE, buf.toByteArray(), httpStatus); } private static void sendReceivePackError(HttpServletRequest req, - HttpServletResponse res, String textForGit) throws IOException { + HttpServletResponse res, String textForGit, int httpStatus) + throws IOException { ByteArrayOutputStream buf = new ByteArrayOutputStream(128); PacketLineOut pckOut = new PacketLineOut(buf); @@ -212,7 +215,7 @@ public class GitSmartHttpTools { writeSideBand(buf, textForGit); else writePacket(pckOut, textForGit); - send(req, res, RECEIVE_PACK_RESULT_TYPE, buf.toByteArray()); + send(req, res, RECEIVE_PACK_RESULT_TYPE, buf.toByteArray(), httpStatus); } private static boolean isReceivePackSideBand(HttpServletRequest req) { @@ -246,9 +249,9 @@ public class GitSmartHttpTools { } private static void send(HttpServletRequest req, HttpServletResponse res, - String type, byte[] buf) throws IOException { + String type, byte[] buf, int httpStatus) throws IOException { ServletUtils.consumeRequestBody(req); - res.setStatus(HttpServletResponse.SC_OK); + res.setStatus(httpStatus); res.setContentType(type); res.setContentLength(buf.length); try (OutputStream os = res.getOutputStream()) { diff --git a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/UploadPackServlet.java b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/UploadPackServlet.java index 2759b8a0fe..23a398f9db 100644 --- a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/UploadPackServlet.java +++ b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/UploadPackServlet.java @@ -10,6 +10,7 @@ package org.eclipse.jgit.http.server; +import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN; import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; @@ -48,6 +49,7 @@ import org.eclipse.jgit.transport.RefAdvertiser.PacketLineOutRefAdvertiser; import org.eclipse.jgit.transport.ServiceMayNotContinueException; import org.eclipse.jgit.transport.UploadPack; import org.eclipse.jgit.transport.UploadPackInternalServerErrorException; +import org.eclipse.jgit.transport.WantNotValidException; import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; import org.eclipse.jgit.transport.resolver.UploadPackFactory; @@ -151,6 +153,16 @@ class UploadPackServlet extends HttpServlet { } } + private static int statusCodeForThrowable(Throwable error) { + if (error instanceof ServiceNotEnabledException) { + return SC_FORBIDDEN; + } + if (error instanceof WantNotValidException) { + return SC_BAD_REQUEST; + } + return SC_INTERNAL_SERVER_ERROR; + } + private final UploadPackErrorHandler handler; UploadPackServlet(@Nullable UploadPackErrorHandler handler) { @@ -169,6 +181,7 @@ class UploadPackServlet extends HttpServlet { UploadPackRunnable r = () -> { UploadPack up = (UploadPack) req.getAttribute(ATTRIBUTE_HANDLER); + // to be explicitly closed by caller @SuppressWarnings("resource") SmartOutputStream out = new SmartOutputStream(req, rsp, false) { @Override @@ -195,6 +208,8 @@ class UploadPackServlet extends HttpServlet { log(up.getRepository(), e.getCause()); consumeRequestBody(req); out.close(); + } finally { + up.close(); } }; @@ -215,9 +230,12 @@ class UploadPackServlet extends HttpServlet { log(up.getRepository(), e); if (!rsp.isCommitted()) { rsp.reset(); - String msg = e instanceof PackProtocolException ? e.getMessage() - : null; - sendError(req, rsp, SC_INTERNAL_SERVER_ERROR, msg); + String msg = null; + if (e instanceof PackProtocolException + || e instanceof ServiceNotEnabledException) { + msg = e.getMessage(); + } + sendError(req, rsp, statusCodeForThrowable(e), msg); } } } diff --git a/org.eclipse.jgit.http.test/build.properties b/org.eclipse.jgit.http.test/build.properties index a909f1301f..a12a660466 100644 --- a/org.eclipse.jgit.http.test/build.properties +++ b/org.eclipse.jgit.http.test/build.properties @@ -4,5 +4,4 @@ output.. = bin/ bin.includes = META-INF/,\ .,\ plugin.properties -additional.bundles = org.apache.log4j,\ - org.slf4j.binding.log4j12 +additional.bundles = org.slf4j.binding.simple diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/HookMessageTest.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/HookMessageTest.java index db9f2ae3d7..20de2567c1 100644 --- a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/HookMessageTest.java +++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/HookMessageTest.java @@ -97,6 +97,7 @@ public class HookMessageTest extends AllFactoriesHttpTestCase { server.setUp(); remoteRepository = src.getRepository(); + addRepoToClose(remoteRepository); remoteURI = toURIish(app, srcName); StoredConfig cfg = remoteRepository.getConfig(); 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 df093c185b..3438c52c8d 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 @@ -23,6 +23,7 @@ import java.io.File; import java.io.OutputStream; import java.net.URI; import java.net.URL; +import java.text.MessageFormat; import java.util.List; import javax.servlet.http.HttpServletRequest; @@ -308,7 +309,10 @@ public class HttpClientTests extends AllFactoriesHttpTestCase { fail("connection opened even though service disabled"); } catch (TransportException err) { String exp = smartAuthNoneURI + ": " - + JGitText.get().serviceNotEnabledNoName; + + MessageFormat.format( + JGitText.get().serviceNotPermitted, + smartAuthNoneURI.toString() + "/", + "git-upload-pack"); assertEquals(exp, err.getMessage()); } } diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/MeasurePackSizeTest.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/MeasurePackSizeTest.java index 9ffa19efef..000eecdccf 100644 --- a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/MeasurePackSizeTest.java +++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/MeasurePackSizeTest.java @@ -90,6 +90,7 @@ public class MeasurePackSizeTest extends AllFactoriesHttpTestCase { server.setUp(); remoteRepository = src.getRepository(); + addRepoToClose(remoteRepository); remoteURI = toURIish(app, srcName); StoredConfig cfg = remoteRepository.getConfig(); 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 887e970a0c..8f3888e4d5 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 @@ -57,7 +57,7 @@ import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.TransportConfigCallback; -import org.eclipse.jgit.errors.RemoteRepositoryException; +import org.eclipse.jgit.errors.NoRemoteRepositoryException; import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.errors.UnsupportedCredentialItem; import org.eclipse.jgit.http.server.GitServlet; @@ -496,8 +496,9 @@ public class SmartClientSmartServerTest extends AllProtocolsHttpTestCase { try { t.openFetch(); fail("fetch connection opened"); - } catch (RemoteRepositoryException notFound) { - assertEquals(uri + ": Git repository not found", + } catch (NoRemoteRepositoryException notFound) { + assertEquals(uri + ": " + uri + + "/info/refs?service=git-upload-pack not found: Not Found", notFound.getMessage()); } } @@ -510,7 +511,7 @@ public class SmartClientSmartServerTest extends AllProtocolsHttpTestCase { assertEquals(join(uri, "info/refs"), info.getPath()); assertEquals(1, info.getParameters().size()); assertEquals("git-upload-pack", info.getParameter("service")); - assertEquals(200, info.getStatus()); + assertEquals(404, info.getStatus()); assertEquals("application/x-git-upload-pack-advertisement", info.getResponseHeader(HDR_CONTENT_TYPE)); } @@ -536,8 +537,9 @@ public class SmartClientSmartServerTest extends AllProtocolsHttpTestCase { Collections.singletonList( new RefSpec(unreachableCommit.name())))); assertTrue(e.getMessage().contains( - "want " + unreachableCommit.name() + " not valid")); + "Bad Request")); } + assertLastRequestStatusCode(400); } @Test @@ -558,8 +560,9 @@ public class SmartClientSmartServerTest extends AllProtocolsHttpTestCase { () -> t.fetch(NullProgressMonitor.INSTANCE, Collections.singletonList(new RefSpec(A.name())))); assertTrue( - e.getMessage().contains("want " + A.name() + " not valid")); + e.getMessage().contains("Bad Request")); } + assertLastRequestStatusCode(400); } @Test @@ -916,6 +919,7 @@ public class SmartClientSmartServerTest extends AllProtocolsHttpTestCase { } catch (TransportException e) { assertTrue(e.getMessage().contains("301")); } + assertLastRequestStatusCode(301); } @Test @@ -934,6 +938,7 @@ public class SmartClientSmartServerTest extends AllProtocolsHttpTestCase { assertTrue( e.getMessage().contains("http.followRedirects is false")); } + assertLastRequestStatusCode(301); } private void assertFetchRequests(List<AccessEvent> requests, int index) { @@ -1605,8 +1610,9 @@ public class SmartClientSmartServerTest extends AllProtocolsHttpTestCase { fail("Server accepted want " + id.name()); } catch (TransportException err) { assertTrue(err.getMessage() - .contains("want " + id.name() + " not valid")); + .contains("Bad Request")); } + assertLastRequestStatusCode(400); } @Test @@ -1650,7 +1656,7 @@ public class SmartClientSmartServerTest extends AllProtocolsHttpTestCase { fail("Successfully served ref with value " + c.getRef(master)); } catch (TransportException err) { assertTrue("Unexpected exception message " + err.getMessage(), - err.getMessage().contains("Internal server error")); + err.getMessage().contains("Server Error")); } } finally { noRefServer.tearDown(); @@ -1821,6 +1827,11 @@ public class SmartClientSmartServerTest extends AllProtocolsHttpTestCase { .getResponseHeader(HDR_CONTENT_TYPE)); } + private void assertLastRequestStatusCode(int statusCode) { + List<AccessEvent> requests = getRequests(); + assertEquals(statusCode, requests.get(requests.size() - 1).getStatus()); + } + private void enableReceivePack() throws IOException { final StoredConfig cfg = remoteRepository.getConfig(); cfg.setBoolean("http", null, "receivepack", true); diff --git a/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/HttpTestCase.java b/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/HttpTestCase.java index 3e7c84f667..877b918695 100644 --- a/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/HttpTestCase.java +++ b/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/HttpTestCase.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.Set; import org.eclipse.jetty.servlet.ServletContextHandler; +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.AnyObjectId; @@ -80,7 +81,9 @@ public abstract class HttpTestCase extends LocalDiskRepositoryTestCase { */ protected TestRepository<Repository> createTestRepository() throws IOException { - return new TestRepository<>(createBareRepository()); + final FileRepository repository = createBareRepository(); + addRepoToClose(repository); + return new TestRepository<>(repository); } /** diff --git a/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/RecordingLogger.java b/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/RecordingLogger.java index 9c3c980ad8..af63084e93 100644 --- a/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/RecordingLogger.java +++ b/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/RecordingLogger.java @@ -180,11 +180,15 @@ public class RecordingLogger extends MarkerIgnoringBase { @Override public void warn(String format, Object arg) { - warn(format, Collections.singleton(arg)); + addWarnings(format, Collections.singleton(arg)); } @Override public void warn(String format, Object... arguments) { + addWarnings(format, arguments); + } + + private void addWarnings(String format, Object... arguments) { synchronized (warnings) { int i = 0; int index = format.indexOf("{}"); diff --git a/org.eclipse.jgit.junit.ssh/META-INF/MANIFEST.MF b/org.eclipse.jgit.junit.ssh/META-INF/MANIFEST.MF index 813195fe31..f80f86a68b 100644 --- a/org.eclipse.jgit.junit.ssh/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.junit.ssh/META-INF/MANIFEST.MF @@ -8,31 +8,31 @@ Bundle-Localization: plugin Bundle-Vendor: %Bundle-Vendor Bundle-ActivationPolicy: lazy Bundle-RequiredExecutionEnvironment: JavaSE-11 -Import-Package: org.apache.sshd.common;version="[2.7.0,2.8.0)", - org.apache.sshd.common.config.keys;version="[2.7.0,2.8.0)", - org.apache.sshd.common.file.virtualfs;version="[2.7.0,2.8.0)", - org.apache.sshd.common.helpers;version="[2.7.0,2.8.0)", - org.apache.sshd.common.io;version="[2.7.0,2.8.0)", - org.apache.sshd.common.kex;version="[2.7.0,2.8.0)", - org.apache.sshd.common.keyprovider;version="[2.7.0,2.8.0)", - org.apache.sshd.common.session;version="[2.7.0,2.8.0)", - org.apache.sshd.common.signature;version="[2.7.0,2.8.0)", - org.apache.sshd.common.util.buffer;version="[2.7.0,2.8.0)", - org.apache.sshd.common.util.logging;version="[2.7.0,2.8.0)", - org.apache.sshd.common.util.security;version="[2.7.0,2.8.0)", - org.apache.sshd.common.util.threads;version="[2.7.0,2.8.0)", - org.apache.sshd.core;version="[2.7.0,2.8.0)", - org.apache.sshd.server;version="[2.7.0,2.8.0)", - org.apache.sshd.server.auth;version="[2.7.0,2.8.0)", - org.apache.sshd.server.auth.gss;version="[2.7.0,2.8.0)", - org.apache.sshd.server.auth.keyboard;version="[2.7.0,2.8.0)", - org.apache.sshd.server.auth.password;version="[2.7.0,2.8.0)", - org.apache.sshd.server.command;version="[2.7.0,2.8.0)", - org.apache.sshd.server.session;version="[2.7.0,2.8.0)", - org.apache.sshd.server.shell;version="[2.7.0,2.8.0)", - org.apache.sshd.server.subsystem;version="[2.7.0,2.8.0)", - org.apache.sshd.sftp;version="[2.7.0,2.8.0)", - org.apache.sshd.sftp.server;version="[2.7.0,2.8.0)", +Import-Package: org.apache.sshd.common;version="[2.8.0,2.9.0)", + org.apache.sshd.common.config.keys;version="[2.8.0,2.9.0)", + org.apache.sshd.common.file.virtualfs;version="[2.8.0,2.9.0)", + org.apache.sshd.common.helpers;version="[2.8.0,2.9.0)", + org.apache.sshd.common.io;version="[2.8.0,2.9.0)", + org.apache.sshd.common.kex;version="[2.8.0,2.9.0)", + org.apache.sshd.common.keyprovider;version="[2.8.0,2.9.0)", + org.apache.sshd.common.session;version="[2.8.0,2.9.0)", + org.apache.sshd.common.signature;version="[2.8.0,2.9.0)", + org.apache.sshd.common.util.buffer;version="[2.8.0,2.9.0)", + org.apache.sshd.common.util.logging;version="[2.8.0,2.9.0)", + org.apache.sshd.common.util.security;version="[2.8.0,2.9.0)", + org.apache.sshd.common.util.threads;version="[2.8.0,2.9.0)", + org.apache.sshd.core;version="[2.8.0,2.9.0)", + org.apache.sshd.server;version="[2.8.0,2.9.0)", + org.apache.sshd.server.auth;version="[2.8.0,2.9.0)", + org.apache.sshd.server.auth.gss;version="[2.8.0,2.9.0)", + org.apache.sshd.server.auth.keyboard;version="[2.8.0,2.9.0)", + org.apache.sshd.server.auth.password;version="[2.8.0,2.9.0)", + org.apache.sshd.server.command;version="[2.8.0,2.9.0)", + org.apache.sshd.server.session;version="[2.8.0,2.9.0)", + org.apache.sshd.server.shell;version="[2.8.0,2.9.0)", + org.apache.sshd.server.subsystem;version="[2.8.0,2.9.0)", + org.apache.sshd.sftp;version="[2.8.0,2.9.0)", + org.apache.sshd.sftp.server;version="[2.8.0,2.9.0)", org.eclipse.jgit.annotations;version="[7.0.0,7.1.0)", org.eclipse.jgit.api;version="[7.0.0,7.1.0)", org.eclipse.jgit.api.errors;version="[7.0.0,7.1.0)", diff --git a/org.eclipse.jgit.junit.ssh/pom.xml b/org.eclipse.jgit.junit.ssh/pom.xml index fe53847b4a..2d4caedf56 100644 --- a/org.eclipse.jgit.junit.ssh/pom.xml +++ b/org.eclipse.jgit.junit.ssh/pom.xml @@ -55,6 +55,16 @@ <groupId>org.apache.sshd</groupId> <artifactId>sshd-sftp</artifactId> <version>${apache-sshd-version}</version> + <exclusions> + <exclusion> + <groupId>org.apache.sshd</groupId> + <artifactId>sshd-core</artifactId> + </exclusion> + <exclusion> + <groupId>org.apache.sshd</groupId> + <artifactId>sshd-common</artifactId> + </exclusion> + </exclusions> </dependency> <dependency> diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java index 494d2f3bae..a3c5b1202d 100644 --- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java +++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java @@ -45,6 +45,8 @@ import org.eclipse.jgit.util.FileUtils; import org.eclipse.jgit.util.SystemReader; import org.junit.After; import org.junit.Before; +import org.junit.Rule; +import org.junit.rules.TestName; /** * JUnit TestCase with specialized support for temporary local repository. @@ -84,18 +86,33 @@ public abstract class LocalDiskRepositoryTestCase { private File tmp; /** + * The current test name. + */ + @Rule + public TestName currentTest = new TestName(); + + private String getTestName() { + String name = currentTest.getMethodName(); + name = name.replaceAll("[^a-zA-Z0-9]", "_"); + name = name.replaceAll("__+", "_"); + if (name.startsWith("_")) { + name = name.substring(1); + } + return name; + } + + /** * Setup test * * @throws Exception */ @Before public void setUp() throws Exception { - tmp = File.createTempFile("jgit_test_", "_tmp"); + tmp = File.createTempFile("jgit_" + getTestName() + '_', "_tmp"); CleanupThread.deleteOnShutdown(tmp); if (!tmp.delete() || !tmp.mkdir()) { throw new IOException("Cannot create " + tmp); } - mockSystemReader = new MockSystemReader(); SystemReader.setInstance(mockSystemReader); diff --git a/org.eclipse.jgit.lfs.server.test/tst/org/eclipse/jgit/lfs/server/fs/PushTest.java b/org.eclipse.jgit.lfs.server.test/tst/org/eclipse/jgit/lfs/server/fs/PushTest.java index 70c0463e11..06708b334a 100644 --- a/org.eclipse.jgit.lfs.server.test/tst/org/eclipse/jgit/lfs/server/fs/PushTest.java +++ b/org.eclipse.jgit.lfs.server.test/tst/org/eclipse/jgit/lfs/server/fs/PushTest.java @@ -11,6 +11,7 @@ package org.eclipse.jgit.lfs.server.fs; import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import java.io.InputStream; import java.nio.file.Files; @@ -29,6 +30,7 @@ import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; +import org.eclipse.jgit.transport.RefSpec; import org.eclipse.jgit.transport.URIish; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.filter.PathFilter; @@ -128,4 +130,18 @@ public class PushTest extends LfsServerTest { server.getRequests().toString()); } + @Test + public void testDeleteBranch() throws Exception { + String branch = "new-branch"; + git.branchCreate().setName(branch).call(); + + String destRef = Constants.R_HEADS + branch; + git.push().setRefSpecs(new RefSpec().setSource(branch).setDestination(destRef)).call(); + + // Should not fail on push. + git.branchDelete().setBranchNames(branch).setForce(true).call(); + git.push().setRefSpecs(new RefSpec().setSource(null).setDestination(destRef)).call(); + + assertTrue(server.getRequests().isEmpty()); + } } diff --git a/org.eclipse.jgit.lfs.server/src/org/eclipse/jgit/lfs/server/fs/ObjectDownloadListener.java b/org.eclipse.jgit.lfs.server/src/org/eclipse/jgit/lfs/server/fs/ObjectDownloadListener.java index cc57947a79..d42701125e 100644 --- a/org.eclipse.jgit.lfs.server/src/org/eclipse/jgit/lfs/server/fs/ObjectDownloadListener.java +++ b/org.eclipse.jgit.lfs.server/src/org/eclipse/jgit/lfs/server/fs/ObjectDownloadListener.java @@ -81,6 +81,7 @@ public class ObjectDownloadListener implements WriteListener { * * Write file content */ + @SuppressWarnings("Finally") @Override public void onWritePossible() throws IOException { while (out.isReady()) { diff --git a/org.eclipse.jgit.lfs.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.lfs.test/META-INF/MANIFEST.MF index 06f2a891df..0b93af2808 100644 --- a/org.eclipse.jgit.lfs.test/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.lfs.test/META-INF/MANIFEST.MF @@ -13,9 +13,12 @@ Import-Package: org.eclipse.jgit.api;version="[7.0.0,7.1.0)", org.eclipse.jgit.junit;version="[7.0.0,7.1.0)", org.eclipse.jgit.lfs;version="[7.0.0,7.1.0)", org.eclipse.jgit.lfs.errors;version="[7.0.0,7.1.0)", + org.eclipse.jgit.lfs.internal;version="[7.0.0,7.1.0)", org.eclipse.jgit.lfs.lib;version="[7.0.0,7.1.0)", org.eclipse.jgit.lib;version="[7.0.0,7.1.0)", org.eclipse.jgit.revwalk;version="[7.0.0,7.1.0)", + org.eclipse.jgit.transport;version="[7.0.0,7.1.0)", + org.eclipse.jgit.transport.http;version="[7.0.0,7.1.0)", org.eclipse.jgit.treewalk;version="[7.0.0,7.1.0)", org.eclipse.jgit.treewalk.filter;version="[7.0.0,7.1.0)", org.eclipse.jgit.util;version="[7.0.0,7.1.0)", diff --git a/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/LfsConfigGitTest.java b/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/LfsConfigGitTest.java new file mode 100644 index 0000000000..3ac41571a4 --- /dev/null +++ b/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/LfsConfigGitTest.java @@ -0,0 +1,301 @@ +/* + * Copyright (C) 2022, Matthias Fromme <mfromme@dspace.de> + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.lfs; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.ResetCommand.ResetType; +import org.eclipse.jgit.attributes.FilterCommand; +import org.eclipse.jgit.attributes.FilterCommandRegistry; +import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.lfs.internal.LfsConnectionFactory; +import org.eclipse.jgit.lfs.lib.Constants; +import org.eclipse.jgit.lib.ConfigConstants; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.StoredConfig; +import org.eclipse.jgit.transport.http.HttpConnection; +import org.eclipse.jgit.util.HttpSupport; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * Test if the lfs config is used in the correct way during checkout. + * + * Two lfs-files are created, one that comes before .gitattributes and + * .lfsconfig in git order (".aaa.txt") and one that comes after ("zzz.txt"). + * + * During checkout/reset it is tested if the correct version of the lfs config + * is used. + * + * TODO: The current behavior seems a little bit strange/unintuitive. Some files + * are checked out before and some after the config files. This leads to the + * behavior, that during a single command the config changes. Since this seems + * to be the same way in native git, the behavior is accepted for now. + * + */ +public class LfsConfigGitTest extends RepositoryTestCase { + + private static final String SMUDGE_NAME = org.eclipse.jgit.lib.Constants.BUILTIN_FILTER_PREFIX + + Constants.ATTR_FILTER_DRIVER_PREFIX + + org.eclipse.jgit.lib.Constants.ATTR_FILTER_TYPE_SMUDGE; + + private static final String LFS_SERVER_URI1 = "https://lfs.server1/test/uri"; + + private static final String EXPECTED_SERVER_URL1 = LFS_SERVER_URI1 + + Protocol.OBJECTS_LFS_ENDPOINT; + + private static final String LFS_SERVER_URI2 = "https://lfs.server2/test/uri"; + + private static final String EXPECTED_SERVER_URL2 = LFS_SERVER_URI2 + + Protocol.OBJECTS_LFS_ENDPOINT; + + private static final String LFS_SERVER_URI3 = "https://lfs.server3/test/uri"; + + private static final String EXPECTED_SERVER_URL3 = LFS_SERVER_URI3 + + Protocol.OBJECTS_LFS_ENDPOINT; + + private static final String FAKE_LFS_POINTER1 = "version https://git-lfs.github.com/spec/v1\n" + + "oid sha256:6ce9fab52ee9a6c4c097def4e049c6acdeba44c99d26e83ba80adec1473c9b2d\n" + + "size 253952\n"; + + private static final String FAKE_LFS_POINTER2 = "version https://git-lfs.github.com/spec/v1\n" + + "oid sha256:a4b711cd989863ae2038758a62672138347abbbae4076a7ad3a545fda7d08f82\n" + + "size 67072\n"; + + private static List<String> checkoutURLs = new ArrayList<>(); + + static class SmudgeFilterMock extends FilterCommand { + public SmudgeFilterMock(Repository db, InputStream in, + OutputStream out) throws IOException { + super(in, out); + HttpConnection lfsServerConn = LfsConnectionFactory.getLfsConnection(db, + HttpSupport.METHOD_POST, Protocol.OPERATION_DOWNLOAD); + checkoutURLs.add(lfsServerConn.getURL().toString()); + } + + @Override + public int run() throws IOException { + // Stupid no impl + in.transferTo(out); + return -1; + } + } + + @BeforeClass + public static void installLfs() { + FilterCommandRegistry.register(SMUDGE_NAME, SmudgeFilterMock::new); + } + + @AfterClass + public static void removeLfs() { + FilterCommandRegistry.unregister(SMUDGE_NAME); + } + + private Git git; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + git = new Git(db); + // commit something + writeTrashFile("Test.txt", "Hello world"); + git.add().addFilepattern("Test.txt").call(); + git.commit().setMessage("Initial commit").call(); + // prepare the config for LFS + StoredConfig config = git.getRepository().getConfig(); + config.setString("filter", "lfs", "smudge", SMUDGE_NAME); + config.setString(ConfigConstants.CONFIG_CORE_SECTION, null, + ConfigConstants.CONFIG_KEY_AUTOCRLF, "false"); + config.save(); + + fileBefore = null; + fileAfter = null; + configFile = null; + gitAttributesFile = null; + } + + File fileBefore; + + File fileAfter; + + File configFile; + + File gitAttributesFile; + + private void createLfsFiles(String lfsPointer) throws Exception { + //File to be checked out before lfs config + String fileNameBefore = ".aaa.txt"; + fileBefore = writeTrashFile(fileNameBefore, lfsPointer); + git.add().addFilepattern(fileNameBefore).call(); + + // File to be checked out after lfs config + String fileNameAfter = "zzz.txt"; + fileAfter = writeTrashFile(fileNameAfter, lfsPointer); + git.add().addFilepattern(fileNameAfter).call(); + + git.commit().setMessage("Commit LFS Pointer files").call(); + } + + + private String addLfsConfigFiles(String lfsServerUrl) throws Exception { + // Add config files to the repo + String lfsConfig1 = createLfsConfig(lfsServerUrl); + git.add().addFilepattern(Constants.DOT_LFS_CONFIG).call(); + // Modify gitattributes on second call, to force checkout too. + if (gitAttributesFile == null) { + gitAttributesFile = writeTrashFile(".gitattributes", + "*.txt filter=lfs"); + } else { + gitAttributesFile = writeTrashFile(".gitattributes", + "*.txt filter=lfs\n"); + } + + git.add().addFilepattern(".gitattributes").call(); + git.commit().setMessage("Commit config files").call(); + return lfsConfig1; + } + + private String createLfsConfig(String lfsServerUrl) throws IOException { + String lfsConfig1 = "[lfs]\n url = " + lfsServerUrl; + configFile = writeTrashFile(Constants.DOT_LFS_CONFIG, lfsConfig1); + return lfsConfig1; + } + + @Test + public void checkoutLfsObjects_reset() throws Exception { + createLfsFiles(FAKE_LFS_POINTER1); + String lfsConfig1 = addLfsConfigFiles(LFS_SERVER_URI1); + + // Delete files to force action on reset + assertTrue(configFile.delete()); + assertTrue(fileBefore.delete()); + assertTrue(fileAfter.delete()); + + assertTrue(gitAttributesFile.delete()); + + // create config file with different url + createLfsConfig(LFS_SERVER_URI3); + + checkoutURLs.clear(); + git.reset().setMode(ResetType.HARD).call(); + + checkFile(configFile, lfsConfig1); + checkFile(fileBefore, FAKE_LFS_POINTER1); + checkFile(fileAfter, FAKE_LFS_POINTER1); + + assertEquals(2, checkoutURLs.size()); + // TODO: Should may be EXPECTED_SERVR_URL1 + assertEquals(EXPECTED_SERVER_URL3, checkoutURLs.get(0)); + assertEquals(EXPECTED_SERVER_URL1, checkoutURLs.get(1)); + } + + @Test + public void checkoutLfsObjects_BranchSwitch() throws Exception { + // Create a new branch "URL1" and add config files + git.checkout().setCreateBranch(true).setName("URL1").call(); + + createLfsFiles(FAKE_LFS_POINTER1); + String lfsConfig1 = addLfsConfigFiles(LFS_SERVER_URI1); + + // Create a second new branch "URL2" and add config files + git.checkout().setCreateBranch(true).setName("URL2").call(); + + createLfsFiles(FAKE_LFS_POINTER2); + String lfsConfig2 = addLfsConfigFiles(LFS_SERVER_URI2); + + checkFile(configFile, lfsConfig2); + checkFile(fileBefore, FAKE_LFS_POINTER2); + checkFile(fileAfter, FAKE_LFS_POINTER2); + + checkoutURLs.clear(); + git.checkout().setName("URL1").call(); + + checkFile(configFile, lfsConfig1); + checkFile(fileBefore, FAKE_LFS_POINTER1); + checkFile(fileAfter, FAKE_LFS_POINTER1); + + assertEquals(2, checkoutURLs.size()); + // TODO: Should may be EXPECTED_SERVR_URL1 + assertEquals(EXPECTED_SERVER_URL2, checkoutURLs.get(0)); + assertEquals(EXPECTED_SERVER_URL1, checkoutURLs.get(1)); + + checkoutURLs.clear(); + git.checkout().setName("URL2").call(); + + checkFile(configFile, lfsConfig2); + checkFile(fileBefore, FAKE_LFS_POINTER2); + checkFile(fileAfter, FAKE_LFS_POINTER2); + + assertEquals(2, checkoutURLs.size()); + // TODO: Should may be EXPECTED_SERVR_URL2 + assertEquals(EXPECTED_SERVER_URL1, checkoutURLs.get(0)); + assertEquals(EXPECTED_SERVER_URL2, checkoutURLs.get(1)); + } + + @Test + public void checkoutLfsObjects_BranchSwitch_ModifiedLocal() + throws Exception { + + // Create a new branch "URL1" and add config files + git.checkout().setCreateBranch(true).setName("URL1").call(); + + createLfsFiles(FAKE_LFS_POINTER1); + addLfsConfigFiles(LFS_SERVER_URI1); + + // Create a second new branch "URL2" and add config files + git.checkout().setCreateBranch(true).setName("URL2").call(); + + createLfsFiles(FAKE_LFS_POINTER2); + addLfsConfigFiles(LFS_SERVER_URI1); + + // create config file with different url + assertTrue(configFile.delete()); + String lfsConfig3 = createLfsConfig(LFS_SERVER_URI3); + + checkFile(configFile, lfsConfig3); + checkFile(fileBefore, FAKE_LFS_POINTER2); + checkFile(fileAfter, FAKE_LFS_POINTER2); + + checkoutURLs.clear(); + git.checkout().setName("URL1").call(); + + checkFile(fileBefore, FAKE_LFS_POINTER1); + checkFile(fileAfter, FAKE_LFS_POINTER1); + checkFile(configFile, lfsConfig3); + + assertEquals(2, checkoutURLs.size()); + + assertEquals(EXPECTED_SERVER_URL3, checkoutURLs.get(0)); + assertEquals(EXPECTED_SERVER_URL3, checkoutURLs.get(1)); + + checkoutURLs.clear(); + git.checkout().setName("URL2").call(); + + checkFile(fileBefore, FAKE_LFS_POINTER2); + checkFile(fileAfter, FAKE_LFS_POINTER2); + checkFile(configFile, lfsConfig3); + + assertEquals(2, checkoutURLs.size()); + assertEquals(EXPECTED_SERVER_URL3, checkoutURLs.get(0)); + assertEquals(EXPECTED_SERVER_URL3, checkoutURLs.get(1)); + } +} diff --git a/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/LfsGitTest.java b/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/LfsGitTest.java index 8964310e41..3e83c8ef49 100644 --- a/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/LfsGitTest.java +++ b/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/LfsGitTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others + * Copyright (C) 2021, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -68,6 +68,27 @@ public class LfsGitTest extends RepositoryTestCase { } @Test + public void testBranchSwitch() throws Exception { + git.branchCreate().setName("abranch").call(); + git.checkout().setName("abranch").call(); + File aFile = writeTrashFile("a.bin", "aaa"); + writeTrashFile(".gitattributes", "a.bin filter=lfs"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("acommit").call(); + git.checkout().setName("master").call(); + git.branchCreate().setName("bbranch").call(); + git.checkout().setName("bbranch").call(); + File bFile = writeTrashFile("b.bin", "bbb"); + writeTrashFile(".gitattributes", "b.bin filter=lfs"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("bcommit").call(); + git.checkout().setName("abranch").call(); + checkFile(aFile, "aaa"); + git.checkout().setName("bbranch").call(); + checkFile(bFile, "bbb"); + } + + @Test public void checkoutNonLfsPointer() throws Exception { String content = "size_t\nsome_function(void* ptr);\n"; File smallFile = writeTrashFile("Test.txt", content); diff --git a/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/internal/LfsConnectionFactoryTest.java b/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/internal/LfsConnectionFactoryTest.java new file mode 100644 index 0000000000..badcb7d7e5 --- /dev/null +++ b/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/internal/LfsConnectionFactoryTest.java @@ -0,0 +1,317 @@ +/* + * Copyright (C) 2022 Nail Samatov <sanail@yandex.ru> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.lfs.internal; + +import static org.eclipse.jgit.lib.Constants.DEFAULT_REMOTE_NAME; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.RemoteAddCommand; +import org.eclipse.jgit.attributes.FilterCommandRegistry; +import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.lfs.CleanFilter; +import org.eclipse.jgit.lfs.Protocol; +import org.eclipse.jgit.lfs.SmudgeFilter; +import org.eclipse.jgit.lfs.errors.LfsConfigInvalidException; +import org.eclipse.jgit.lfs.lib.Constants; +import org.eclipse.jgit.lib.ConfigConstants; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.StoredConfig; +import org.eclipse.jgit.transport.URIish; +import org.eclipse.jgit.transport.http.HttpConnection; +import org.eclipse.jgit.util.HttpSupport; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +public class LfsConnectionFactoryTest extends RepositoryTestCase { + + private static final String SMUDGE_NAME = org.eclipse.jgit.lib.Constants.BUILTIN_FILTER_PREFIX + + Constants.ATTR_FILTER_DRIVER_PREFIX + + org.eclipse.jgit.lib.Constants.ATTR_FILTER_TYPE_SMUDGE; + + private static final String CLEAN_NAME = org.eclipse.jgit.lib.Constants.BUILTIN_FILTER_PREFIX + + Constants.ATTR_FILTER_DRIVER_PREFIX + + org.eclipse.jgit.lib.Constants.ATTR_FILTER_TYPE_CLEAN; + + private final static String LFS_SERVER_URL1 = "https://lfs.server1/test/uri"; + + private final static String LFS_SERVER_URL2 = "https://lfs.server2/test/uri"; + + private final static String ORIGIN_URL = "https://git.server/test/uri"; + + private Git git; + + @BeforeClass + public static void installLfs() { + FilterCommandRegistry.register(SMUDGE_NAME, SmudgeFilter.FACTORY); + FilterCommandRegistry.register(CLEAN_NAME, CleanFilter.FACTORY); + } + + @AfterClass + public static void removeLfs() { + FilterCommandRegistry.unregister(SMUDGE_NAME); + FilterCommandRegistry.unregister(CLEAN_NAME); + } + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + git = new Git(db); + + // Just to have a non empty repo + writeTrashFile("Test.txt", "Hello world from the LFS Factory Test"); + git.add().addFilepattern("Test.txt").call(); + git.commit().setMessage("Initial commit").call(); + } + + @Test + public void lfsUrlFromRemoteUrlWithDotGit() throws Exception { + addRemoteUrl("https://localhost/repo.git"); + checkLfsUrl("https://localhost/repo.git/info/lfs"); + } + + @Test + public void lfsUrlFromRemoteUrlWithoutDotGit() throws Exception { + addRemoteUrl("https://localhost/repo"); + checkLfsUrl("https://localhost/repo.git/info/lfs"); + } + + @Test + public void lfsUrlFromLocalConfig() throws Exception { + addRemoteUrl("https://localhost/repo"); + + StoredConfig cfg = ((Repository) db).getConfig(); + cfg.setString(ConfigConstants.CONFIG_SECTION_LFS, + null, + ConfigConstants.CONFIG_KEY_URL, + "https://localhost/repo/lfs"); + cfg.save(); + + checkLfsUrl("https://localhost/repo/lfs"); + } + + @Test + public void lfsUrlFromOriginConfig() throws Exception { + addRemoteUrl("https://localhost/repo"); + + StoredConfig cfg = ((Repository) db).getConfig(); + cfg.setString(ConfigConstants.CONFIG_SECTION_LFS, + org.eclipse.jgit.lib.Constants.DEFAULT_REMOTE_NAME, + ConfigConstants.CONFIG_KEY_URL, + "https://localhost/repo/lfs"); + cfg.save(); + + checkLfsUrl("https://localhost/repo/lfs"); + } + + @Test + public void lfsUrlNotConfigured() throws Exception { + assertThrows(LfsConfigInvalidException.class, + () -> LfsConnectionFactory.getLfsConnection(db, + HttpSupport.METHOD_POST, Protocol.OPERATION_DOWNLOAD)); + } + + @Test + public void checkGetLfsConnection_lfsurl_lfsconfigFromWorkingDir() + throws Exception { + writeLfsConfig(); + checkLfsUrl(LFS_SERVER_URL1); + } + + @Test + public void checkGetLfsConnection_lfsurl_lfsconfigFromIndex() + throws Exception { + writeLfsConfig(); + git.add().addFilepattern(Constants.DOT_LFS_CONFIG).call(); + deleteTrashFile(Constants.DOT_LFS_CONFIG); + checkLfsUrl(LFS_SERVER_URL1); + } + + @Test + public void checkGetLfsConnection_lfsurl_lfsconfigFromHEAD() + throws Exception { + writeLfsConfig(); + git.add().addFilepattern(Constants.DOT_LFS_CONFIG).call(); + git.commit().setMessage("Commit LFS Config").call(); + + /* + * reading .lfsconfig from HEAD seems only testable using a bare repo, + * since otherwise working tree or index are used + */ + File directory = createTempDirectory("testBareRepo"); + try (Repository bareRepoDb = Git.cloneRepository() + .setDirectory(directory) + .setURI(db.getDirectory().toURI().toString()).setBare(true) + .call().getRepository()) { + + checkLfsUrl(LFS_SERVER_URL1); + } + } + + @Test + public void checkGetLfsConnection_remote_lfsconfigFromWorkingDir() + throws Exception { + addRemoteUrl(ORIGIN_URL); + writeLfsConfig(LFS_SERVER_URL1, "lfs", DEFAULT_REMOTE_NAME, "url"); + checkLfsUrl(LFS_SERVER_URL1); + } + + /** + * Test the config file precedence. + * + * Checking only with the local repository config is sufficient since from + * that point the "normal" precedence is used. + * + * @throws Exception + */ + @Test + public void checkGetLfsConnection_ConfigFilePrecedence_lfsconfigFromWorkingDir() + throws Exception { + writeLfsConfig(); + checkLfsUrl(LFS_SERVER_URL1); + + StoredConfig config = git.getRepository().getConfig(); + config.setString(ConfigConstants.CONFIG_SECTION_LFS, null, + ConfigConstants.CONFIG_KEY_URL, LFS_SERVER_URL2); + config.save(); + + checkLfsUrl(LFS_SERVER_URL2); + } + + @Test + public void checkGetLfsConnection_InvalidLfsConfig_WorkingDir() + throws Exception { + writeInvalidLfsConfig(); + LfsConfigInvalidException actualException = assertThrows( + LfsConfigInvalidException.class, () -> { + LfsConnectionFactory.getLfsConnection(db, HttpSupport.METHOD_POST, + Protocol.OPERATION_DOWNLOAD); + }); + assertTrue(getStackTrace(actualException) + .contains("Invalid line in config file")); + } + + @Test + public void checkGetLfsConnection_InvalidLfsConfig_Index() + throws Exception { + writeInvalidLfsConfig(); + git.add().addFilepattern(Constants.DOT_LFS_CONFIG).call(); + deleteTrashFile(Constants.DOT_LFS_CONFIG); + LfsConfigInvalidException actualException = assertThrows( + LfsConfigInvalidException.class, () -> { + LfsConnectionFactory.getLfsConnection(db, HttpSupport.METHOD_POST, + Protocol.OPERATION_DOWNLOAD); + }); + assertTrue(getStackTrace(actualException) + .contains("Invalid line in config file")); + } + + @Test + public void checkGetLfsConnection_InvalidLfsConfig_HEAD() throws Exception { + writeInvalidLfsConfig(); + git.add().addFilepattern(Constants.DOT_LFS_CONFIG).call(); + git.commit().setMessage("Commit LFS Config").call(); + + /* + * reading .lfsconfig from HEAD seems only testable using a bare repo, + * since otherwise working tree or index are used + */ + File directory = createTempDirectory("testBareRepo"); + try (Repository bareRepoDb = Git.cloneRepository() + .setDirectory(directory) + .setURI(db.getDirectory().toURI().toString()).setBare(true) + .call().getRepository()) { + LfsConfigInvalidException actualException = assertThrows( + LfsConfigInvalidException.class, + () -> { + LfsConnectionFactory.getLfsConnection(db, + HttpSupport.METHOD_POST, + Protocol.OPERATION_DOWNLOAD); + }); + assertTrue(getStackTrace(actualException) + .contains("Invalid line in config file")); + } + } + + private void addRemoteUrl(String remotUrl) throws Exception { + RemoteAddCommand add = git.remoteAdd(); + add.setUri(new URIish(remotUrl)); + add.setName(org.eclipse.jgit.lib.Constants.DEFAULT_REMOTE_NAME); + add.call(); + } + + /** + * Returns the stack trace of the provided exception as string + * + * @param actualException + * @return The exception stack trace as string + */ + private String getStackTrace(Exception actualException) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + actualException.printStackTrace(pw); + return sw.toString(); + } + + private void writeLfsConfig() throws IOException { + writeLfsConfig(LFS_SERVER_URL1, "lfs", "url"); + } + + private void writeLfsConfig(String lfsUrl, String section, String name) + throws IOException { + writeLfsConfig(lfsUrl, section, null, name); + } + + /* + * Write simple lfs config with single entry. Do not use FileBasedConfig to + * avoid introducing new dependency (for now). + */ + private void writeLfsConfig(String lfsUrl, String section, + String subsection, String name) throws IOException { + StringBuilder config = new StringBuilder(); + config.append("["); + config.append(section); + if (subsection != null) { + config.append(" \""); + config.append(subsection); + config.append("\""); + } + config.append("]\n"); + config.append(" "); + config.append(name); + config.append(" = "); + config.append(lfsUrl); + writeTrashFile(Constants.DOT_LFS_CONFIG, config.toString()); + } + + private void writeInvalidLfsConfig() throws IOException { + writeTrashFile(Constants.DOT_LFS_CONFIG, + "{lfs]\n url = " + LFS_SERVER_URL1); + } + + private void checkLfsUrl(String lfsUrl) throws IOException { + HttpConnection lfsServerConn; + lfsServerConn = LfsConnectionFactory.getLfsConnection(db, + HttpSupport.METHOD_POST, Protocol.OPERATION_DOWNLOAD); + + assertEquals(lfsUrl + Protocol.OBJECTS_LFS_ENDPOINT, + lfsServerConn.getURL().toString()); + } +} diff --git a/org.eclipse.jgit.lfs/META-INF/MANIFEST.MF b/org.eclipse.jgit.lfs/META-INF/MANIFEST.MF index 1d5f76ffa9..d19f613e57 100644 --- a/org.eclipse.jgit.lfs/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.lfs/META-INF/MANIFEST.MF @@ -17,6 +17,7 @@ Import-Package: com.google.gson;version="[2.8.2,3.0.0)", org.eclipse.jgit.api.errors;version="[7.0.0,7.1.0)", org.eclipse.jgit.attributes;version="[7.0.0,7.1.0)", org.eclipse.jgit.diff;version="[7.0.0,7.1.0)", + org.eclipse.jgit.dircache;version="[7.0.0,7.1.0)", org.eclipse.jgit.errors;version="[7.0.0,7.1.0)", org.eclipse.jgit.hooks;version="[7.0.0,7.1.0)", org.eclipse.jgit.internal.storage.file;version="[7.0.0,7.1.0)", diff --git a/org.eclipse.jgit.lfs/resources/org/eclipse/jgit/lfs/internal/LfsText.properties b/org.eclipse.jgit.lfs/resources/org/eclipse/jgit/lfs/internal/LfsText.properties index 0e00f146ae..c4c0dacf42 100644 --- a/org.eclipse.jgit.lfs/resources/org/eclipse/jgit/lfs/internal/LfsText.properties +++ b/org.eclipse.jgit.lfs/resources/org/eclipse/jgit/lfs/internal/LfsText.properties @@ -1,19 +1,18 @@ corruptLongObject=The content hash ''{0}'' of the long object ''{1}'' doesn''t match its id, the corrupt object will be deleted. -incorrectLONG_OBJECT_ID_LENGTH=Incorrect LONG_OBJECT_ID_LENGTH. -inconsistentMediafileLength=Mediafile {0} has unexpected length; expected {1} but found {2}. +dotLfsConfigReadFailed=Reading .lfsconfig failed inconsistentContentLength=Unexpected content length reported by LFS server ({0}), expected {1} but reported was {2} +inconsistentMediafileLength=Mediafile {0} has unexpected length; expected {1} but found {2}. +incorrectLONG_OBJECT_ID_LENGTH=Incorrect LONG_OBJECT_ID_LENGTH. invalidLongId=Invalid id: {0} invalidLongIdLength=Invalid id length {0}; should be {1} +lfsFailedToGetRepository=failed to get repository {0} +lfsNoDownloadUrl=Need to download object from LFS server but couldn't determine LFS server URL +lfsUnauthorized=Not authorized to perform operation {0} on repository {1} lfsUnavailable=LFS is not available for repository {0} +missingLocalObject=Local Object {0} is missing protocolError=LFS Protocol Error {0}: {1} -requiredHashFunctionNotAvailable=Required hash function {0} not available. repositoryNotFound=Repository {0} not found repositoryReadOnly=Repository {0} is read-only -lfsUnavailable=LFS is not available for repository {0} -lfsUnathorized=Not authorized to perform operation {0} on repository {1} -lfsFailedToGetRepository=failed to get repository {0} -lfsNoDownloadUrl="Need to download object from LFS server but couldn't determine LFS server URL" +requiredHashFunctionNotAvailable=Required hash function {0} not available. serverFailure=When trying to open a connection to {0} the server responded with an error code. rc={1} -wrongAmoutOfDataReceived=While downloading data from the content server {0} {1} bytes have been received while {2} have been expected -userConfigInvalid="User config file {0} invalid {1}" -missingLocalObject="Local Object {0} is missing"
\ No newline at end of file +wrongAmountOfDataReceived=While downloading data from the content server {0} {1} bytes have been received while {2} have been expected
\ No newline at end of file diff --git a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/LfsPrePushHook.java b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/LfsPrePushHook.java index d6ce855794..9b3d60812a 100644 --- a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/LfsPrePushHook.java +++ b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/LfsPrePushHook.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017, Markus Duft <markus.duft@ssi-schaefer.com> and others + * Copyright (C) 2017, 2022 Markus Duft <markus.duft@ssi-schaefer.com> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -101,8 +101,10 @@ public class LfsPrePushHook extends PrePushHook { } HttpConnection api = LfsConnectionFactory.getLfsConnection( getRepository(), METHOD_POST, OPERATION_UPLOAD); - Map<String, LfsPointer> oid2ptr = requestBatchUpload(api, toPush); - uploadContents(api, oid2ptr); + if (!isDryRun()) { + Map<String, LfsPointer> oid2ptr = requestBatchUpload(api, toPush); + uploadContents(api, oid2ptr); + } return EMPTY; } @@ -113,6 +115,9 @@ public class LfsPrePushHook extends PrePushHook { try (ObjectWalk walk = new ObjectWalk(getRepository())) { for (RemoteRefUpdate up : refs) { + if (up.isDelete()) { + continue; + } walk.setRewriteParents(false); excludeRemoteRefs(walk); walk.markStart(walk.parseCommit(up.getNewObjectId())); diff --git a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/SmudgeFilter.java b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/SmudgeFilter.java index 3411887567..c26a1bfbb3 100644 --- a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/SmudgeFilter.java +++ b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/SmudgeFilter.java @@ -205,7 +205,7 @@ public class SmudgeFilter extends FilterCommand { long bytesCopied = Files.copy(contentIn, path); if (bytesCopied != o.size) { throw new IOException(MessageFormat.format( - LfsText.get().wrongAmoutOfDataReceived, + LfsText.get().wrongAmountOfDataReceived, contentServerConn.getURL(), Long.valueOf(bytesCopied), Long.valueOf(o.size))); diff --git a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/errors/LfsUnauthorized.java b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/errors/LfsUnauthorized.java index 36889db8a6..0dc6aeab29 100644 --- a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/errors/LfsUnauthorized.java +++ b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/errors/LfsUnauthorized.java @@ -31,7 +31,7 @@ public class LfsUnauthorized extends LfsException { * the repository name. */ public LfsUnauthorized(String operation, String name) { - super(MessageFormat.format(LfsText.get().lfsUnathorized, operation, + super(MessageFormat.format(LfsText.get().lfsUnauthorized, operation, name)); } } diff --git a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsConfig.java b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsConfig.java new file mode 100644 index 0000000000..857ccbe056 --- /dev/null +++ b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsConfig.java @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2022, Matthias Fromme <mfromme@dspace.de> + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.lfs.internal; + +import java.io.File; +import java.io.IOException; + +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lfs.errors.LfsConfigInvalidException; +import org.eclipse.jgit.lfs.lib.Constants; +import org.eclipse.jgit.lib.BlobBasedConfig; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevTree; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.treewalk.TreeWalk; + +import static org.eclipse.jgit.lib.Constants.HEAD; + +/** + * Encapsulate access to the {@code .lfsconfig}. + * <p> + * According to the git lfs documentation the order to find the + * {@code .lfsconfig} file is: + * </p> + * <ol> + * <li>in the root of the working tree</li> + * <li>in the index</li> + * <li>in the HEAD; for bare repositories this is the only place that is + * searched</li> + * </ol> + * <p> + * Values from the {@code .lfsconfig} are used only if not specified in another + * git config file to allow local override without modifiction of a committed + * file. + * </p> + * + * @see <a href= + * "https://github.com/git-lfs/git-lfs/blob/main/docs/man/git-lfs-config.5.ronn">Configuration + * options for git-lfs</a> + */ +public class LfsConfig { + private Repository db; + private Config delegate; + + /** + * Create a new instance of the LfsConfig. + * + * @param db + * the associated repo + */ + public LfsConfig(Repository db) { + this.db = db; + } + + /** + * Getter for the delegate to allow lazy initialization. + * + * @return the delegate {@link Config} + * @throws IOException + */ + private Config getDelegate() throws IOException { + if (delegate == null) { + delegate = this.load(); + } + return delegate; + } + + /** + * Read the .lfsconfig file from the repository + * + * An empty config is returned be empty if no lfs config exists. + * + * @return The loaded lfs config + * + * @throws IOException + */ + private Config load() throws IOException { + Config result = null; + + if (!db.isBare()) { + result = loadFromWorkingTree(); + if (result == null) { + result = loadFromIndex(); + } + } + + if (result == null) { + result = loadFromHead(); + } + + if (result == null) { + result = emptyConfig(); + } + + return result; + } + + /** + * Try to read the lfs config from a file called .lfsconfig at the top level + * of the working tree. + * + * @return the config, or <code>null</code> + * @throws IOException + */ + @Nullable + private Config loadFromWorkingTree() + throws IOException { + File lfsConfig = db.getFS().resolve(db.getWorkTree(), + Constants.DOT_LFS_CONFIG); + if (lfsConfig.isFile()) { + FileBasedConfig config = new FileBasedConfig(lfsConfig, db.getFS()); + try { + config.load(); + return config; + } catch (ConfigInvalidException e) { + throw new LfsConfigInvalidException( + LfsText.get().dotLfsConfigReadFailed, e); + } + } + return null; + } + + /** + * Try to read the lfs config from an entry called .lfsconfig contained in + * the index. + * + * @return the config, or <code>null</code> if the entry does not exist + * @throws IOException + */ + @Nullable + private Config loadFromIndex() + throws IOException { + try { + DirCacheEntry entry = db.readDirCache() + .getEntry(Constants.DOT_LFS_CONFIG); + if (entry != null) { + return new BlobBasedConfig(null, db, entry.getObjectId()); + } + } catch (ConfigInvalidException e) { + throw new LfsConfigInvalidException( + LfsText.get().dotLfsConfigReadFailed, e); + } + return null; + } + + /** + * Try to read the lfs config from an entry called .lfsconfig contained in + * the head revision. + * + * @return the config, or <code>null</code> if the file does not exist + * @throws IOException + */ + @Nullable + private Config loadFromHead() throws IOException { + try (RevWalk revWalk = new RevWalk(db)) { + ObjectId headCommitId = db.resolve(HEAD); + if (headCommitId == null) { + return null; + } + RevCommit commit = revWalk.parseCommit(headCommitId); + RevTree tree = commit.getTree(); + TreeWalk treewalk = TreeWalk.forPath(db, Constants.DOT_LFS_CONFIG, + tree); + if (treewalk != null) { + return new BlobBasedConfig(null, db, treewalk.getObjectId(0)); + } + } catch (ConfigInvalidException e) { + throw new LfsConfigInvalidException( + LfsText.get().dotLfsConfigReadFailed, e); + } + return null; + } + + /** + * Create an empty config as fallback to avoid null pointer checks. + * + * @return an empty config + */ + private Config emptyConfig() { + return new Config(); + } + + /** + * Get string value or null if not found. + * + * First tries to find the value in the git config files. If not found tries + * to find data in .lfsconfig. + * + * @param section + * the section + * @param subsection + * the subsection for the value + * @param name + * the key name + * @return a String value from the config, <code>null</code> if not found + * @throws IOException + */ + @Nullable + public String getString(final String section, final String subsection, + final String name) throws IOException { + String result = db.getConfig().getString(section, subsection, name); + if (result == null) { + result = getDelegate().getString(section, subsection, name); + } + return result; + } +} diff --git a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsConnectionFactory.java b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsConnectionFactory.java index e221913bea..12b688d157 100644 --- a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsConnectionFactory.java +++ b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017, Markus Duft <markus.duft@ssi-schaefer.com> and others + * Copyright (C) 2017, 2022 Markus Duft <markus.duft@ssi-schaefer.com> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -39,12 +39,12 @@ import org.eclipse.jgit.transport.URIish; import org.eclipse.jgit.transport.http.HttpConnection; import org.eclipse.jgit.util.HttpSupport; import org.eclipse.jgit.util.SshSupport; +import org.eclipse.jgit.util.StringUtils; /** * Provides means to get a valid LFS connection for a given repository. */ public class LfsConnectionFactory { - private static final int SSH_AUTH_TIMEOUT_SECONDS = 30; private static final String SCHEME_HTTPS = "https"; //$NON-NLS-1$ private static final String SCHEME_SSH = "ssh"; //$NON-NLS-1$ @@ -64,7 +64,7 @@ public class LfsConnectionFactory { * be used for * @param purpose * the action, e.g. Protocol.OPERATION_DOWNLOAD - * @return the url for the lfs server. e.g. + * @return the connection for the lfs server. e.g. * "https://github.com/github/git-lfs.git/info/lfs" * @throws IOException */ @@ -92,13 +92,30 @@ public class LfsConnectionFactory { return connection; } + /** + * Get LFS Server URL. + * + * @param db + * the repository to work with + * @param purpose + * the action, e.g. Protocol.OPERATION_DOWNLOAD + * @param additionalHeaders + * additional headers that can be used to connect to LFS server + * @return the URL for the LFS server. e.g. + * "https://github.com/github/git-lfs.git/info/lfs" + * @throws IOException + * if the LFS config is invalid or cannot be accessed + * @see <a href= + * "https://github.com/git-lfs/git-lfs/blob/main/docs/api/server-discovery.md"> + * Server Discovery documentation</a> + */ private static String getLfsUrl(Repository db, String purpose, Map<String, String> additionalHeaders) - throws LfsConfigInvalidException { - StoredConfig config = db.getConfig(); + throws IOException { + LfsConfig config = new LfsConfig(db); String lfsUrl = config.getString(ConfigConstants.CONFIG_SECTION_LFS, - null, - ConfigConstants.CONFIG_KEY_URL); + null, ConfigConstants.CONFIG_KEY_URL); + Exception ex = null; if (lfsUrl == null) { String remoteUrl = null; @@ -106,6 +123,7 @@ public class LfsConnectionFactory { lfsUrl = config.getString(ConfigConstants.CONFIG_SECTION_LFS, remote, ConfigConstants.CONFIG_KEY_URL); + // This could be done better (more precise logic), but according // to https://github.com/git-lfs/git-lfs/issues/1759 git-lfs // generally only supports 'origin' in an integrated workflow. @@ -125,8 +143,6 @@ public class LfsConnectionFactory { | CommandFailedException e) { ex = e; } - } else { - lfsUrl = lfsUrl + Protocol.INFO_LFS_ENDPOINT; } } if (lfsUrl == null) { @@ -149,7 +165,8 @@ public class LfsConnectionFactory { additionalHeaders.putAll(action.header); return action.href; } - return remoteUrl + Protocol.INFO_LFS_ENDPOINT; + return StringUtils.nameWithDotGit(remoteUrl) + + Protocol.INFO_LFS_ENDPOINT; } private static Protocol.ExpiringAction getSshAuthentication( diff --git a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsText.java b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsText.java index 1ca37a9f66..8ef8f59f93 100644 --- a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsText.java +++ b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsText.java @@ -28,21 +28,21 @@ public class LfsText extends TranslationBundle { // @formatter:off /***/ public String corruptLongObject; - /***/ public String inconsistentMediafileLength; + /***/ public String dotLfsConfigReadFailed; /***/ public String inconsistentContentLength; + /***/ public String inconsistentMediafileLength; /***/ public String incorrectLONG_OBJECT_ID_LENGTH; /***/ public String invalidLongId; /***/ public String invalidLongIdLength; + /***/ public String lfsFailedToGetRepository; + /***/ public String lfsNoDownloadUrl; + /***/ public String lfsUnauthorized; /***/ public String lfsUnavailable; + /***/ public String missingLocalObject; /***/ public String protocolError; - /***/ public String requiredHashFunctionNotAvailable; /***/ public String repositoryNotFound; /***/ public String repositoryReadOnly; - /***/ public String lfsUnathorized; - /***/ public String lfsFailedToGetRepository; - /***/ public String lfsNoDownloadUrl; + /***/ public String requiredHashFunctionNotAvailable; /***/ public String serverFailure; - /***/ public String wrongAmoutOfDataReceived; - /***/ public String userConfigInvalid; - /***/ public String missingLocalObject; + /***/ public String wrongAmountOfDataReceived; } diff --git a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/lib/Constants.java b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/lib/Constants.java index 3212a63504..9b41ec31f1 100644 --- a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/lib/Constants.java +++ b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/lib/Constants.java @@ -82,6 +82,13 @@ public final class Constants { public static final String ATTR_FILTER_DRIVER_PREFIX = "lfs/"; /** + * Config file name for lfs specific configuration + * + * @since 6.1 + */ + public static final String DOT_LFS_CONFIG = ".lfsconfig"; + + /** * Create a new digest function for objects. * * @return a new digest object. diff --git a/org.eclipse.jgit.packaging/.settings/org.eclipse.core.resources.prefs b/org.eclipse.jgit.packaging/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000000..99f26c0203 --- /dev/null +++ b/org.eclipse.jgit.packaging/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/<project>=UTF-8 diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.gpg.bc.feature/.settings/org.eclipse.core.resources.prefs b/org.eclipse.jgit.packaging/org.eclipse.jgit.gpg.bc.feature/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000000..99f26c0203 --- /dev/null +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.gpg.bc.feature/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/<project>=UTF-8 diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.repository/category.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.repository/category.xml index af9523fd4f..7fa2b837e7 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.repository/category.xml +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.repository/category.xml @@ -165,12 +165,6 @@ <bundle id="org.apache.httpcomponents.httpcore.source"> <category name="JGit-dependency-bundles"/> </bundle> - <bundle id="org.apache.log4j"> - <category name="JGit-dependency-bundles"/> - </bundle> - <bundle id="org.apache.log4j.source"> - <category name="JGit-dependency-bundles"/> - </bundle> <bundle id="org.apache.sshd.osgi"> <category name="JGit-dependency-bundles"/> </bundle> @@ -219,10 +213,10 @@ <bundle id="org.slf4j.api.source"> <category name="JGit-dependency-bundles"/> </bundle> - <bundle id="org.slf4j.binding.log4j12"> + <bundle id="org.slf4j.binding.simple"> <category name="JGit-dependency-bundles"/> </bundle> - <bundle id="org.slf4j.binding.log4j12.source"> + <bundle id="org.slf4j.binding.simple.source"> <category name="JGit-dependency-bundles"/> </bundle> <bundle id="org.tukaani.xz"> diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.jsch.feature/.settings/org.eclipse.core.resources.prefs b/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.jsch.feature/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000000..99f26c0203 --- /dev/null +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.jsch.feature/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/<project>=UTF-8 diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/.settings/org.eclipse.core.resources.prefs b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000000..99f26c0203 --- /dev/null +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/<project>=UTF-8 diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.17.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.17.target index 6d960335dc..3d4f2378d1 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.17.target +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.17.target @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="UTF-8" standalone="no"?> <?pde?> <!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl --> -<target name="jgit-4.17" sequenceNumber="1638648728"> +<target name="jgit-4.17" sequenceNumber="1654550635"> <locations> <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit"> <unit id="jakarta.servlet-api" version="4.0.0"/> @@ -23,12 +23,12 @@ <repository id="jetty-10.0.x" location="https://download.eclipse.org/eclipse/jetty/10.0.6/"/> </location> <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit"> - <unit id="com.google.gson" version="2.8.8.v20211029-0838"/> - <unit id="com.google.gson.source" version="2.8.8.v20211029-0838"/> + <unit id="com.google.gson" version="2.8.9.v20220111-1409"/> + <unit id="com.google.gson.source" version="2.8.9.v20220111-1409"/> <unit id="com.jcraft.jsch" version="0.1.55.v20190404-1902"/> <unit id="com.jcraft.jsch.source" version="0.1.55.v20190404-1902"/> - <unit id="com.jcraft.jzlib" version="1.1.1.v201205102305"/> - <unit id="com.jcraft.jzlib.source" version="1.1.1.v201205102305"/> + <unit id="com.jcraft.jzlib" version="1.1.3.v20220502-1820"/> + <unit id="com.jcraft.jzlib.source" version="1.1.3.v20220502-1820"/> <unit id="com.sun.jna" version="5.8.0.v20210503-0343"/> <unit id="com.sun.jna.source" version="5.8.0.v20210503-0343"/> <unit id="com.sun.jna.platform" version="5.8.0.v20210406-1004"/> @@ -39,8 +39,8 @@ <unit id="net.bytebuddy.byte-buddy-agent" version="1.9.0.v20181106-1534"/> <unit id="net.bytebuddy.byte-buddy-agent.source" version="1.9.0.v20181106-1534"/> <unit id="net.bytebuddy.byte-buddy.source" version="1.9.0.v20181107-1410"/> - <unit id="net.i2p.crypto.eddsa" version="0.3.0.v20210923-1401"/> - <unit id="net.i2p.crypto.eddsa.source" version="0.3.0.v20210923-1401"/> + <unit id="net.i2p.crypto.eddsa" version="0.3.0.v20220506-1020"/> + <unit id="net.i2p.crypto.eddsa.source" version="0.3.0.v20220506-1020"/> <unit id="org.apache.ant" version="1.10.12.v20211102-1452"/> <unit id="org.apache.ant.source" version="1.10.12.v20211102-1452"/> <unit id="org.apache.commons.codec" version="1.14.0.v20200818-1422"/> @@ -51,24 +51,22 @@ <unit id="org.apache.commons.logging.source" version="1.2.0.v20180409-1502"/> <unit id="org.apache.httpcomponents.httpclient" version="4.5.13.v20210128-2225"/> <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.13.v20210128-2225"/> - <unit id="org.apache.httpcomponents.httpcore" version="4.4.14.v20210128-2225"/> - <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.14.v20210128-2225"/> - <unit id="org.apache.log4j" version="1.2.15.v201012070815"/> - <unit id="org.apache.log4j.source" version="1.2.15.v201012070815"/> - <unit id="org.apache.sshd.osgi" version="2.7.0.v20210623-0618"/> - <unit id="org.apache.sshd.osgi.source" version="2.7.0.v20210623-0618"/> - <unit id="org.apache.sshd.sftp" version="2.7.0.v20210623-0618"/> - <unit id="org.apache.sshd.sftp.source" version="2.7.0.v20210623-0618"/> + <unit id="org.apache.httpcomponents.httpcore" version="4.4.15.v20220209-2345"/> + <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.15.v20220209-2345"/> + <unit id="org.apache.sshd.osgi" version="2.8.0.v20211227-1750"/> + <unit id="org.apache.sshd.osgi.source" version="2.8.0.v20211227-1750"/> + <unit id="org.apache.sshd.sftp" version="2.8.0.v20211227-1750"/> + <unit id="org.apache.sshd.sftp.source" version="2.8.0.v20211227-1750"/> <unit id="org.assertj" version="3.20.2.v20210706-1104"/> <unit id="org.assertj.source" version="3.20.2.v20210706-1104"/> - <unit id="org.bouncycastle.bcpg" version="1.69.0.v20210713-1924"/> - <unit id="org.bouncycastle.bcpg.source" version="1.69.0.v20210713-1924"/> - <unit id="org.bouncycastle.bcpkix" version="1.69.0.v20210713-1924"/> - <unit id="org.bouncycastle.bcpkix.source" version="1.69.0.v20210713-1924"/> - <unit id="org.bouncycastle.bcprov" version="1.69.0.v20210923-1401"/> - <unit id="org.bouncycastle.bcprov.source" version="1.69.0.v20210923-1401"/> - <unit id="org.bouncycastle.bcutil" version="1.69.0.v20210713-1924"/> - <unit id="org.bouncycastle.bcutil.source" version="1.69.0.v20210713-1924"/> + <unit id="org.bouncycastle.bcpg" version="1.70.0.v20220507-1208"/> + <unit id="org.bouncycastle.bcpg.source" version="1.70.0.v20220507-1208"/> + <unit id="org.bouncycastle.bcpkix" version="1.70.0.v20220105-1522"/> + <unit id="org.bouncycastle.bcpkix.source" version="1.70.0.v20220105-1522"/> + <unit id="org.bouncycastle.bcprov" version="1.70.0.v20220507-1208"/> + <unit id="org.bouncycastle.bcprov.source" version="1.70.0.v20220507-1208"/> + <unit id="org.bouncycastle.bcutil" version="1.70.0.v20220105-1522"/> + <unit id="org.bouncycastle.bcutil.source" version="1.70.0.v20220105-1522"/> <unit id="org.hamcrest" version="2.2.0.v20210711-0821"/> <unit id="org.hamcrest.source" version="2.2.0.v20210711-0821"/> <unit id="org.hamcrest.core" version="1.3.0.v20180420-1519"/> @@ -85,11 +83,11 @@ <unit id="org.objenesis.source" version="2.6.0.v20180420-1519"/> <unit id="org.slf4j.api" version="1.7.30.v20200204-2150"/> <unit id="org.slf4j.api.source" version="1.7.30.v20200204-2150"/> - <unit id="org.slf4j.binding.log4j12" version="1.7.30.v20201108-2042"/> - <unit id="org.slf4j.binding.log4j12.source" version="1.7.30.v20201108-2042"/> + <unit id="org.slf4j.binding.simple" version="1.7.30.v20200204-2150"/> + <unit id="org.slf4j.binding.simple.source" version="1.7.30.v20200204-2150"/> <unit id="org.tukaani.xz" version="1.9.0.v20210624-1259"/> <unit id="org.tukaani.xz.source" version="1.9.0.v20210624-1259"/> - <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20211122181901/repository"/> + <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20220531185310/repository"/> </location> <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit"> <unit id="org.eclipse.osgi" version="0.0.0"/> diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.17.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.17.tpd index cd364d3563..9c824c5fd4 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.17.tpd +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.17.tpd @@ -1,7 +1,7 @@ target "jgit-4.17" with source configurePhase include "projects/jetty-10.0.x.tpd" -include "orbit/R20211122181901-2021-12.tpd" +include "orbit/R20220531185310-2022-06.tpd" location "https://download.eclipse.org/releases/2020-09/" { org.eclipse.osgi lazy diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.18.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.18.target index cbe26bbf16..603fefbadf 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.18.target +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.18.target @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="UTF-8" standalone="no"?> <?pde?> <!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl --> -<target name="jgit-4.18" sequenceNumber="1638648728"> +<target name="jgit-4.18" sequenceNumber="1654550635"> <locations> <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit"> <unit id="jakarta.servlet-api" version="4.0.0"/> @@ -23,12 +23,12 @@ <repository id="jetty-10.0.x" location="https://download.eclipse.org/eclipse/jetty/10.0.6/"/> </location> <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit"> - <unit id="com.google.gson" version="2.8.8.v20211029-0838"/> - <unit id="com.google.gson.source" version="2.8.8.v20211029-0838"/> + <unit id="com.google.gson" version="2.8.9.v20220111-1409"/> + <unit id="com.google.gson.source" version="2.8.9.v20220111-1409"/> <unit id="com.jcraft.jsch" version="0.1.55.v20190404-1902"/> <unit id="com.jcraft.jsch.source" version="0.1.55.v20190404-1902"/> - <unit id="com.jcraft.jzlib" version="1.1.1.v201205102305"/> - <unit id="com.jcraft.jzlib.source" version="1.1.1.v201205102305"/> + <unit id="com.jcraft.jzlib" version="1.1.3.v20220502-1820"/> + <unit id="com.jcraft.jzlib.source" version="1.1.3.v20220502-1820"/> <unit id="com.sun.jna" version="5.8.0.v20210503-0343"/> <unit id="com.sun.jna.source" version="5.8.0.v20210503-0343"/> <unit id="com.sun.jna.platform" version="5.8.0.v20210406-1004"/> @@ -39,8 +39,8 @@ <unit id="net.bytebuddy.byte-buddy-agent" version="1.9.0.v20181106-1534"/> <unit id="net.bytebuddy.byte-buddy-agent.source" version="1.9.0.v20181106-1534"/> <unit id="net.bytebuddy.byte-buddy.source" version="1.9.0.v20181107-1410"/> - <unit id="net.i2p.crypto.eddsa" version="0.3.0.v20210923-1401"/> - <unit id="net.i2p.crypto.eddsa.source" version="0.3.0.v20210923-1401"/> + <unit id="net.i2p.crypto.eddsa" version="0.3.0.v20220506-1020"/> + <unit id="net.i2p.crypto.eddsa.source" version="0.3.0.v20220506-1020"/> <unit id="org.apache.ant" version="1.10.12.v20211102-1452"/> <unit id="org.apache.ant.source" version="1.10.12.v20211102-1452"/> <unit id="org.apache.commons.codec" version="1.14.0.v20200818-1422"/> @@ -51,24 +51,22 @@ <unit id="org.apache.commons.logging.source" version="1.2.0.v20180409-1502"/> <unit id="org.apache.httpcomponents.httpclient" version="4.5.13.v20210128-2225"/> <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.13.v20210128-2225"/> - <unit id="org.apache.httpcomponents.httpcore" version="4.4.14.v20210128-2225"/> - <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.14.v20210128-2225"/> - <unit id="org.apache.log4j" version="1.2.15.v201012070815"/> - <unit id="org.apache.log4j.source" version="1.2.15.v201012070815"/> - <unit id="org.apache.sshd.osgi" version="2.7.0.v20210623-0618"/> - <unit id="org.apache.sshd.osgi.source" version="2.7.0.v20210623-0618"/> - <unit id="org.apache.sshd.sftp" version="2.7.0.v20210623-0618"/> - <unit id="org.apache.sshd.sftp.source" version="2.7.0.v20210623-0618"/> + <unit id="org.apache.httpcomponents.httpcore" version="4.4.15.v20220209-2345"/> + <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.15.v20220209-2345"/> + <unit id="org.apache.sshd.osgi" version="2.8.0.v20211227-1750"/> + <unit id="org.apache.sshd.osgi.source" version="2.8.0.v20211227-1750"/> + <unit id="org.apache.sshd.sftp" version="2.8.0.v20211227-1750"/> + <unit id="org.apache.sshd.sftp.source" version="2.8.0.v20211227-1750"/> <unit id="org.assertj" version="3.20.2.v20210706-1104"/> <unit id="org.assertj.source" version="3.20.2.v20210706-1104"/> - <unit id="org.bouncycastle.bcpg" version="1.69.0.v20210713-1924"/> - <unit id="org.bouncycastle.bcpg.source" version="1.69.0.v20210713-1924"/> - <unit id="org.bouncycastle.bcpkix" version="1.69.0.v20210713-1924"/> - <unit id="org.bouncycastle.bcpkix.source" version="1.69.0.v20210713-1924"/> - <unit id="org.bouncycastle.bcprov" version="1.69.0.v20210923-1401"/> - <unit id="org.bouncycastle.bcprov.source" version="1.69.0.v20210923-1401"/> - <unit id="org.bouncycastle.bcutil" version="1.69.0.v20210713-1924"/> - <unit id="org.bouncycastle.bcutil.source" version="1.69.0.v20210713-1924"/> + <unit id="org.bouncycastle.bcpg" version="1.70.0.v20220507-1208"/> + <unit id="org.bouncycastle.bcpg.source" version="1.70.0.v20220507-1208"/> + <unit id="org.bouncycastle.bcpkix" version="1.70.0.v20220105-1522"/> + <unit id="org.bouncycastle.bcpkix.source" version="1.70.0.v20220105-1522"/> + <unit id="org.bouncycastle.bcprov" version="1.70.0.v20220507-1208"/> + <unit id="org.bouncycastle.bcprov.source" version="1.70.0.v20220507-1208"/> + <unit id="org.bouncycastle.bcutil" version="1.70.0.v20220105-1522"/> + <unit id="org.bouncycastle.bcutil.source" version="1.70.0.v20220105-1522"/> <unit id="org.hamcrest" version="2.2.0.v20210711-0821"/> <unit id="org.hamcrest.source" version="2.2.0.v20210711-0821"/> <unit id="org.hamcrest.core" version="1.3.0.v20180420-1519"/> @@ -85,11 +83,11 @@ <unit id="org.objenesis.source" version="2.6.0.v20180420-1519"/> <unit id="org.slf4j.api" version="1.7.30.v20200204-2150"/> <unit id="org.slf4j.api.source" version="1.7.30.v20200204-2150"/> - <unit id="org.slf4j.binding.log4j12" version="1.7.30.v20201108-2042"/> - <unit id="org.slf4j.binding.log4j12.source" version="1.7.30.v20201108-2042"/> + <unit id="org.slf4j.binding.simple" version="1.7.30.v20200204-2150"/> + <unit id="org.slf4j.binding.simple.source" version="1.7.30.v20200204-2150"/> <unit id="org.tukaani.xz" version="1.9.0.v20210624-1259"/> <unit id="org.tukaani.xz.source" version="1.9.0.v20210624-1259"/> - <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20211122181901/repository"/> + <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20220531185310/repository"/> </location> <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit"> <unit id="org.eclipse.osgi" version="0.0.0"/> diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.18.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.18.tpd index 27f3471c1a..1dcdd9bebb 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.18.tpd +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.18.tpd @@ -1,7 +1,7 @@ target "jgit-4.18" with source configurePhase include "projects/jetty-10.0.x.tpd" -include "orbit/R20211122181901-2021-12.tpd" +include "orbit/R20220531185310-2022-06.tpd" location "https://download.eclipse.org/releases/2020-12/" { org.eclipse.osgi lazy diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.19.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.19.target index ceff3052c3..cc418b1c8a 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.19.target +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.19.target @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="UTF-8" standalone="no"?> <?pde?> <!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl --> -<target name="jgit-4.19-staging" sequenceNumber="1638648728"> +<target name="jgit-4.19-staging" sequenceNumber="1654550632"> <locations> <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit"> <unit id="jakarta.servlet-api" version="4.0.0"/> @@ -23,12 +23,12 @@ <repository id="jetty-10.0.x" location="https://download.eclipse.org/eclipse/jetty/10.0.6/"/> </location> <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit"> - <unit id="com.google.gson" version="2.8.8.v20211029-0838"/> - <unit id="com.google.gson.source" version="2.8.8.v20211029-0838"/> + <unit id="com.google.gson" version="2.8.9.v20220111-1409"/> + <unit id="com.google.gson.source" version="2.8.9.v20220111-1409"/> <unit id="com.jcraft.jsch" version="0.1.55.v20190404-1902"/> <unit id="com.jcraft.jsch.source" version="0.1.55.v20190404-1902"/> - <unit id="com.jcraft.jzlib" version="1.1.1.v201205102305"/> - <unit id="com.jcraft.jzlib.source" version="1.1.1.v201205102305"/> + <unit id="com.jcraft.jzlib" version="1.1.3.v20220502-1820"/> + <unit id="com.jcraft.jzlib.source" version="1.1.3.v20220502-1820"/> <unit id="com.sun.jna" version="5.8.0.v20210503-0343"/> <unit id="com.sun.jna.source" version="5.8.0.v20210503-0343"/> <unit id="com.sun.jna.platform" version="5.8.0.v20210406-1004"/> @@ -39,8 +39,8 @@ <unit id="net.bytebuddy.byte-buddy-agent" version="1.9.0.v20181106-1534"/> <unit id="net.bytebuddy.byte-buddy-agent.source" version="1.9.0.v20181106-1534"/> <unit id="net.bytebuddy.byte-buddy.source" version="1.9.0.v20181107-1410"/> - <unit id="net.i2p.crypto.eddsa" version="0.3.0.v20210923-1401"/> - <unit id="net.i2p.crypto.eddsa.source" version="0.3.0.v20210923-1401"/> + <unit id="net.i2p.crypto.eddsa" version="0.3.0.v20220506-1020"/> + <unit id="net.i2p.crypto.eddsa.source" version="0.3.0.v20220506-1020"/> <unit id="org.apache.ant" version="1.10.12.v20211102-1452"/> <unit id="org.apache.ant.source" version="1.10.12.v20211102-1452"/> <unit id="org.apache.commons.codec" version="1.14.0.v20200818-1422"/> @@ -51,24 +51,22 @@ <unit id="org.apache.commons.logging.source" version="1.2.0.v20180409-1502"/> <unit id="org.apache.httpcomponents.httpclient" version="4.5.13.v20210128-2225"/> <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.13.v20210128-2225"/> - <unit id="org.apache.httpcomponents.httpcore" version="4.4.14.v20210128-2225"/> - <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.14.v20210128-2225"/> - <unit id="org.apache.log4j" version="1.2.15.v201012070815"/> - <unit id="org.apache.log4j.source" version="1.2.15.v201012070815"/> - <unit id="org.apache.sshd.osgi" version="2.7.0.v20210623-0618"/> - <unit id="org.apache.sshd.osgi.source" version="2.7.0.v20210623-0618"/> - <unit id="org.apache.sshd.sftp" version="2.7.0.v20210623-0618"/> - <unit id="org.apache.sshd.sftp.source" version="2.7.0.v20210623-0618"/> + <unit id="org.apache.httpcomponents.httpcore" version="4.4.15.v20220209-2345"/> + <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.15.v20220209-2345"/> + <unit id="org.apache.sshd.osgi" version="2.8.0.v20211227-1750"/> + <unit id="org.apache.sshd.osgi.source" version="2.8.0.v20211227-1750"/> + <unit id="org.apache.sshd.sftp" version="2.8.0.v20211227-1750"/> + <unit id="org.apache.sshd.sftp.source" version="2.8.0.v20211227-1750"/> <unit id="org.assertj" version="3.20.2.v20210706-1104"/> <unit id="org.assertj.source" version="3.20.2.v20210706-1104"/> - <unit id="org.bouncycastle.bcpg" version="1.69.0.v20210713-1924"/> - <unit id="org.bouncycastle.bcpg.source" version="1.69.0.v20210713-1924"/> - <unit id="org.bouncycastle.bcpkix" version="1.69.0.v20210713-1924"/> - <unit id="org.bouncycastle.bcpkix.source" version="1.69.0.v20210713-1924"/> - <unit id="org.bouncycastle.bcprov" version="1.69.0.v20210923-1401"/> - <unit id="org.bouncycastle.bcprov.source" version="1.69.0.v20210923-1401"/> - <unit id="org.bouncycastle.bcutil" version="1.69.0.v20210713-1924"/> - <unit id="org.bouncycastle.bcutil.source" version="1.69.0.v20210713-1924"/> + <unit id="org.bouncycastle.bcpg" version="1.70.0.v20220507-1208"/> + <unit id="org.bouncycastle.bcpg.source" version="1.70.0.v20220507-1208"/> + <unit id="org.bouncycastle.bcpkix" version="1.70.0.v20220105-1522"/> + <unit id="org.bouncycastle.bcpkix.source" version="1.70.0.v20220105-1522"/> + <unit id="org.bouncycastle.bcprov" version="1.70.0.v20220507-1208"/> + <unit id="org.bouncycastle.bcprov.source" version="1.70.0.v20220507-1208"/> + <unit id="org.bouncycastle.bcutil" version="1.70.0.v20220105-1522"/> + <unit id="org.bouncycastle.bcutil.source" version="1.70.0.v20220105-1522"/> <unit id="org.hamcrest" version="2.2.0.v20210711-0821"/> <unit id="org.hamcrest.source" version="2.2.0.v20210711-0821"/> <unit id="org.hamcrest.core" version="1.3.0.v20180420-1519"/> @@ -85,11 +83,11 @@ <unit id="org.objenesis.source" version="2.6.0.v20180420-1519"/> <unit id="org.slf4j.api" version="1.7.30.v20200204-2150"/> <unit id="org.slf4j.api.source" version="1.7.30.v20200204-2150"/> - <unit id="org.slf4j.binding.log4j12" version="1.7.30.v20201108-2042"/> - <unit id="org.slf4j.binding.log4j12.source" version="1.7.30.v20201108-2042"/> + <unit id="org.slf4j.binding.simple" version="1.7.30.v20200204-2150"/> + <unit id="org.slf4j.binding.simple.source" version="1.7.30.v20200204-2150"/> <unit id="org.tukaani.xz" version="1.9.0.v20210624-1259"/> <unit id="org.tukaani.xz.source" version="1.9.0.v20210624-1259"/> - <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20211122181901/repository"/> + <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20220531185310/repository"/> </location> <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit"> <unit id="org.eclipse.osgi" version="0.0.0"/> diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.19.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.19.tpd index 419999652d..806851c6e2 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.19.tpd +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.19.tpd @@ -1,7 +1,7 @@ target "jgit-4.19-staging" with source configurePhase include "projects/jetty-10.0.x.tpd" -include "orbit/R20211122181901-2021-12.tpd" +include "orbit/R20220531185310-2022-06.tpd" location "https://download.eclipse.org/staging/2021-03/" { org.eclipse.osgi lazy diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.20.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.20.target index e2fed0675a..efcb591efa 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.20.target +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.20.target @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="UTF-8" standalone="no"?> <?pde?> <!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl --> -<target name="jgit-4.20" sequenceNumber="1638648728"> +<target name="jgit-4.20" sequenceNumber="1654550634"> <locations> <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit"> <unit id="jakarta.servlet-api" version="4.0.0"/> @@ -23,12 +23,12 @@ <repository id="jetty-10.0.x" location="https://download.eclipse.org/eclipse/jetty/10.0.6/"/> </location> <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit"> - <unit id="com.google.gson" version="2.8.8.v20211029-0838"/> - <unit id="com.google.gson.source" version="2.8.8.v20211029-0838"/> + <unit id="com.google.gson" version="2.8.9.v20220111-1409"/> + <unit id="com.google.gson.source" version="2.8.9.v20220111-1409"/> <unit id="com.jcraft.jsch" version="0.1.55.v20190404-1902"/> <unit id="com.jcraft.jsch.source" version="0.1.55.v20190404-1902"/> - <unit id="com.jcraft.jzlib" version="1.1.1.v201205102305"/> - <unit id="com.jcraft.jzlib.source" version="1.1.1.v201205102305"/> + <unit id="com.jcraft.jzlib" version="1.1.3.v20220502-1820"/> + <unit id="com.jcraft.jzlib.source" version="1.1.3.v20220502-1820"/> <unit id="com.sun.jna" version="5.8.0.v20210503-0343"/> <unit id="com.sun.jna.source" version="5.8.0.v20210503-0343"/> <unit id="com.sun.jna.platform" version="5.8.0.v20210406-1004"/> @@ -39,8 +39,8 @@ <unit id="net.bytebuddy.byte-buddy-agent" version="1.9.0.v20181106-1534"/> <unit id="net.bytebuddy.byte-buddy-agent.source" version="1.9.0.v20181106-1534"/> <unit id="net.bytebuddy.byte-buddy.source" version="1.9.0.v20181107-1410"/> - <unit id="net.i2p.crypto.eddsa" version="0.3.0.v20210923-1401"/> - <unit id="net.i2p.crypto.eddsa.source" version="0.3.0.v20210923-1401"/> + <unit id="net.i2p.crypto.eddsa" version="0.3.0.v20220506-1020"/> + <unit id="net.i2p.crypto.eddsa.source" version="0.3.0.v20220506-1020"/> <unit id="org.apache.ant" version="1.10.12.v20211102-1452"/> <unit id="org.apache.ant.source" version="1.10.12.v20211102-1452"/> <unit id="org.apache.commons.codec" version="1.14.0.v20200818-1422"/> @@ -51,24 +51,22 @@ <unit id="org.apache.commons.logging.source" version="1.2.0.v20180409-1502"/> <unit id="org.apache.httpcomponents.httpclient" version="4.5.13.v20210128-2225"/> <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.13.v20210128-2225"/> - <unit id="org.apache.httpcomponents.httpcore" version="4.4.14.v20210128-2225"/> - <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.14.v20210128-2225"/> - <unit id="org.apache.log4j" version="1.2.15.v201012070815"/> - <unit id="org.apache.log4j.source" version="1.2.15.v201012070815"/> - <unit id="org.apache.sshd.osgi" version="2.7.0.v20210623-0618"/> - <unit id="org.apache.sshd.osgi.source" version="2.7.0.v20210623-0618"/> - <unit id="org.apache.sshd.sftp" version="2.7.0.v20210623-0618"/> - <unit id="org.apache.sshd.sftp.source" version="2.7.0.v20210623-0618"/> + <unit id="org.apache.httpcomponents.httpcore" version="4.4.15.v20220209-2345"/> + <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.15.v20220209-2345"/> + <unit id="org.apache.sshd.osgi" version="2.8.0.v20211227-1750"/> + <unit id="org.apache.sshd.osgi.source" version="2.8.0.v20211227-1750"/> + <unit id="org.apache.sshd.sftp" version="2.8.0.v20211227-1750"/> + <unit id="org.apache.sshd.sftp.source" version="2.8.0.v20211227-1750"/> <unit id="org.assertj" version="3.20.2.v20210706-1104"/> <unit id="org.assertj.source" version="3.20.2.v20210706-1104"/> - <unit id="org.bouncycastle.bcpg" version="1.69.0.v20210713-1924"/> - <unit id="org.bouncycastle.bcpg.source" version="1.69.0.v20210713-1924"/> - <unit id="org.bouncycastle.bcpkix" version="1.69.0.v20210713-1924"/> - <unit id="org.bouncycastle.bcpkix.source" version="1.69.0.v20210713-1924"/> - <unit id="org.bouncycastle.bcprov" version="1.69.0.v20210923-1401"/> - <unit id="org.bouncycastle.bcprov.source" version="1.69.0.v20210923-1401"/> - <unit id="org.bouncycastle.bcutil" version="1.69.0.v20210713-1924"/> - <unit id="org.bouncycastle.bcutil.source" version="1.69.0.v20210713-1924"/> + <unit id="org.bouncycastle.bcpg" version="1.70.0.v20220507-1208"/> + <unit id="org.bouncycastle.bcpg.source" version="1.70.0.v20220507-1208"/> + <unit id="org.bouncycastle.bcpkix" version="1.70.0.v20220105-1522"/> + <unit id="org.bouncycastle.bcpkix.source" version="1.70.0.v20220105-1522"/> + <unit id="org.bouncycastle.bcprov" version="1.70.0.v20220507-1208"/> + <unit id="org.bouncycastle.bcprov.source" version="1.70.0.v20220507-1208"/> + <unit id="org.bouncycastle.bcutil" version="1.70.0.v20220105-1522"/> + <unit id="org.bouncycastle.bcutil.source" version="1.70.0.v20220105-1522"/> <unit id="org.hamcrest" version="2.2.0.v20210711-0821"/> <unit id="org.hamcrest.source" version="2.2.0.v20210711-0821"/> <unit id="org.hamcrest.core" version="1.3.0.v20180420-1519"/> @@ -85,11 +83,11 @@ <unit id="org.objenesis.source" version="2.6.0.v20180420-1519"/> <unit id="org.slf4j.api" version="1.7.30.v20200204-2150"/> <unit id="org.slf4j.api.source" version="1.7.30.v20200204-2150"/> - <unit id="org.slf4j.binding.log4j12" version="1.7.30.v20201108-2042"/> - <unit id="org.slf4j.binding.log4j12.source" version="1.7.30.v20201108-2042"/> + <unit id="org.slf4j.binding.simple" version="1.7.30.v20200204-2150"/> + <unit id="org.slf4j.binding.simple.source" version="1.7.30.v20200204-2150"/> <unit id="org.tukaani.xz" version="1.9.0.v20210624-1259"/> <unit id="org.tukaani.xz.source" version="1.9.0.v20210624-1259"/> - <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20211122181901/repository"/> + <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20220531185310/repository"/> </location> <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit"> <unit id="org.eclipse.osgi" version="0.0.0"/> diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.20.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.20.tpd index 37123e5fa5..a3ea58300e 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.20.tpd +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.20.tpd @@ -1,7 +1,7 @@ target "jgit-4.20" with source configurePhase include "projects/jetty-10.0.x.tpd" -include "orbit/R20211122181901-2021-12.tpd" +include "orbit/R20220531185310-2022-06.tpd" location "https://download.eclipse.org/releases/2021-06/" { org.eclipse.osgi lazy diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.21.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.21.target index 4bd55466ec..85ed31f441 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.21.target +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.21.target @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="UTF-8" standalone="no"?> <?pde?> <!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl --> -<target name="jgit-4.21" sequenceNumber="1638648728"> +<target name="jgit-4.21" sequenceNumber="1654550635"> <locations> <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit"> <unit id="jakarta.servlet-api" version="4.0.0"/> @@ -23,12 +23,12 @@ <repository id="jetty-10.0.x" location="https://download.eclipse.org/eclipse/jetty/10.0.6/"/> </location> <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit"> - <unit id="com.google.gson" version="2.8.8.v20211029-0838"/> - <unit id="com.google.gson.source" version="2.8.8.v20211029-0838"/> + <unit id="com.google.gson" version="2.8.9.v20220111-1409"/> + <unit id="com.google.gson.source" version="2.8.9.v20220111-1409"/> <unit id="com.jcraft.jsch" version="0.1.55.v20190404-1902"/> <unit id="com.jcraft.jsch.source" version="0.1.55.v20190404-1902"/> - <unit id="com.jcraft.jzlib" version="1.1.1.v201205102305"/> - <unit id="com.jcraft.jzlib.source" version="1.1.1.v201205102305"/> + <unit id="com.jcraft.jzlib" version="1.1.3.v20220502-1820"/> + <unit id="com.jcraft.jzlib.source" version="1.1.3.v20220502-1820"/> <unit id="com.sun.jna" version="5.8.0.v20210503-0343"/> <unit id="com.sun.jna.source" version="5.8.0.v20210503-0343"/> <unit id="com.sun.jna.platform" version="5.8.0.v20210406-1004"/> @@ -39,8 +39,8 @@ <unit id="net.bytebuddy.byte-buddy-agent" version="1.9.0.v20181106-1534"/> <unit id="net.bytebuddy.byte-buddy-agent.source" version="1.9.0.v20181106-1534"/> <unit id="net.bytebuddy.byte-buddy.source" version="1.9.0.v20181107-1410"/> - <unit id="net.i2p.crypto.eddsa" version="0.3.0.v20210923-1401"/> - <unit id="net.i2p.crypto.eddsa.source" version="0.3.0.v20210923-1401"/> + <unit id="net.i2p.crypto.eddsa" version="0.3.0.v20220506-1020"/> + <unit id="net.i2p.crypto.eddsa.source" version="0.3.0.v20220506-1020"/> <unit id="org.apache.ant" version="1.10.12.v20211102-1452"/> <unit id="org.apache.ant.source" version="1.10.12.v20211102-1452"/> <unit id="org.apache.commons.codec" version="1.14.0.v20200818-1422"/> @@ -51,24 +51,22 @@ <unit id="org.apache.commons.logging.source" version="1.2.0.v20180409-1502"/> <unit id="org.apache.httpcomponents.httpclient" version="4.5.13.v20210128-2225"/> <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.13.v20210128-2225"/> - <unit id="org.apache.httpcomponents.httpcore" version="4.4.14.v20210128-2225"/> - <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.14.v20210128-2225"/> - <unit id="org.apache.log4j" version="1.2.15.v201012070815"/> - <unit id="org.apache.log4j.source" version="1.2.15.v201012070815"/> - <unit id="org.apache.sshd.osgi" version="2.7.0.v20210623-0618"/> - <unit id="org.apache.sshd.osgi.source" version="2.7.0.v20210623-0618"/> - <unit id="org.apache.sshd.sftp" version="2.7.0.v20210623-0618"/> - <unit id="org.apache.sshd.sftp.source" version="2.7.0.v20210623-0618"/> + <unit id="org.apache.httpcomponents.httpcore" version="4.4.15.v20220209-2345"/> + <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.15.v20220209-2345"/> + <unit id="org.apache.sshd.osgi" version="2.8.0.v20211227-1750"/> + <unit id="org.apache.sshd.osgi.source" version="2.8.0.v20211227-1750"/> + <unit id="org.apache.sshd.sftp" version="2.8.0.v20211227-1750"/> + <unit id="org.apache.sshd.sftp.source" version="2.8.0.v20211227-1750"/> <unit id="org.assertj" version="3.20.2.v20210706-1104"/> <unit id="org.assertj.source" version="3.20.2.v20210706-1104"/> - <unit id="org.bouncycastle.bcpg" version="1.69.0.v20210713-1924"/> - <unit id="org.bouncycastle.bcpg.source" version="1.69.0.v20210713-1924"/> - <unit id="org.bouncycastle.bcpkix" version="1.69.0.v20210713-1924"/> - <unit id="org.bouncycastle.bcpkix.source" version="1.69.0.v20210713-1924"/> - <unit id="org.bouncycastle.bcprov" version="1.69.0.v20210923-1401"/> - <unit id="org.bouncycastle.bcprov.source" version="1.69.0.v20210923-1401"/> - <unit id="org.bouncycastle.bcutil" version="1.69.0.v20210713-1924"/> - <unit id="org.bouncycastle.bcutil.source" version="1.69.0.v20210713-1924"/> + <unit id="org.bouncycastle.bcpg" version="1.70.0.v20220507-1208"/> + <unit id="org.bouncycastle.bcpg.source" version="1.70.0.v20220507-1208"/> + <unit id="org.bouncycastle.bcpkix" version="1.70.0.v20220105-1522"/> + <unit id="org.bouncycastle.bcpkix.source" version="1.70.0.v20220105-1522"/> + <unit id="org.bouncycastle.bcprov" version="1.70.0.v20220507-1208"/> + <unit id="org.bouncycastle.bcprov.source" version="1.70.0.v20220507-1208"/> + <unit id="org.bouncycastle.bcutil" version="1.70.0.v20220105-1522"/> + <unit id="org.bouncycastle.bcutil.source" version="1.70.0.v20220105-1522"/> <unit id="org.hamcrest" version="2.2.0.v20210711-0821"/> <unit id="org.hamcrest.source" version="2.2.0.v20210711-0821"/> <unit id="org.hamcrest.core" version="1.3.0.v20180420-1519"/> @@ -85,11 +83,11 @@ <unit id="org.objenesis.source" version="2.6.0.v20180420-1519"/> <unit id="org.slf4j.api" version="1.7.30.v20200204-2150"/> <unit id="org.slf4j.api.source" version="1.7.30.v20200204-2150"/> - <unit id="org.slf4j.binding.log4j12" version="1.7.30.v20201108-2042"/> - <unit id="org.slf4j.binding.log4j12.source" version="1.7.30.v20201108-2042"/> + <unit id="org.slf4j.binding.simple" version="1.7.30.v20200204-2150"/> + <unit id="org.slf4j.binding.simple.source" version="1.7.30.v20200204-2150"/> <unit id="org.tukaani.xz" version="1.9.0.v20210624-1259"/> <unit id="org.tukaani.xz.source" version="1.9.0.v20210624-1259"/> - <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20211122181901/repository"/> + <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20220531185310/repository"/> </location> <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit"> <unit id="org.eclipse.osgi" version="0.0.0"/> diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.21.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.21.tpd index 2949e50a2d..0808601826 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.21.tpd +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.21.tpd @@ -1,7 +1,7 @@ target "jgit-4.21" with source configurePhase include "projects/jetty-10.0.x.tpd" -include "orbit/R20211122181901-2021-12.tpd" +include "orbit/R20220531185310-2022-06.tpd" location "https://download.eclipse.org/releases/2021-09/" { org.eclipse.osgi lazy diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.22.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.22.target index 98f44a8dc2..e7b5a31992 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.22.target +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.22.target @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="UTF-8" standalone="no"?> <?pde?> <!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl --> -<target name="jgit-4.22" sequenceNumber="1638648736"> +<target name="jgit-4.22" sequenceNumber="1654550634"> <locations> <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit"> <unit id="jakarta.servlet-api" version="4.0.0"/> @@ -23,12 +23,12 @@ <repository id="jetty-10.0.x" location="https://download.eclipse.org/eclipse/jetty/10.0.6/"/> </location> <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit"> - <unit id="com.google.gson" version="2.8.8.v20211029-0838"/> - <unit id="com.google.gson.source" version="2.8.8.v20211029-0838"/> + <unit id="com.google.gson" version="2.8.9.v20220111-1409"/> + <unit id="com.google.gson.source" version="2.8.9.v20220111-1409"/> <unit id="com.jcraft.jsch" version="0.1.55.v20190404-1902"/> <unit id="com.jcraft.jsch.source" version="0.1.55.v20190404-1902"/> - <unit id="com.jcraft.jzlib" version="1.1.1.v201205102305"/> - <unit id="com.jcraft.jzlib.source" version="1.1.1.v201205102305"/> + <unit id="com.jcraft.jzlib" version="1.1.3.v20220502-1820"/> + <unit id="com.jcraft.jzlib.source" version="1.1.3.v20220502-1820"/> <unit id="com.sun.jna" version="5.8.0.v20210503-0343"/> <unit id="com.sun.jna.source" version="5.8.0.v20210503-0343"/> <unit id="com.sun.jna.platform" version="5.8.0.v20210406-1004"/> @@ -39,8 +39,8 @@ <unit id="net.bytebuddy.byte-buddy-agent" version="1.9.0.v20181106-1534"/> <unit id="net.bytebuddy.byte-buddy-agent.source" version="1.9.0.v20181106-1534"/> <unit id="net.bytebuddy.byte-buddy.source" version="1.9.0.v20181107-1410"/> - <unit id="net.i2p.crypto.eddsa" version="0.3.0.v20210923-1401"/> - <unit id="net.i2p.crypto.eddsa.source" version="0.3.0.v20210923-1401"/> + <unit id="net.i2p.crypto.eddsa" version="0.3.0.v20220506-1020"/> + <unit id="net.i2p.crypto.eddsa.source" version="0.3.0.v20220506-1020"/> <unit id="org.apache.ant" version="1.10.12.v20211102-1452"/> <unit id="org.apache.ant.source" version="1.10.12.v20211102-1452"/> <unit id="org.apache.commons.codec" version="1.14.0.v20200818-1422"/> @@ -51,24 +51,22 @@ <unit id="org.apache.commons.logging.source" version="1.2.0.v20180409-1502"/> <unit id="org.apache.httpcomponents.httpclient" version="4.5.13.v20210128-2225"/> <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.13.v20210128-2225"/> - <unit id="org.apache.httpcomponents.httpcore" version="4.4.14.v20210128-2225"/> - <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.14.v20210128-2225"/> - <unit id="org.apache.log4j" version="1.2.15.v201012070815"/> - <unit id="org.apache.log4j.source" version="1.2.15.v201012070815"/> - <unit id="org.apache.sshd.osgi" version="2.7.0.v20210623-0618"/> - <unit id="org.apache.sshd.osgi.source" version="2.7.0.v20210623-0618"/> - <unit id="org.apache.sshd.sftp" version="2.7.0.v20210623-0618"/> - <unit id="org.apache.sshd.sftp.source" version="2.7.0.v20210623-0618"/> + <unit id="org.apache.httpcomponents.httpcore" version="4.4.15.v20220209-2345"/> + <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.15.v20220209-2345"/> + <unit id="org.apache.sshd.osgi" version="2.8.0.v20211227-1750"/> + <unit id="org.apache.sshd.osgi.source" version="2.8.0.v20211227-1750"/> + <unit id="org.apache.sshd.sftp" version="2.8.0.v20211227-1750"/> + <unit id="org.apache.sshd.sftp.source" version="2.8.0.v20211227-1750"/> <unit id="org.assertj" version="3.20.2.v20210706-1104"/> <unit id="org.assertj.source" version="3.20.2.v20210706-1104"/> - <unit id="org.bouncycastle.bcpg" version="1.69.0.v20210713-1924"/> - <unit id="org.bouncycastle.bcpg.source" version="1.69.0.v20210713-1924"/> - <unit id="org.bouncycastle.bcpkix" version="1.69.0.v20210713-1924"/> - <unit id="org.bouncycastle.bcpkix.source" version="1.69.0.v20210713-1924"/> - <unit id="org.bouncycastle.bcprov" version="1.69.0.v20210923-1401"/> - <unit id="org.bouncycastle.bcprov.source" version="1.69.0.v20210923-1401"/> - <unit id="org.bouncycastle.bcutil" version="1.69.0.v20210713-1924"/> - <unit id="org.bouncycastle.bcutil.source" version="1.69.0.v20210713-1924"/> + <unit id="org.bouncycastle.bcpg" version="1.70.0.v20220507-1208"/> + <unit id="org.bouncycastle.bcpg.source" version="1.70.0.v20220507-1208"/> + <unit id="org.bouncycastle.bcpkix" version="1.70.0.v20220105-1522"/> + <unit id="org.bouncycastle.bcpkix.source" version="1.70.0.v20220105-1522"/> + <unit id="org.bouncycastle.bcprov" version="1.70.0.v20220507-1208"/> + <unit id="org.bouncycastle.bcprov.source" version="1.70.0.v20220507-1208"/> + <unit id="org.bouncycastle.bcutil" version="1.70.0.v20220105-1522"/> + <unit id="org.bouncycastle.bcutil.source" version="1.70.0.v20220105-1522"/> <unit id="org.hamcrest" version="2.2.0.v20210711-0821"/> <unit id="org.hamcrest.source" version="2.2.0.v20210711-0821"/> <unit id="org.hamcrest.core" version="1.3.0.v20180420-1519"/> @@ -85,11 +83,11 @@ <unit id="org.objenesis.source" version="2.6.0.v20180420-1519"/> <unit id="org.slf4j.api" version="1.7.30.v20200204-2150"/> <unit id="org.slf4j.api.source" version="1.7.30.v20200204-2150"/> - <unit id="org.slf4j.binding.log4j12" version="1.7.30.v20201108-2042"/> - <unit id="org.slf4j.binding.log4j12.source" version="1.7.30.v20201108-2042"/> + <unit id="org.slf4j.binding.simple" version="1.7.30.v20200204-2150"/> + <unit id="org.slf4j.binding.simple.source" version="1.7.30.v20200204-2150"/> <unit id="org.tukaani.xz" version="1.9.0.v20210624-1259"/> <unit id="org.tukaani.xz.source" version="1.9.0.v20210624-1259"/> - <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20211122181901/repository"/> + <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20220531185310/repository"/> </location> <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit"> <unit id="org.eclipse.osgi" version="0.0.0"/> diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.22.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.22.tpd index db3db28773..5697574d3f 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.22.tpd +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.22.tpd @@ -1,7 +1,7 @@ target "jgit-4.22" with source configurePhase include "projects/jetty-10.0.x.tpd" -include "orbit/R20211122181901-2021-12.tpd" +include "orbit/R20220531185310-2022-06.tpd" location "https://download.eclipse.org/releases/2021-12/" { org.eclipse.osgi lazy diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.23.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.23.target new file mode 100644 index 0000000000..3c18e4b75c --- /dev/null +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.23.target @@ -0,0 +1,97 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<?pde?> +<!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl --> +<target name="jgit-4.23" sequenceNumber="1654550634"> + <locations> + <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit"> + <unit id="jakarta.servlet-api" version="4.0.0"/> + <unit id="jakarta.servlet-api.source" version="4.0.0"/> + <unit id="org.eclipse.jetty.http" version="10.0.6"/> + <unit id="org.eclipse.jetty.http.source" version="10.0.6"/> + <unit id="org.eclipse.jetty.io" version="10.0.6"/> + <unit id="org.eclipse.jetty.io.source" version="10.0.6"/> + <unit id="org.eclipse.jetty.security" version="10.0.6"/> + <unit id="org.eclipse.jetty.security.source" version="10.0.6"/> + <unit id="org.eclipse.jetty.server" version="10.0.6"/> + <unit id="org.eclipse.jetty.server.source" version="10.0.6"/> + <unit id="org.eclipse.jetty.servlet" version="10.0.6"/> + <unit id="org.eclipse.jetty.servlet.source" version="10.0.6"/> + <unit id="org.eclipse.jetty.util" version="10.0.6"/> + <unit id="org.eclipse.jetty.util.source" version="10.0.6"/> + <unit id="org.eclipse.jetty.util.ajax" version="10.0.6"/> + <unit id="org.eclipse.jetty.util.ajax.source" version="10.0.6"/> + <repository id="jetty-10.0.x" location="https://download.eclipse.org/eclipse/jetty/10.0.6/"/> + </location> + <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit"> + <unit id="com.google.gson" version="2.8.9.v20220111-1409"/> + <unit id="com.google.gson.source" version="2.8.9.v20220111-1409"/> + <unit id="com.jcraft.jsch" version="0.1.55.v20190404-1902"/> + <unit id="com.jcraft.jsch.source" version="0.1.55.v20190404-1902"/> + <unit id="com.jcraft.jzlib" version="1.1.3.v20220502-1820"/> + <unit id="com.jcraft.jzlib.source" version="1.1.3.v20220502-1820"/> + <unit id="com.sun.jna" version="5.8.0.v20210503-0343"/> + <unit id="com.sun.jna.source" version="5.8.0.v20210503-0343"/> + <unit id="com.sun.jna.platform" version="5.8.0.v20210406-1004"/> + <unit id="com.sun.jna.platform.source" version="5.8.0.v20210406-1004"/> + <unit id="javaewah" version="1.1.13.v20211029-0839"/> + <unit id="javaewah.source" version="1.1.13.v20211029-0839"/> + <unit id="net.bytebuddy.byte-buddy" version="1.9.0.v20181107-1410"/> + <unit id="net.bytebuddy.byte-buddy-agent" version="1.9.0.v20181106-1534"/> + <unit id="net.bytebuddy.byte-buddy-agent.source" version="1.9.0.v20181106-1534"/> + <unit id="net.bytebuddy.byte-buddy.source" version="1.9.0.v20181107-1410"/> + <unit id="net.i2p.crypto.eddsa" version="0.3.0.v20220506-1020"/> + <unit id="net.i2p.crypto.eddsa.source" version="0.3.0.v20220506-1020"/> + <unit id="org.apache.ant" version="1.10.12.v20211102-1452"/> + <unit id="org.apache.ant.source" version="1.10.12.v20211102-1452"/> + <unit id="org.apache.commons.codec" version="1.14.0.v20200818-1422"/> + <unit id="org.apache.commons.codec.source" version="1.14.0.v20200818-1422"/> + <unit id="org.apache.commons.compress" version="1.21.0.v20211103-2100"/> + <unit id="org.apache.commons.compress.source" version="1.21.0.v20211103-2100"/> + <unit id="org.apache.commons.logging" version="1.2.0.v20180409-1502"/> + <unit id="org.apache.commons.logging.source" version="1.2.0.v20180409-1502"/> + <unit id="org.apache.httpcomponents.httpclient" version="4.5.13.v20210128-2225"/> + <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.13.v20210128-2225"/> + <unit id="org.apache.httpcomponents.httpcore" version="4.4.15.v20220209-2345"/> + <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.15.v20220209-2345"/> + <unit id="org.apache.sshd.osgi" version="2.8.0.v20211227-1750"/> + <unit id="org.apache.sshd.osgi.source" version="2.8.0.v20211227-1750"/> + <unit id="org.apache.sshd.sftp" version="2.8.0.v20211227-1750"/> + <unit id="org.apache.sshd.sftp.source" version="2.8.0.v20211227-1750"/> + <unit id="org.assertj" version="3.20.2.v20210706-1104"/> + <unit id="org.assertj.source" version="3.20.2.v20210706-1104"/> + <unit id="org.bouncycastle.bcpg" version="1.70.0.v20220507-1208"/> + <unit id="org.bouncycastle.bcpg.source" version="1.70.0.v20220507-1208"/> + <unit id="org.bouncycastle.bcpkix" version="1.70.0.v20220105-1522"/> + <unit id="org.bouncycastle.bcpkix.source" version="1.70.0.v20220105-1522"/> + <unit id="org.bouncycastle.bcprov" version="1.70.0.v20220507-1208"/> + <unit id="org.bouncycastle.bcprov.source" version="1.70.0.v20220507-1208"/> + <unit id="org.bouncycastle.bcutil" version="1.70.0.v20220105-1522"/> + <unit id="org.bouncycastle.bcutil.source" version="1.70.0.v20220105-1522"/> + <unit id="org.hamcrest" version="2.2.0.v20210711-0821"/> + <unit id="org.hamcrest.source" version="2.2.0.v20210711-0821"/> + <unit id="org.hamcrest.core" version="1.3.0.v20180420-1519"/> + <unit id="org.hamcrest.core.source" version="1.3.0.v20180420-1519"/> + <unit id="org.hamcrest.library" version="1.3.0.v20180524-2246"/> + <unit id="org.hamcrest.library.source" version="1.3.0.v20180524-2246"/> + <unit id="org.junit" version="4.13.2.v20211018-1956"/> + <unit id="org.junit.source" version="4.13.2.v20211018-1956"/> + <unit id="org.kohsuke.args4j" version="2.33.0.v20160323-2218"/> + <unit id="org.kohsuke.args4j.source" version="2.33.0.v20160323-2218"/> + <unit id="org.mockito" version="2.23.0.v20200310-1642"/> + <unit id="org.mockito.source" version="2.23.0.v20200310-1642"/> + <unit id="org.objenesis" version="2.6.0.v20180420-1519"/> + <unit id="org.objenesis.source" version="2.6.0.v20180420-1519"/> + <unit id="org.slf4j.api" version="1.7.30.v20200204-2150"/> + <unit id="org.slf4j.api.source" version="1.7.30.v20200204-2150"/> + <unit id="org.slf4j.binding.simple" version="1.7.30.v20200204-2150"/> + <unit id="org.slf4j.binding.simple.source" version="1.7.30.v20200204-2150"/> + <unit id="org.tukaani.xz" version="1.9.0.v20210624-1259"/> + <unit id="org.tukaani.xz.source" version="1.9.0.v20210624-1259"/> + <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20220531185310/repository"/> + </location> + <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit"> + <unit id="org.eclipse.osgi" version="0.0.0"/> + <repository location="https://download.eclipse.org/releases/2022-03/"/> + </location> + </locations> +</target> diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.23.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.23.tpd new file mode 100644 index 0000000000..7fd421a882 --- /dev/null +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.23.tpd @@ -0,0 +1,8 @@ +target "jgit-4.23" with source configurePhase + +include "projects/jetty-10.0.x.tpd" +include "orbit/R20220531185310-2022-06.tpd" + +location "https://download.eclipse.org/releases/2022-03/" { + org.eclipse.osgi lazy +} diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.24.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.24.target new file mode 100644 index 0000000000..bd45e85e78 --- /dev/null +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.24.target @@ -0,0 +1,97 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<?pde?> +<!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl --> +<target name="jgit-4.24" sequenceNumber="1655375254"> + <locations> + <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit"> + <unit id="jakarta.servlet-api" version="4.0.0"/> + <unit id="jakarta.servlet-api.source" version="4.0.0"/> + <unit id="org.eclipse.jetty.http" version="10.0.6"/> + <unit id="org.eclipse.jetty.http.source" version="10.0.6"/> + <unit id="org.eclipse.jetty.io" version="10.0.6"/> + <unit id="org.eclipse.jetty.io.source" version="10.0.6"/> + <unit id="org.eclipse.jetty.security" version="10.0.6"/> + <unit id="org.eclipse.jetty.security.source" version="10.0.6"/> + <unit id="org.eclipse.jetty.server" version="10.0.6"/> + <unit id="org.eclipse.jetty.server.source" version="10.0.6"/> + <unit id="org.eclipse.jetty.servlet" version="10.0.6"/> + <unit id="org.eclipse.jetty.servlet.source" version="10.0.6"/> + <unit id="org.eclipse.jetty.util" version="10.0.6"/> + <unit id="org.eclipse.jetty.util.source" version="10.0.6"/> + <unit id="org.eclipse.jetty.util.ajax" version="10.0.6"/> + <unit id="org.eclipse.jetty.util.ajax.source" version="10.0.6"/> + <repository id="jetty-10.0.x" location="https://download.eclipse.org/eclipse/jetty/10.0.6/"/> + </location> + <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit"> + <unit id="com.google.gson" version="2.8.9.v20220111-1409"/> + <unit id="com.google.gson.source" version="2.8.9.v20220111-1409"/> + <unit id="com.jcraft.jsch" version="0.1.55.v20190404-1902"/> + <unit id="com.jcraft.jsch.source" version="0.1.55.v20190404-1902"/> + <unit id="com.jcraft.jzlib" version="1.1.3.v20220502-1820"/> + <unit id="com.jcraft.jzlib.source" version="1.1.3.v20220502-1820"/> + <unit id="com.sun.jna" version="5.8.0.v20210503-0343"/> + <unit id="com.sun.jna.source" version="5.8.0.v20210503-0343"/> + <unit id="com.sun.jna.platform" version="5.8.0.v20210406-1004"/> + <unit id="com.sun.jna.platform.source" version="5.8.0.v20210406-1004"/> + <unit id="javaewah" version="1.1.13.v20211029-0839"/> + <unit id="javaewah.source" version="1.1.13.v20211029-0839"/> + <unit id="net.bytebuddy.byte-buddy" version="1.9.0.v20181107-1410"/> + <unit id="net.bytebuddy.byte-buddy-agent" version="1.9.0.v20181106-1534"/> + <unit id="net.bytebuddy.byte-buddy-agent.source" version="1.9.0.v20181106-1534"/> + <unit id="net.bytebuddy.byte-buddy.source" version="1.9.0.v20181107-1410"/> + <unit id="net.i2p.crypto.eddsa" version="0.3.0.v20220506-1020"/> + <unit id="net.i2p.crypto.eddsa.source" version="0.3.0.v20220506-1020"/> + <unit id="org.apache.ant" version="1.10.12.v20211102-1452"/> + <unit id="org.apache.ant.source" version="1.10.12.v20211102-1452"/> + <unit id="org.apache.commons.codec" version="1.14.0.v20200818-1422"/> + <unit id="org.apache.commons.codec.source" version="1.14.0.v20200818-1422"/> + <unit id="org.apache.commons.compress" version="1.21.0.v20211103-2100"/> + <unit id="org.apache.commons.compress.source" version="1.21.0.v20211103-2100"/> + <unit id="org.apache.commons.logging" version="1.2.0.v20180409-1502"/> + <unit id="org.apache.commons.logging.source" version="1.2.0.v20180409-1502"/> + <unit id="org.apache.httpcomponents.httpclient" version="4.5.13.v20210128-2225"/> + <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.13.v20210128-2225"/> + <unit id="org.apache.httpcomponents.httpcore" version="4.4.15.v20220209-2345"/> + <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.15.v20220209-2345"/> + <unit id="org.apache.sshd.osgi" version="2.8.0.v20211227-1750"/> + <unit id="org.apache.sshd.osgi.source" version="2.8.0.v20211227-1750"/> + <unit id="org.apache.sshd.sftp" version="2.8.0.v20211227-1750"/> + <unit id="org.apache.sshd.sftp.source" version="2.8.0.v20211227-1750"/> + <unit id="org.assertj" version="3.20.2.v20210706-1104"/> + <unit id="org.assertj.source" version="3.20.2.v20210706-1104"/> + <unit id="org.bouncycastle.bcpg" version="1.70.0.v20220507-1208"/> + <unit id="org.bouncycastle.bcpg.source" version="1.70.0.v20220507-1208"/> + <unit id="org.bouncycastle.bcpkix" version="1.70.0.v20220105-1522"/> + <unit id="org.bouncycastle.bcpkix.source" version="1.70.0.v20220105-1522"/> + <unit id="org.bouncycastle.bcprov" version="1.70.0.v20220507-1208"/> + <unit id="org.bouncycastle.bcprov.source" version="1.70.0.v20220507-1208"/> + <unit id="org.bouncycastle.bcutil" version="1.70.0.v20220105-1522"/> + <unit id="org.bouncycastle.bcutil.source" version="1.70.0.v20220105-1522"/> + <unit id="org.hamcrest" version="2.2.0.v20210711-0821"/> + <unit id="org.hamcrest.source" version="2.2.0.v20210711-0821"/> + <unit id="org.hamcrest.core" version="1.3.0.v20180420-1519"/> + <unit id="org.hamcrest.core.source" version="1.3.0.v20180420-1519"/> + <unit id="org.hamcrest.library" version="1.3.0.v20180524-2246"/> + <unit id="org.hamcrest.library.source" version="1.3.0.v20180524-2246"/> + <unit id="org.junit" version="4.13.2.v20211018-1956"/> + <unit id="org.junit.source" version="4.13.2.v20211018-1956"/> + <unit id="org.kohsuke.args4j" version="2.33.0.v20160323-2218"/> + <unit id="org.kohsuke.args4j.source" version="2.33.0.v20160323-2218"/> + <unit id="org.mockito" version="2.23.0.v20200310-1642"/> + <unit id="org.mockito.source" version="2.23.0.v20200310-1642"/> + <unit id="org.objenesis" version="2.6.0.v20180420-1519"/> + <unit id="org.objenesis.source" version="2.6.0.v20180420-1519"/> + <unit id="org.slf4j.api" version="1.7.30.v20200204-2150"/> + <unit id="org.slf4j.api.source" version="1.7.30.v20200204-2150"/> + <unit id="org.slf4j.binding.simple" version="1.7.30.v20200204-2150"/> + <unit id="org.slf4j.binding.simple.source" version="1.7.30.v20200204-2150"/> + <unit id="org.tukaani.xz" version="1.9.0.v20210624-1259"/> + <unit id="org.tukaani.xz.source" version="1.9.0.v20210624-1259"/> + <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20220531185310/repository"/> + </location> + <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit"> + <unit id="org.eclipse.osgi" version="0.0.0"/> + <repository location="https://download.eclipse.org/releases/2022-06/"/> + </location> + </locations> +</target> diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.24.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.24.tpd new file mode 100644 index 0000000000..f258adc2d4 --- /dev/null +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.24.tpd @@ -0,0 +1,8 @@ +target "jgit-4.24" with source configurePhase + +include "projects/jetty-10.0.x.tpd" +include "orbit/R20220531185310-2022-06.tpd" + +location "https://download.eclipse.org/releases/2022-06/" { + org.eclipse.osgi lazy +} diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.25.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.25.target new file mode 100644 index 0000000000..0ec9d1024f --- /dev/null +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.25.target @@ -0,0 +1,97 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<?pde?> +<!-- generated with https://github.com/eclipse-cbi/targetplatform-dsl --> +<target name="jgit-4.25" sequenceNumber="1655454378"> + <locations> + <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit"> + <unit id="jakarta.servlet-api" version="4.0.0"/> + <unit id="jakarta.servlet-api.source" version="4.0.0"/> + <unit id="org.eclipse.jetty.http" version="10.0.6"/> + <unit id="org.eclipse.jetty.http.source" version="10.0.6"/> + <unit id="org.eclipse.jetty.io" version="10.0.6"/> + <unit id="org.eclipse.jetty.io.source" version="10.0.6"/> + <unit id="org.eclipse.jetty.security" version="10.0.6"/> + <unit id="org.eclipse.jetty.security.source" version="10.0.6"/> + <unit id="org.eclipse.jetty.server" version="10.0.6"/> + <unit id="org.eclipse.jetty.server.source" version="10.0.6"/> + <unit id="org.eclipse.jetty.servlet" version="10.0.6"/> + <unit id="org.eclipse.jetty.servlet.source" version="10.0.6"/> + <unit id="org.eclipse.jetty.util" version="10.0.6"/> + <unit id="org.eclipse.jetty.util.source" version="10.0.6"/> + <unit id="org.eclipse.jetty.util.ajax" version="10.0.6"/> + <unit id="org.eclipse.jetty.util.ajax.source" version="10.0.6"/> + <repository id="jetty-10.0.x" location="https://download.eclipse.org/eclipse/jetty/10.0.6/"/> + </location> + <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit"> + <unit id="com.google.gson" version="2.8.9.v20220111-1409"/> + <unit id="com.google.gson.source" version="2.8.9.v20220111-1409"/> + <unit id="com.jcraft.jsch" version="0.1.55.v20190404-1902"/> + <unit id="com.jcraft.jsch.source" version="0.1.55.v20190404-1902"/> + <unit id="com.jcraft.jzlib" version="1.1.3.v20220502-1820"/> + <unit id="com.jcraft.jzlib.source" version="1.1.3.v20220502-1820"/> + <unit id="com.sun.jna" version="5.8.0.v20210503-0343"/> + <unit id="com.sun.jna.source" version="5.8.0.v20210503-0343"/> + <unit id="com.sun.jna.platform" version="5.8.0.v20210406-1004"/> + <unit id="com.sun.jna.platform.source" version="5.8.0.v20210406-1004"/> + <unit id="javaewah" version="1.1.13.v20211029-0839"/> + <unit id="javaewah.source" version="1.1.13.v20211029-0839"/> + <unit id="net.bytebuddy.byte-buddy" version="1.9.0.v20181107-1410"/> + <unit id="net.bytebuddy.byte-buddy-agent" version="1.9.0.v20181106-1534"/> + <unit id="net.bytebuddy.byte-buddy-agent.source" version="1.9.0.v20181106-1534"/> + <unit id="net.bytebuddy.byte-buddy.source" version="1.9.0.v20181107-1410"/> + <unit id="net.i2p.crypto.eddsa" version="0.3.0.v20220506-1020"/> + <unit id="net.i2p.crypto.eddsa.source" version="0.3.0.v20220506-1020"/> + <unit id="org.apache.ant" version="1.10.12.v20211102-1452"/> + <unit id="org.apache.ant.source" version="1.10.12.v20211102-1452"/> + <unit id="org.apache.commons.codec" version="1.14.0.v20200818-1422"/> + <unit id="org.apache.commons.codec.source" version="1.14.0.v20200818-1422"/> + <unit id="org.apache.commons.compress" version="1.21.0.v20211103-2100"/> + <unit id="org.apache.commons.compress.source" version="1.21.0.v20211103-2100"/> + <unit id="org.apache.commons.logging" version="1.2.0.v20180409-1502"/> + <unit id="org.apache.commons.logging.source" version="1.2.0.v20180409-1502"/> + <unit id="org.apache.httpcomponents.httpclient" version="4.5.13.v20210128-2225"/> + <unit id="org.apache.httpcomponents.httpclient.source" version="4.5.13.v20210128-2225"/> + <unit id="org.apache.httpcomponents.httpcore" version="4.4.15.v20220209-2345"/> + <unit id="org.apache.httpcomponents.httpcore.source" version="4.4.15.v20220209-2345"/> + <unit id="org.apache.sshd.osgi" version="2.8.0.v20211227-1750"/> + <unit id="org.apache.sshd.osgi.source" version="2.8.0.v20211227-1750"/> + <unit id="org.apache.sshd.sftp" version="2.8.0.v20211227-1750"/> + <unit id="org.apache.sshd.sftp.source" version="2.8.0.v20211227-1750"/> + <unit id="org.assertj" version="3.20.2.v20210706-1104"/> + <unit id="org.assertj.source" version="3.20.2.v20210706-1104"/> + <unit id="org.bouncycastle.bcpg" version="1.70.0.v20220507-1208"/> + <unit id="org.bouncycastle.bcpg.source" version="1.70.0.v20220507-1208"/> + <unit id="org.bouncycastle.bcpkix" version="1.70.0.v20220105-1522"/> + <unit id="org.bouncycastle.bcpkix.source" version="1.70.0.v20220105-1522"/> + <unit id="org.bouncycastle.bcprov" version="1.70.0.v20220507-1208"/> + <unit id="org.bouncycastle.bcprov.source" version="1.70.0.v20220507-1208"/> + <unit id="org.bouncycastle.bcutil" version="1.70.0.v20220105-1522"/> + <unit id="org.bouncycastle.bcutil.source" version="1.70.0.v20220105-1522"/> + <unit id="org.hamcrest" version="2.2.0.v20210711-0821"/> + <unit id="org.hamcrest.source" version="2.2.0.v20210711-0821"/> + <unit id="org.hamcrest.core" version="1.3.0.v20180420-1519"/> + <unit id="org.hamcrest.core.source" version="1.3.0.v20180420-1519"/> + <unit id="org.hamcrest.library" version="1.3.0.v20180524-2246"/> + <unit id="org.hamcrest.library.source" version="1.3.0.v20180524-2246"/> + <unit id="org.junit" version="4.13.2.v20211018-1956"/> + <unit id="org.junit.source" version="4.13.2.v20211018-1956"/> + <unit id="org.kohsuke.args4j" version="2.33.0.v20160323-2218"/> + <unit id="org.kohsuke.args4j.source" version="2.33.0.v20160323-2218"/> + <unit id="org.mockito" version="2.23.0.v20200310-1642"/> + <unit id="org.mockito.source" version="2.23.0.v20200310-1642"/> + <unit id="org.objenesis" version="2.6.0.v20180420-1519"/> + <unit id="org.objenesis.source" version="2.6.0.v20180420-1519"/> + <unit id="org.slf4j.api" version="1.7.30.v20200204-2150"/> + <unit id="org.slf4j.api.source" version="1.7.30.v20200204-2150"/> + <unit id="org.slf4j.binding.simple" version="1.7.30.v20200204-2150"/> + <unit id="org.slf4j.binding.simple.source" version="1.7.30.v20200204-2150"/> + <unit id="org.tukaani.xz" version="1.9.0.v20210624-1259"/> + <unit id="org.tukaani.xz.source" version="1.9.0.v20210624-1259"/> + <repository location="https://download.eclipse.org/tools/orbit/downloads/drops/R20220531185310/repository"/> + </location> + <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit"> + <unit id="org.eclipse.osgi" version="0.0.0"/> + <repository location="https://download.eclipse.org/staging/2022-09/"/> + </location> + </locations> +</target> diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.25.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.25.tpd new file mode 100644 index 0000000000..e6a577571d --- /dev/null +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.25.tpd @@ -0,0 +1,8 @@ +target "jgit-4.25" with source configurePhase + +include "projects/jetty-10.0.x.tpd" +include "orbit/R20220531185310-2022-06.tpd" + +location "https://download.eclipse.org/staging/2022-09/" { + org.eclipse.osgi lazy +} diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20211213173813-2021-12.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20211213173813-2021-12.tpd new file mode 100644 index 0000000000..0c7c846738 --- /dev/null +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20211213173813-2021-12.tpd @@ -0,0 +1,69 @@ +target "R20211213173813-2021-12" with source configurePhase +// see https://download.eclipse.org/tools/orbit/downloads/ + +location "https://download.eclipse.org/tools/orbit/downloads/drops/R20211213173813/repository" { + com.google.gson [2.8.8.v20211029-0838,2.8.8.v20211029-0838] + com.google.gson.source [2.8.8.v20211029-0838,2.8.8.v20211029-0838] + com.jcraft.jsch [0.1.55.v20190404-1902,0.1.55.v20190404-1902] + com.jcraft.jsch.source [0.1.55.v20190404-1902,0.1.55.v20190404-1902] + com.jcraft.jzlib [1.1.1.v201205102305,1.1.1.v201205102305] + com.jcraft.jzlib.source [1.1.1.v201205102305,1.1.1.v201205102305] + com.sun.jna [5.8.0.v20210503-0343,5.8.0.v20210503-0343] + com.sun.jna.source [5.8.0.v20210503-0343,5.8.0.v20210503-0343] + com.sun.jna.platform [5.8.0.v20210406-1004,5.8.0.v20210406-1004] + com.sun.jna.platform.source [5.8.0.v20210406-1004,5.8.0.v20210406-1004] + javaewah [1.1.13.v20211029-0839,1.1.13.v20211029-0839] + javaewah.source [1.1.13.v20211029-0839,1.1.13.v20211029-0839] + net.bytebuddy.byte-buddy [1.9.0.v20181107-1410,1.9.0.v20181107-1410] + net.bytebuddy.byte-buddy-agent [1.9.0.v20181106-1534,1.9.0.v20181106-1534] + net.bytebuddy.byte-buddy-agent.source [1.9.0.v20181106-1534,1.9.0.v20181106-1534] + net.bytebuddy.byte-buddy.source [1.9.0.v20181107-1410,1.9.0.v20181107-1410] + net.i2p.crypto.eddsa [0.3.0.v20210923-1401,0.3.0.v20210923-1401] + net.i2p.crypto.eddsa.source [0.3.0.v20210923-1401,0.3.0.v20210923-1401] + org.apache.ant [1.10.12.v20211102-1452,1.10.12.v20211102-1452] + org.apache.ant.source [1.10.12.v20211102-1452,1.10.12.v20211102-1452] + org.apache.commons.codec [1.14.0.v20200818-1422,1.14.0.v20200818-1422] + org.apache.commons.codec.source [1.14.0.v20200818-1422,1.14.0.v20200818-1422] + org.apache.commons.compress [1.21.0.v20211103-2100,1.21.0.v20211103-2100] + org.apache.commons.compress.source [1.21.0.v20211103-2100,1.21.0.v20211103-2100] + org.apache.commons.logging [1.2.0.v20180409-1502,1.2.0.v20180409-1502] + org.apache.commons.logging.source [1.2.0.v20180409-1502,1.2.0.v20180409-1502] + org.apache.httpcomponents.httpclient [4.5.13.v20210128-2225,4.5.13.v20210128-2225] + org.apache.httpcomponents.httpclient.source [4.5.13.v20210128-2225,4.5.13.v20210128-2225] + org.apache.httpcomponents.httpcore [4.4.14.v20210128-2225,4.4.14.v20210128-2225] + org.apache.httpcomponents.httpcore.source [4.4.14.v20210128-2225,4.4.14.v20210128-2225] + org.apache.sshd.osgi [2.7.0.v20210623-0618,2.7.0.v20210623-0618] + org.apache.sshd.osgi.source [2.7.0.v20210623-0618,2.7.0.v20210623-0618] + org.apache.sshd.sftp [2.7.0.v20210623-0618,2.7.0.v20210623-0618] + org.apache.sshd.sftp.source [2.7.0.v20210623-0618,2.7.0.v20210623-0618] + org.assertj [3.20.2.v20210706-1104,3.20.2.v20210706-1104] + org.assertj.source [3.20.2.v20210706-1104,3.20.2.v20210706-1104] + org.bouncycastle.bcpg [1.69.0.v20210713-1924,1.69.0.v20210713-1924] + org.bouncycastle.bcpg.source [1.69.0.v20210713-1924,1.69.0.v20210713-1924] + org.bouncycastle.bcpkix [1.69.0.v20210713-1924,1.69.0.v20210713-1924] + org.bouncycastle.bcpkix.source [1.69.0.v20210713-1924,1.69.0.v20210713-1924] + org.bouncycastle.bcprov [1.69.0.v20210923-1401,1.69.0.v20210923-1401] + org.bouncycastle.bcprov.source [1.69.0.v20210923-1401,1.69.0.v20210923-1401] + org.bouncycastle.bcutil [1.69.0.v20210713-1924,1.69.0.v20210713-1924] + org.bouncycastle.bcutil.source [1.69.0.v20210713-1924,1.69.0.v20210713-1924] + org.hamcrest [2.2.0.v20210711-0821,2.2.0.v20210711-0821] + org.hamcrest.source [2.2.0.v20210711-0821,2.2.0.v20210711-0821] + org.hamcrest.core [1.3.0.v20180420-1519,1.3.0.v20180420-1519] + org.hamcrest.core.source [1.3.0.v20180420-1519,1.3.0.v20180420-1519] + org.hamcrest.library [1.3.0.v20180524-2246,1.3.0.v20180524-2246] + org.hamcrest.library.source [1.3.0.v20180524-2246,1.3.0.v20180524-2246] + org.junit [4.13.2.v20211018-1956,4.13.2.v20211018-1956] + org.junit.source [4.13.2.v20211018-1956,4.13.2.v20211018-1956] + org.kohsuke.args4j [2.33.0.v20160323-2218,2.33.0.v20160323-2218] + org.kohsuke.args4j.source [2.33.0.v20160323-2218,2.33.0.v20160323-2218] + org.mockito [2.23.0.v20200310-1642,2.23.0.v20200310-1642] + org.mockito.source [2.23.0.v20200310-1642,2.23.0.v20200310-1642] + org.objenesis [2.6.0.v20180420-1519,2.6.0.v20180420-1519] + org.objenesis.source [2.6.0.v20180420-1519,2.6.0.v20180420-1519] + org.slf4j.api [1.7.30.v20200204-2150,1.7.30.v20200204-2150] + org.slf4j.api.source [1.7.30.v20200204-2150,1.7.30.v20200204-2150] + org.slf4j.binding.simple [1.7.30.v20200204-2150,1.7.30.v20200204-2150] + org.slf4j.binding.simple.source [1.7.30.v20200204-2150,1.7.30.v20200204-2150] + org.tukaani.xz [1.9.0.v20210624-1259,1.9.0.v20210624-1259] + org.tukaani.xz.source [1.9.0.v20210624-1259,1.9.0.v20210624-1259] +} diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20220302172233-2022-03.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20220302172233-2022-03.tpd new file mode 100644 index 0000000000..fafc689268 --- /dev/null +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20220302172233-2022-03.tpd @@ -0,0 +1,69 @@ +target "R20220302172233" with source configurePhase +// see https://download.eclipse.org/tools/orbit/downloads/ + +location "https://download.eclipse.org/tools/orbit/downloads/drops/R20220302172233/repository" { + com.google.gson [2.8.9.v20220111-1409,2.8.9.v20220111-1409] + com.google.gson.source [2.8.9.v20220111-1409,2.8.9.v20220111-1409] + com.jcraft.jsch [0.1.55.v20190404-1902,0.1.55.v20190404-1902] + com.jcraft.jsch.source [0.1.55.v20190404-1902,0.1.55.v20190404-1902] + com.jcraft.jzlib [1.1.1.v201205102305,1.1.1.v201205102305] + com.jcraft.jzlib.source [1.1.1.v201205102305,1.1.1.v201205102305] + com.sun.jna [5.8.0.v20210503-0343,5.8.0.v20210503-0343] + com.sun.jna.source [5.8.0.v20210503-0343,5.8.0.v20210503-0343] + com.sun.jna.platform [5.8.0.v20210406-1004,5.8.0.v20210406-1004] + com.sun.jna.platform.source [5.8.0.v20210406-1004,5.8.0.v20210406-1004] + javaewah [1.1.13.v20211029-0839,1.1.13.v20211029-0839] + javaewah.source [1.1.13.v20211029-0839,1.1.13.v20211029-0839] + net.bytebuddy.byte-buddy [1.9.0.v20181107-1410,1.9.0.v20181107-1410] + net.bytebuddy.byte-buddy-agent [1.9.0.v20181106-1534,1.9.0.v20181106-1534] + net.bytebuddy.byte-buddy-agent.source [1.9.0.v20181106-1534,1.9.0.v20181106-1534] + net.bytebuddy.byte-buddy.source [1.9.0.v20181107-1410,1.9.0.v20181107-1410] + net.i2p.crypto.eddsa [0.3.0.v20210923-1401,0.3.0.v20210923-1401] + net.i2p.crypto.eddsa.source [0.3.0.v20210923-1401,0.3.0.v20210923-1401] + org.apache.ant [1.10.12.v20211102-1452,1.10.12.v20211102-1452] + org.apache.ant.source [1.10.12.v20211102-1452,1.10.12.v20211102-1452] + org.apache.commons.codec [1.14.0.v20200818-1422,1.14.0.v20200818-1422] + org.apache.commons.codec.source [1.14.0.v20200818-1422,1.14.0.v20200818-1422] + org.apache.commons.compress [1.21.0.v20211103-2100,1.21.0.v20211103-2100] + org.apache.commons.compress.source [1.21.0.v20211103-2100,1.21.0.v20211103-2100] + org.apache.commons.logging [1.2.0.v20180409-1502,1.2.0.v20180409-1502] + org.apache.commons.logging.source [1.2.0.v20180409-1502,1.2.0.v20180409-1502] + org.apache.httpcomponents.httpclient [4.5.13.v20210128-2225,4.5.13.v20210128-2225] + org.apache.httpcomponents.httpclient.source [4.5.13.v20210128-2225,4.5.13.v20210128-2225] + org.apache.httpcomponents.httpcore [4.4.15.v20220209-2345,4.4.15.v20220209-2345] + org.apache.httpcomponents.httpcore.source [4.4.15.v20220209-2345,4.4.15.v20220209-2345] + org.apache.sshd.osgi [2.8.0.v20211227-1750,2.8.0.v20211227-1750] + org.apache.sshd.osgi.source [2.8.0.v20211227-1750,2.8.0.v20211227-1750] + org.apache.sshd.sftp [2.8.0.v20211227-1750,2.8.0.v20211227-1750] + org.apache.sshd.sftp.source [2.8.0.v20211227-1750,2.8.0.v20211227-1750] + org.assertj [3.20.2.v20210706-1104,3.20.2.v20210706-1104] + org.assertj.source [3.20.2.v20210706-1104,3.20.2.v20210706-1104] + org.bouncycastle.bcpg [1.70.0.v20220105-1522,1.70.0.v20220105-1522] + org.bouncycastle.bcpg.source [1.70.0.v20220105-1522,1.70.0.v20220105-1522] + org.bouncycastle.bcpkix [1.70.0.v20220105-1522,1.70.0.v20220105-1522] + org.bouncycastle.bcpkix.source [1.70.0.v20220105-1522,1.70.0.v20220105-1522] + org.bouncycastle.bcprov [1.70.0.v20220105-1522,1.70.0.v20220105-1522] + org.bouncycastle.bcprov.source [1.70.0.v20220105-1522,1.70.0.v20220105-1522] + org.bouncycastle.bcutil [1.70.0.v20220105-1522,1.70.0.v20220105-1522] + org.bouncycastle.bcutil.source [1.70.0.v20220105-1522,1.70.0.v20220105-1522] + org.hamcrest [2.2.0.v20210711-0821,2.2.0.v20210711-0821] + org.hamcrest.source [2.2.0.v20210711-0821,2.2.0.v20210711-0821] + org.hamcrest.core [1.3.0.v20180420-1519,1.3.0.v20180420-1519] + org.hamcrest.core.source [1.3.0.v20180420-1519,1.3.0.v20180420-1519] + org.hamcrest.library [1.3.0.v20180524-2246,1.3.0.v20180524-2246] + org.hamcrest.library.source [1.3.0.v20180524-2246,1.3.0.v20180524-2246] + org.junit [4.13.2.v20211018-1956,4.13.2.v20211018-1956] + org.junit.source [4.13.2.v20211018-1956,4.13.2.v20211018-1956] + org.kohsuke.args4j [2.33.0.v20160323-2218,2.33.0.v20160323-2218] + org.kohsuke.args4j.source [2.33.0.v20160323-2218,2.33.0.v20160323-2218] + org.mockito [2.23.0.v20200310-1642,2.23.0.v20200310-1642] + org.mockito.source [2.23.0.v20200310-1642,2.23.0.v20200310-1642] + org.objenesis [2.6.0.v20180420-1519,2.6.0.v20180420-1519] + org.objenesis.source [2.6.0.v20180420-1519,2.6.0.v20180420-1519] + org.slf4j.api [1.7.30.v20200204-2150,1.7.30.v20200204-2150] + org.slf4j.api.source [1.7.30.v20200204-2150,1.7.30.v20200204-2150] + org.slf4j.binding.simple [1.7.30.v20200204-2150,1.7.30.v20200204-2150] + org.slf4j.binding.simple.source [1.7.30.v20200204-2150,1.7.30.v20200204-2150] + org.tukaani.xz [1.9.0.v20210624-1259,1.9.0.v20210624-1259] + org.tukaani.xz.source [1.9.0.v20210624-1259,1.9.0.v20210624-1259] +} diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20220531185310-2022-06.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20220531185310-2022-06.tpd new file mode 100644 index 0000000000..3c74497c21 --- /dev/null +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20220531185310-2022-06.tpd @@ -0,0 +1,69 @@ +target "R20220531185310-2022-06" with source configurePhase +// see https://download.eclipse.org/tools/orbit/downloads/ + +location "https://download.eclipse.org/tools/orbit/downloads/drops/R20220531185310/repository" { + com.google.gson [2.8.9.v20220111-1409,2.8.9.v20220111-1409] + com.google.gson.source [2.8.9.v20220111-1409,2.8.9.v20220111-1409] + com.jcraft.jsch [0.1.55.v20190404-1902,0.1.55.v20190404-1902] + com.jcraft.jsch.source [0.1.55.v20190404-1902,0.1.55.v20190404-1902] + com.jcraft.jzlib [1.1.3.v20220502-1820,1.1.3.v20220502-1820] + com.jcraft.jzlib.source [1.1.3.v20220502-1820,1.1.3.v20220502-1820] + com.sun.jna [5.8.0.v20210503-0343,5.8.0.v20210503-0343] + com.sun.jna.source [5.8.0.v20210503-0343,5.8.0.v20210503-0343] + com.sun.jna.platform [5.8.0.v20210406-1004,5.8.0.v20210406-1004] + com.sun.jna.platform.source [5.8.0.v20210406-1004,5.8.0.v20210406-1004] + javaewah [1.1.13.v20211029-0839,1.1.13.v20211029-0839] + javaewah.source [1.1.13.v20211029-0839,1.1.13.v20211029-0839] + net.bytebuddy.byte-buddy [1.9.0.v20181107-1410,1.9.0.v20181107-1410] + net.bytebuddy.byte-buddy-agent [1.9.0.v20181106-1534,1.9.0.v20181106-1534] + net.bytebuddy.byte-buddy-agent.source [1.9.0.v20181106-1534,1.9.0.v20181106-1534] + net.bytebuddy.byte-buddy.source [1.9.0.v20181107-1410,1.9.0.v20181107-1410] + net.i2p.crypto.eddsa [0.3.0.v20220506-1020,0.3.0.v20220506-1020] + net.i2p.crypto.eddsa.source [0.3.0.v20220506-1020,0.3.0.v20220506-1020] + org.apache.ant [1.10.12.v20211102-1452,1.10.12.v20211102-1452] + org.apache.ant.source [1.10.12.v20211102-1452,1.10.12.v20211102-1452] + org.apache.commons.codec [1.14.0.v20200818-1422,1.14.0.v20200818-1422] + org.apache.commons.codec.source [1.14.0.v20200818-1422,1.14.0.v20200818-1422] + org.apache.commons.compress [1.21.0.v20211103-2100,1.21.0.v20211103-2100] + org.apache.commons.compress.source [1.21.0.v20211103-2100,1.21.0.v20211103-2100] + org.apache.commons.logging [1.2.0.v20180409-1502,1.2.0.v20180409-1502] + org.apache.commons.logging.source [1.2.0.v20180409-1502,1.2.0.v20180409-1502] + org.apache.httpcomponents.httpclient [4.5.13.v20210128-2225,4.5.13.v20210128-2225] + org.apache.httpcomponents.httpclient.source [4.5.13.v20210128-2225,4.5.13.v20210128-2225] + org.apache.httpcomponents.httpcore [4.4.15.v20220209-2345,4.4.15.v20220209-2345] + org.apache.httpcomponents.httpcore.source [4.4.15.v20220209-2345,4.4.15.v20220209-2345] + org.apache.sshd.osgi [2.8.0.v20211227-1750,2.8.0.v20211227-1750] + org.apache.sshd.osgi.source [2.8.0.v20211227-1750,2.8.0.v20211227-1750] + org.apache.sshd.sftp [2.8.0.v20211227-1750,2.8.0.v20211227-1750] + org.apache.sshd.sftp.source [2.8.0.v20211227-1750,2.8.0.v20211227-1750] + org.assertj [3.20.2.v20210706-1104,3.20.2.v20210706-1104] + org.assertj.source [3.20.2.v20210706-1104,3.20.2.v20210706-1104] + org.bouncycastle.bcpg [1.70.0.v20220507-1208,1.70.0.v20220507-1208] + org.bouncycastle.bcpg.source [1.70.0.v20220507-1208,1.70.0.v20220507-1208] + org.bouncycastle.bcpkix [1.70.0.v20220105-1522,1.70.0.v20220105-1522] + org.bouncycastle.bcpkix.source [1.70.0.v20220105-1522,1.70.0.v20220105-1522] + org.bouncycastle.bcprov [1.70.0.v20220507-1208,1.70.0.v20220507-1208] + org.bouncycastle.bcprov.source [1.70.0.v20220507-1208,1.70.0.v20220507-1208] + org.bouncycastle.bcutil [1.70.0.v20220105-1522,1.70.0.v20220105-1522] + org.bouncycastle.bcutil.source [1.70.0.v20220105-1522,1.70.0.v20220105-1522] + org.hamcrest [2.2.0.v20210711-0821,2.2.0.v20210711-0821] + org.hamcrest.source [2.2.0.v20210711-0821,2.2.0.v20210711-0821] + org.hamcrest.core [1.3.0.v20180420-1519,1.3.0.v20180420-1519] + org.hamcrest.core.source [1.3.0.v20180420-1519,1.3.0.v20180420-1519] + org.hamcrest.library [1.3.0.v20180524-2246,1.3.0.v20180524-2246] + org.hamcrest.library.source [1.3.0.v20180524-2246,1.3.0.v20180524-2246] + org.junit [4.13.2.v20211018-1956,4.13.2.v20211018-1956] + org.junit.source [4.13.2.v20211018-1956,4.13.2.v20211018-1956] + org.kohsuke.args4j [2.33.0.v20160323-2218,2.33.0.v20160323-2218] + org.kohsuke.args4j.source [2.33.0.v20160323-2218,2.33.0.v20160323-2218] + org.mockito [2.23.0.v20200310-1642,2.23.0.v20200310-1642] + org.mockito.source [2.23.0.v20200310-1642,2.23.0.v20200310-1642] + org.objenesis [2.6.0.v20180420-1519,2.6.0.v20180420-1519] + org.objenesis.source [2.6.0.v20180420-1519,2.6.0.v20180420-1519] + org.slf4j.api [1.7.30.v20200204-2150,1.7.30.v20200204-2150] + org.slf4j.api.source [1.7.30.v20200204-2150,1.7.30.v20200204-2150] + org.slf4j.binding.simple [1.7.30.v20200204-2150,1.7.30.v20200204-2150] + org.slf4j.binding.simple.source [1.7.30.v20200204-2150,1.7.30.v20200204-2150] + org.tukaani.xz [1.9.0.v20210624-1259,1.9.0.v20210624-1259] + org.tukaani.xz.source [1.9.0.v20210624-1259,1.9.0.v20210624-1259] +} diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/pom.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/pom.xml index 6744f6b047..71d2e93414 100644 --- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/pom.xml +++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/pom.xml @@ -22,31 +22,4 @@ <artifactId>org.eclipse.jgit.target</artifactId> <packaging>pom</packaging> <name>JGit Target Platform</name> - - <build> - <plugins> - <plugin> - <groupId>org.codehaus.mojo</groupId> - <artifactId>build-helper-maven-plugin</artifactId> - <executions> - <execution> - <id>attach-artifacts</id> - <phase>package</phase> - <goals> - <goal>attach-artifact</goal> - </goals> - <configuration> - <artifacts> - <artifact> - <file>${target-platform}.target</file> - <type>target</type> - <classifier>${target-platform}</classifier> - </artifact> - </artifacts> - </configuration> - </execution> - </executions> - </plugin> - </plugins> - </build> </project>
\ No newline at end of file diff --git a/org.eclipse.jgit.packaging/pom.xml b/org.eclipse.jgit.packaging/pom.xml index e2b7b79478..60206619f7 100644 --- a/org.eclipse.jgit.packaging/pom.xml +++ b/org.eclipse.jgit.packaging/pom.xml @@ -23,7 +23,7 @@ <properties> <java.version>11</java.version> - <tycho-version>2.5.0</tycho-version> + <tycho-version>2.6.0</tycho-version> <tycho-extras-version>${tycho-version}</tycho-extras-version> <target-platform>jgit-4.17</target-platform> </properties> @@ -231,12 +231,7 @@ <resolver>p2</resolver> <pomDependencies>consider</pomDependencies> <target> - <artifact> - <groupId>org.eclipse.jgit</groupId> - <artifactId>org.eclipse.jgit.target</artifactId> - <version>${project.version}</version> - <classifier>${target-platform}</classifier> - </artifact> + <file>${project.basedir}/../org.eclipse.jgit.target/${target-platform}.target</file> </target> <environments> <environment> @@ -264,6 +259,11 @@ <ws>cocoa</ws> <arch>x86_64</arch> </environment> + <environment> + <os>macosx</os> + <ws>cocoa</ws> + <arch>aarch64</arch> + </environment> </environments> </configuration> </plugin> diff --git a/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF index 9f9ee463b7..23f7c34a52 100644 --- a/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF @@ -12,7 +12,8 @@ Import-Package: org.eclipse.jgit.api;version="[7.0.0,7.1.0)", org.eclipse.jgit.api.errors;version="[7.0.0,7.1.0)", org.eclipse.jgit.diff;version="[7.0.0,7.1.0)", org.eclipse.jgit.dircache;version="[7.0.0,7.1.0)", - org.eclipse.jgit.internal.storage.file;version="7.0.0", + org.eclipse.jgit.internal.diffmergetool;version="[7.0.0,7.1.0)", + org.eclipse.jgit.internal.storage.file;version="[7.0.0,7.1.0)", org.eclipse.jgit.junit;version="[7.0.0,7.1.0)", org.eclipse.jgit.lib;version="[7.0.0,7.1.0)", org.eclipse.jgit.lib.internal;version="[7.0.0,7.1.0)", @@ -26,7 +27,7 @@ Import-Package: org.eclipse.jgit.api;version="[7.0.0,7.1.0)", org.eclipse.jgit.treewalk;version="[7.0.0,7.1.0)", org.eclipse.jgit.util;version="[7.0.0,7.1.0)", org.eclipse.jgit.util.io;version="[7.0.0,7.1.0)", - org.hamcrest.core;bundle-version="[2.2.0,3.0.0)", + org.hamcrest.core;bundle-version="[1.1.0,3.0.0)", org.junit;version="[4.13,5.0.0)", org.junit.rules;version="[4.13,5.0.0)", org.kohsuke.args4j;version="[2.33.0,3.0.0)" 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 4bad73b5b5..c78544309b 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 @@ -89,6 +89,31 @@ public class DescribeTest extends CLIRepositoryTestCase { } @Test + public void testDescribeCommitMatchAbbrev() throws Exception { + initialCommitAndTag(); + secondCommit(); + assertArrayEquals(new String[] { "v1.0-1-g56f6cebdf3f5", "" }, + execute("git describe --abbrev 12 --match v1.*")); + } + + @Test + public void testDescribeCommitMatchAbbrevMin() throws Exception { + initialCommitAndTag(); + secondCommit(); + assertArrayEquals(new String[] { "v1.0-1-g56f6", "" }, + execute("git describe --abbrev -5 --match v1.*")); + } + + @Test + public void testDescribeCommitMatchAbbrevMax() throws Exception { + initialCommitAndTag(); + secondCommit(); + assertArrayEquals(new String[] { + "v1.0-1-g56f6cebdf3f5ceeecd803365abf0996fb1fa006d", "" }, + execute("git describe --abbrev 50 --match v1.*")); + } + + @Test public void testDescribeCommitMatch2() throws Exception { initialCommitAndTag(); secondCommit(); diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java index e7bf48417d..f0908cebc4 100644 --- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021, Simeon Andreev <simeon.danailov.andreev@gmail.com> and others. + * Copyright (C) 2021-2022, Simeon Andreev <simeon.danailov.andreev@gmail.com> and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -9,72 +9,147 @@ */ package org.eclipse.jgit.pgm; -import static org.junit.Assert.assertEquals; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DIFFTOOL_SECTION; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DIFF_SECTION; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CMD; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PROMPT; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL; +import static org.junit.Assert.fail; +import java.io.File; +import java.io.InputStream; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; -import org.eclipse.jgit.api.Git; -import org.eclipse.jgit.diff.DiffEntry; -import org.eclipse.jgit.internal.diffmergetool.CommandLineDiffTool; -import org.eclipse.jgit.lib.CLIRepositoryTestCase; -import org.eclipse.jgit.pgm.opt.CmdLineParser; -import org.eclipse.jgit.pgm.opt.SubcommandHandler; -import org.eclipse.jgit.revwalk.RevCommit; -import org.eclipse.jgit.treewalk.FileTreeIterator; -import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.internal.diffmergetool.DiffTools; +import org.eclipse.jgit.internal.diffmergetool.ExternalDiffTool; +import org.eclipse.jgit.lib.StoredConfig; import org.junit.Before; import org.junit.Test; -import org.kohsuke.args4j.Argument; /** * Testing the {@code difftool} command. */ -public class DiffToolTest extends CLIRepositoryTestCase { - public static class GitCliJGitWrapperParser { - @Argument(index = 0, metaVar = "metaVar_command", required = true, handler = SubcommandHandler.class) - TextBuiltin subcommand; +public class DiffToolTest extends ToolTestCase { - @Argument(index = 1, metaVar = "metaVar_arg") - List<String> arguments = new ArrayList<>(); - } + private static final String DIFF_TOOL = CONFIG_DIFFTOOL_SECTION; - private String[] runAndCaptureUsingInitRaw(String... args) - throws Exception { - CLIGitCommand.Result result = new CLIGitCommand.Result(); + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + configureEchoTool(TOOL_NAME); + } - GitCliJGitWrapperParser bean = new GitCliJGitWrapperParser(); - CmdLineParser clp = new CmdLineParser(bean); - clp.parseArgument(args); + @Test(expected = Die.class) + public void testUndefinedTool() throws Exception { + String toolName = "undefined"; + String[] conflictingFilenames = createUnstagedChanges(); - TextBuiltin cmd = bean.subcommand; - cmd.initRaw(db, null, null, result.out, result.err); - cmd.execute(bean.arguments.toArray(new String[bean.arguments.size()])); - if (cmd.getOutputWriter() != null) { - cmd.getOutputWriter().flush(); + List<String> expectedErrors = new ArrayList<>(); + for (String changedFilename : conflictingFilenames) { + expectedErrors.add("External diff tool is not defined: " + toolName); + expectedErrors.add("compare of " + changedFilename + " failed"); } - if (cmd.getErrorWriter() != null) { - cmd.getErrorWriter().flush(); - } - return result.outLines().toArray(new String[0]); + + runAndCaptureUsingInitRaw(expectedErrors, DIFF_TOOL, "--no-prompt", + "--tool", toolName); + fail("Expected exception to be thrown due to undefined external tool"); } - private Git git; + @Test(expected = Die.class) + public void testUserToolWithCommandNotFoundError() throws Exception { + String toolName = "customTool"; - @Override - @Before - public void setUp() throws Exception { - super.setUp(); - git = new Git(db); - git.commit().setMessage("initial commit").call(); + int errorReturnCode = 127; // command not found + String command = "exit " + errorReturnCode; + + StoredConfig config = db.getConfig(); + config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD, + command); + + createMergeConflict(); + runAndCaptureUsingInitRaw(DIFF_TOOL, "--no-prompt", "--tool", toolName); + + fail("Expected exception to be thrown due to external tool exiting with error code: " + + errorReturnCode); + } + + @Test(expected = Die.class) + public void testEmptyToolName() throws Exception { + assumeLinuxPlatform(); + + String emptyToolName = ""; + + StoredConfig config = db.getConfig(); + // the default diff tool is configured without a subsection + String subsection = null; + config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_TOOL, + emptyToolName); + + createUnstagedChanges(); + + String araxisErrorLine = "compare: unrecognized option `-wait' @ error/compare.c/CompareImageCommand/1123."; + String[] expectedErrorOutput = { araxisErrorLine, araxisErrorLine, }; + runAndCaptureUsingInitRaw(Arrays.asList(expectedErrorOutput), DIFF_TOOL, + "--no-prompt"); + fail("Expected exception to be thrown due to external tool exiting with an error"); + } + + @Test + public void testToolWithPrompt() throws Exception { + String[] inputLines = { + "y", // accept launching diff tool + "y", // accept launching diff tool + }; + + String[] conflictingFilenames = createUnstagedChanges(); + String[] expectedOutput = getExpectedCompareOutput(conflictingFilenames); + + String option = "--tool"; + + InputStream inputStream = createInputStream(inputLines); + assertArrayOfLinesEquals("Incorrect output for option: " + option, + expectedOutput, runAndCaptureUsingInitRaw(inputStream, + DIFF_TOOL, "--prompt", option, TOOL_NAME)); + } + + @Test + public void testToolAbortLaunch() throws Exception { + String[] inputLines = { + "y", // accept launching diff tool + "n", // don't launch diff tool + }; + + String[] conflictingFilenames = createUnstagedChanges(); + int abortIndex = 1; + String[] expectedOutput = getExpectedAbortOutput(conflictingFilenames, abortIndex); + + String option = "--tool"; + + InputStream inputStream = createInputStream(inputLines); + assertArrayOfLinesEquals("Incorrect output for option: " + option, + expectedOutput, + runAndCaptureUsingInitRaw(inputStream, DIFF_TOOL, "--prompt", option, + TOOL_NAME)); + } + + @Test(expected = Die.class) + public void testNotDefinedTool() throws Exception { + createUnstagedChanges(); + + runAndCaptureUsingInitRaw(DIFF_TOOL, "--tool", "undefined"); + fail("Expected exception when trying to run undefined tool"); } @Test public void testTool() throws Exception { - RevCommit commit = createUnstagedChanges(); - List<DiffEntry> changes = getRepositoryChanges(commit); - String[] expectedOutput = getExpectedDiffToolOutput(changes); + String[] conflictFilenames = createUnstagedChanges(); + String[] expectedOutput = getExpectedToolOutputNoPrompt(conflictFilenames); String[] options = { "--tool", @@ -84,69 +159,88 @@ public class DiffToolTest extends CLIRepositoryTestCase { for (String option : options) { assertArrayOfLinesEquals("Incorrect output for option: " + option, expectedOutput, - runAndCaptureUsingInitRaw("difftool", option, - "some_tool")); + runAndCaptureUsingInitRaw(DIFF_TOOL, option, + TOOL_NAME)); } } @Test public void testToolTrustExitCode() throws Exception { - RevCommit commit = createUnstagedChanges(); - List<DiffEntry> changes = getRepositoryChanges(commit); - String[] expectedOutput = getExpectedDiffToolOutput(changes); + String[] conflictingFilenames = createUnstagedChanges(); + String[] expectedOutput = getExpectedToolOutputNoPrompt(conflictingFilenames); String[] options = { "--tool", "-t", }; for (String option : options) { assertArrayOfLinesEquals("Incorrect output for option: " + option, - expectedOutput, runAndCaptureUsingInitRaw("difftool", - "--trust-exit-code", option, "some_tool")); + expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL, + "--trust-exit-code", option, TOOL_NAME)); } } @Test public void testToolNoGuiNoPromptNoTrustExitcode() throws Exception { - RevCommit commit = createUnstagedChanges(); - List<DiffEntry> changes = getRepositoryChanges(commit); - String[] expectedOutput = getExpectedDiffToolOutput(changes); + String[] conflictingFilenames = createUnstagedChanges(); + String[] expectedOutput = getExpectedToolOutputNoPrompt(conflictingFilenames); String[] options = { "--tool", "-t", }; for (String option : options) { assertArrayOfLinesEquals("Incorrect output for option: " + option, - expectedOutput, runAndCaptureUsingInitRaw("difftool", + expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL, "--no-gui", "--no-prompt", "--no-trust-exit-code", - option, "some_tool")); + option, TOOL_NAME)); } } @Test public void testToolCached() throws Exception { - RevCommit commit = createStagedChanges(); - List<DiffEntry> changes = getRepositoryChanges(commit); - String[] expectedOutput = getExpectedDiffToolOutput(changes); + String[] conflictingFilenames = createStagedChanges(); + Pattern[] expectedOutput = getExpectedCachedToolOutputNoPrompt(conflictingFilenames); String[] options = { "--cached", "--staged", }; for (String option : options) { - assertArrayOfLinesEquals("Incorrect output for option: " + option, - expectedOutput, runAndCaptureUsingInitRaw("difftool", - option, "--tool", "some_tool")); + assertArrayOfMatchingLines("Incorrect output for option: " + option, + expectedOutput, runAndCaptureUsingInitRaw(DIFF_TOOL, + option, "--tool", TOOL_NAME)); } } @Test public void testToolHelp() throws Exception { - CommandLineDiffTool[] defaultTools = CommandLineDiffTool.values(); List<String> expectedOutput = new ArrayList<>(); - expectedOutput.add("git difftool --tool=<tool> may be set to one of the following:"); - for (CommandLineDiffTool defaultTool : defaultTools) { - String toolName = defaultTool.name(); + + DiffTools diffTools = new DiffTools(db); + Map<String, ExternalDiffTool> predefinedTools = diffTools + .getPredefinedTools(true); + List<ExternalDiffTool> availableTools = new ArrayList<>(); + List<ExternalDiffTool> notAvailableTools = new ArrayList<>(); + for (ExternalDiffTool tool : predefinedTools.values()) { + if (tool.isAvailable()) { + availableTools.add(tool); + } else { + notAvailableTools.add(tool); + } + } + + expectedOutput.add( + "'git difftool --tool=<tool>' may be set to one of the following:"); + for (ExternalDiffTool tool : availableTools) { + String toolName = tool.getName(); + expectedOutput.add(toolName); + } + String customToolHelpLine = TOOL_NAME + "." + CONFIG_KEY_CMD + " " + + getEchoCommand(); + expectedOutput.add("user-defined:"); + expectedOutput.add(customToolHelpLine); + expectedOutput.add( + "The following tools are valid, but not currently available:"); + for (ExternalDiffTool tool : notAvailableTools) { + String toolName = tool.getName(); expectedOutput.add(toolName); } String[] userDefinedToolsHelp = { - "user-defined:", - "The following tools are valid, but not currently available:", "Some of the tools listed above only work in a windowed", "environment. If run in a terminal-only session, they will fail.", }; @@ -154,52 +248,99 @@ public class DiffToolTest extends CLIRepositoryTestCase { String option = "--tool-help"; assertArrayOfLinesEquals("Incorrect output for option: " + option, - expectedOutput.toArray(new String[0]), runAndCaptureUsingInitRaw("difftool", option)); - } - - private RevCommit createUnstagedChanges() throws Exception { - writeTrashFile("a", "Hello world a"); - writeTrashFile("b", "Hello world b"); - git.add().addFilepattern(".").call(); - RevCommit commit = git.commit().setMessage("files a & b").call(); - writeTrashFile("a", "New Hello world a"); - writeTrashFile("b", "New Hello world b"); - return commit; - } - - private RevCommit createStagedChanges() throws Exception { - RevCommit commit = createUnstagedChanges(); - git.add().addFilepattern(".").call(); - return commit; - } - - private List<DiffEntry> getRepositoryChanges(RevCommit commit) - throws Exception { - TreeWalk tw = new TreeWalk(db); - tw.addTree(commit.getTree()); - FileTreeIterator modifiedTree = new FileTreeIterator(db); - tw.addTree(modifiedTree); - List<DiffEntry> changes = DiffEntry.scan(tw); - return changes; - } - - private String[] getExpectedDiffToolOutput(List<DiffEntry> changes) { - String[] expectedToolOutput = new String[changes.size()]; - for (int i = 0; i < changes.size(); ++i) { - DiffEntry change = changes.get(i); - String newPath = change.getNewPath(); - String oldPath = change.getOldPath(); - String newIdName = change.getNewId().name(); - String oldIdName = change.getOldId().name(); - String expectedLine = "M\t" + newPath + " (" + newIdName + ")" - + "\t" + oldPath + " (" + oldIdName + ")"; - expectedToolOutput[i] = expectedLine; + expectedOutput.toArray(new String[0]), + runAndCaptureUsingInitRaw(DIFF_TOOL, option)); + } + + private void configureEchoTool(String toolName) { + StoredConfig config = db.getConfig(); + // the default diff tool is configured without a subsection + String subsection = null; + config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_TOOL, + toolName); + + String command = getEchoCommand(); + + config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD, + command); + /* + * prevent prompts as we are running in tests and there is no user to + * interact with on the command line + */ + config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_PROMPT, + String.valueOf(false)); + } + + private String[] getExpectedToolOutputNoPrompt(String[] conflictingFilenames) { + String[] expectedToolOutput = new String[conflictingFilenames.length]; + for (int i = 0; i < conflictingFilenames.length; ++i) { + String newPath = conflictingFilenames[i]; + Path fullPath = getFullPath(newPath); + expectedToolOutput[i] = fullPath.toString(); } return expectedToolOutput; } - private static void assertArrayOfLinesEquals(String failMessage, - String[] expected, String[] actual) { - assertEquals(failMessage, toString(expected), toString(actual)); + private Pattern[] getExpectedCachedToolOutputNoPrompt(String[] conflictingFilenames) { + String tmpDir = System.getProperty("java.io.tmpdir"); + if (tmpDir.endsWith(File.separator)) { + tmpDir = tmpDir.substring(0, tmpDir.length() - 1); + } + Pattern emptyPattern = Pattern.compile(""); + List<Pattern> expectedToolOutput = new ArrayList<>(); + for (int i = 0; i < conflictingFilenames.length; ++i) { + String changedFilename = conflictingFilenames[i]; + Path fullPath = getFullPath(changedFilename); + String filename = fullPath.getFileName().toString(); + String regexp = tmpDir + File.separatorChar + filename + + "_REMOTE_.*"; + Pattern pattern = Pattern.compile(regexp); + expectedToolOutput.add(pattern); + expectedToolOutput.add(emptyPattern); + } + expectedToolOutput.add(emptyPattern); + return expectedToolOutput.toArray(new Pattern[0]); + } + + private String[] getExpectedCompareOutput(String[] conflictingFilenames) { + List<String> expected = new ArrayList<>(); + int n = conflictingFilenames.length; + for (int i = 0; i < n; ++i) { + String changedFilename = conflictingFilenames[i]; + expected.add( + "Viewing (" + (i + 1) + "/" + n + "): '" + changedFilename + + "'"); + expected.add("Launch '" + TOOL_NAME + "' [Y/n]?"); + Path fullPath = getFullPath(changedFilename); + expected.add(fullPath.toString()); + } + return expected.toArray(new String[0]); + } + + private String[] getExpectedAbortOutput(String[] conflictingFilenames, + int abortIndex) { + List<String> expected = new ArrayList<>(); + int n = conflictingFilenames.length; + for (int i = 0; i < n; ++i) { + String changedFilename = conflictingFilenames[i]; + expected.add( + "Viewing (" + (i + 1) + "/" + n + "): '" + changedFilename + + "'"); + expected.add("Launch '" + TOOL_NAME + "' [Y/n]?"); + if (i == abortIndex) { + break; + } + Path fullPath = getFullPath(changedFilename); + expected.add(fullPath.toString()); + } + return expected.toArray(new String[0]); + } + + private static String getEchoCommand() { + /* + * use 'REMOTE' placeholder, as it will be replaced by a file path + * within the repository. + */ + return "(echo \"$REMOTE\")"; } } diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/FetchTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/FetchTest.java index 564bd5fa94..1c6b7839d3 100644 --- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/FetchTest.java +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/FetchTest.java @@ -33,6 +33,7 @@ public class FetchTest extends CLIRepositoryTestCase { git.commit().setMessage("initial commit").call(); Repository remoteRepository = createWorkRepository(); + addRepoToClose(remoteRepository); remoteGit = new Git(remoteRepository); // setup the first repository to fetch from the second repository diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java new file mode 100644 index 0000000000..54c4f26099 --- /dev/null +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java @@ -0,0 +1,397 @@ +/* + * Copyright (C) 2022, Simeon Andreev <simeon.danailov.andreev@gmail.com> and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.pgm; + +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CMD; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PROMPT; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGETOOL_SECTION; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGE_SECTION; +import static org.junit.Assert.fail; + +import java.io.InputStream; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.eclipse.jgit.internal.diffmergetool.ExternalMergeTool; +import org.eclipse.jgit.internal.diffmergetool.MergeTools; +import org.eclipse.jgit.lib.StoredConfig; +import org.junit.Before; +import org.junit.Test; + +/** + * Testing the {@code mergetool} command. + */ +public class MergeToolTest extends ToolTestCase { + + private static final String MERGE_TOOL = CONFIG_MERGETOOL_SECTION; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + configureEchoTool(TOOL_NAME); + } + + @Test + public void testUndefinedTool() throws Exception { + String toolName = "undefined"; + String[] conflictingFilenames = createMergeConflict(); + + List<String> expectedErrors = new ArrayList<>(); + for (String conflictingFilename : conflictingFilenames) { + expectedErrors.add("External merge tool is not defined: " + toolName); + expectedErrors.add("merge of " + conflictingFilename + " failed"); + } + + runAndCaptureUsingInitRaw(expectedErrors, MERGE_TOOL, + "--no-prompt", "--tool", toolName); + } + + @Test(expected = Die.class) + public void testUserToolWithCommandNotFoundError() throws Exception { + String toolName = "customTool"; + + int errorReturnCode = 127; // command not found + String command = "exit " + errorReturnCode; + + StoredConfig config = db.getConfig(); + config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD, + command); + + createMergeConflict(); + runAndCaptureUsingInitRaw(MERGE_TOOL, "--no-prompt", "--tool", + toolName); + + fail("Expected exception to be thrown due to external tool exiting with error code: " + + errorReturnCode); + } + + @Test + public void testEmptyToolName() throws Exception { + assumeLinuxPlatform(); + + String emptyToolName = ""; + + StoredConfig config = db.getConfig(); + // the default merge tool is configured without a subsection + String subsection = null; + config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_TOOL, + emptyToolName); + + createMergeConflict(); + + String araxisErrorLine = "compare: unrecognized option `-wait' @ error/compare.c/CompareImageCommand/1123."; + String[] expectedErrorOutput = { araxisErrorLine, araxisErrorLine, }; + runAndCaptureUsingInitRaw(Arrays.asList(expectedErrorOutput), + MERGE_TOOL, "--no-prompt"); + } + + @Test + public void testAbortMerge() throws Exception { + String[] inputLines = { + "y", // start tool for merge resolution + "n", // don't accept merge tool result + "n", // don't continue resolution + }; + String[] conflictingFilenames = createMergeConflict(); + int abortIndex = 1; + String[] expectedOutput = getExpectedAbortMergeOutput( + conflictingFilenames, + abortIndex); + + String option = "--tool"; + + InputStream inputStream = createInputStream(inputLines); + assertArrayOfLinesEquals("Incorrect output for option: " + option, + expectedOutput, runAndCaptureUsingInitRaw(inputStream, + MERGE_TOOL, "--prompt", option, TOOL_NAME)); + } + + @Test + public void testAbortLaunch() throws Exception { + String[] inputLines = { + "n", // abort merge tool launch + }; + String[] conflictingFilenames = createMergeConflict(); + String[] expectedOutput = getExpectedAbortLaunchOutput( + conflictingFilenames); + + String option = "--tool"; + + InputStream inputStream = createInputStream(inputLines); + assertArrayOfLinesEquals("Incorrect output for option: " + option, + expectedOutput, runAndCaptureUsingInitRaw(inputStream, + MERGE_TOOL, "--prompt", option, TOOL_NAME)); + } + + @Test + public void testMergeConflict() throws Exception { + String[] inputLines = { + "y", // start tool for merge resolution + "y", // accept merge result as successful + "y", // start tool for merge resolution + "y", // accept merge result as successful + }; + String[] conflictingFilenames = createMergeConflict(); + String[] expectedOutput = getExpectedMergeConflictOutput( + conflictingFilenames); + + String option = "--tool"; + + InputStream inputStream = createInputStream(inputLines); + assertArrayOfLinesEquals("Incorrect output for option: " + option, + expectedOutput, runAndCaptureUsingInitRaw(inputStream, + MERGE_TOOL, "--prompt", option, TOOL_NAME)); + } + + @Test + public void testDeletedConflict() throws Exception { + String[] inputLines = { + "d", // choose delete option to resolve conflict + "m", // choose merge option to resolve conflict + }; + String[] conflictingFilenames = createDeletedConflict(); + String[] expectedOutput = getExpectedDeletedConflictOutput( + conflictingFilenames); + + String option = "--tool"; + + InputStream inputStream = createInputStream(inputLines); + assertArrayOfLinesEquals("Incorrect output for option: " + option, + expectedOutput, runAndCaptureUsingInitRaw(inputStream, + MERGE_TOOL, "--prompt", option, TOOL_NAME)); + } + + @Test + public void testNoConflict() throws Exception { + createStagedChanges(); + String[] expectedOutput = { "No files need merging" }; + + String[] options = { "--tool", "-t", }; + + for (String option : options) { + assertArrayOfLinesEquals("Incorrect output for option: " + option, + expectedOutput, + runAndCaptureUsingInitRaw(MERGE_TOOL, option, TOOL_NAME)); + } + } + + @Test + public void testMergeConflictNoPrompt() throws Exception { + String[] conflictingFilenames = createMergeConflict(); + String[] expectedOutput = getExpectedMergeConflictOutputNoPrompt( + conflictingFilenames); + + String option = "--tool"; + + assertArrayOfLinesEquals("Incorrect output for option: " + option, + expectedOutput, + runAndCaptureUsingInitRaw(MERGE_TOOL, option, TOOL_NAME)); + } + + @Test + public void testMergeConflictNoGuiNoPrompt() throws Exception { + String[] conflictingFilenames = createMergeConflict(); + String[] expectedOutput = getExpectedMergeConflictOutputNoPrompt( + conflictingFilenames); + + String option = "--tool"; + + assertArrayOfLinesEquals("Incorrect output for option: " + option, + expectedOutput, runAndCaptureUsingInitRaw(MERGE_TOOL, + "--no-gui", "--no-prompt", option, TOOL_NAME)); + } + + @Test + public void testToolHelp() throws Exception { + List<String> expectedOutput = new ArrayList<>(); + + MergeTools diffTools = new MergeTools(db); + Map<String, ExternalMergeTool> predefinedTools = diffTools + .getPredefinedTools(true); + List<ExternalMergeTool> availableTools = new ArrayList<>(); + List<ExternalMergeTool> notAvailableTools = new ArrayList<>(); + for (ExternalMergeTool tool : predefinedTools.values()) { + if (tool.isAvailable()) { + availableTools.add(tool); + } else { + notAvailableTools.add(tool); + } + } + + expectedOutput.add( + "'git mergetool --tool=<tool>' may be set to one of the following:"); + for (ExternalMergeTool tool : availableTools) { + String toolName = tool.getName(); + expectedOutput.add(toolName); + } + String customToolHelpLine = TOOL_NAME + "." + CONFIG_KEY_CMD + " " + + getEchoCommand(); + expectedOutput.add("user-defined:"); + expectedOutput.add(customToolHelpLine); + expectedOutput.add( + "The following tools are valid, but not currently available:"); + for (ExternalMergeTool tool : notAvailableTools) { + String toolName = tool.getName(); + expectedOutput.add(toolName); + } + String[] userDefinedToolsHelp = { + "Some of the tools listed above only work in a windowed", + "environment. If run in a terminal-only session, they will fail.", }; + expectedOutput.addAll(Arrays.asList(userDefinedToolsHelp)); + + String option = "--tool-help"; + assertArrayOfLinesEquals("Incorrect output for option: " + option, + expectedOutput.toArray(new String[0]), + runAndCaptureUsingInitRaw(MERGE_TOOL, option)); + } + + private void configureEchoTool(String toolName) { + StoredConfig config = db.getConfig(); + // the default merge tool is configured without a subsection + String subsection = null; + config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_TOOL, + toolName); + + String command = getEchoCommand(); + + config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD, + command); + /* + * prevent prompts as we are running in tests and there is no user to + * interact with on the command line + */ + config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_PROMPT, + String.valueOf(false)); + } + + private String[] getExpectedMergeConflictOutputNoPrompt( + String[] conflictFilenames) { + List<String> expected = new ArrayList<>(); + expected.add("Merging:"); + for (String conflictFilename : conflictFilenames) { + expected.add(conflictFilename); + } + for (String conflictFilename : conflictFilenames) { + expected.add("Normal merge conflict for '" + conflictFilename + + "':"); + expected.add("{local}: modified file"); + expected.add("{remote}: modified file"); + Path filePath = getFullPath(conflictFilename); + expected.add(filePath.toString()); + expected.add(conflictFilename + " seems unchanged."); + } + return expected.toArray(new String[0]); + } + + private static String[] getExpectedAbortLaunchOutput( + String[] conflictFilenames) { + List<String> expected = new ArrayList<>(); + expected.add("Merging:"); + for (String conflictFilename : conflictFilenames) { + expected.add(conflictFilename); + } + if (conflictFilenames.length > 1) { + String conflictFilename = conflictFilenames[0]; + expected.add( + "Normal merge conflict for '" + conflictFilename + "':"); + expected.add("{local}: modified file"); + expected.add("{remote}: modified file"); + expected.add("Hit return to start merge resolution tool (" + + TOOL_NAME + "):"); + } + return expected.toArray(new String[0]); + } + + private String[] getExpectedAbortMergeOutput( + String[] conflictFilenames, int abortIndex) { + List<String> expected = new ArrayList<>(); + expected.add("Merging:"); + for (String conflictFilename : conflictFilenames) { + expected.add(conflictFilename); + } + for (int i = 0; i < conflictFilenames.length; ++i) { + if (i == abortIndex) { + break; + } + + String conflictFilename = conflictFilenames[i]; + expected.add( + "Normal merge conflict for '" + conflictFilename + "':"); + expected.add("{local}: modified file"); + expected.add("{remote}: modified file"); + Path fullPath = getFullPath(conflictFilename); + expected.add("Hit return to start merge resolution tool (" + + TOOL_NAME + "): " + fullPath); + expected.add(conflictFilename + " seems unchanged."); + expected.add("Was the merge successful [y/n]?"); + if (i < conflictFilenames.length - 1) { + expected.add( + "\tContinue merging other unresolved paths [y/n]?"); + } + } + return expected.toArray(new String[0]); + } + + private String[] getExpectedMergeConflictOutput( + String[] conflictFilenames) { + List<String> expected = new ArrayList<>(); + expected.add("Merging:"); + for (String conflictFilename : conflictFilenames) { + expected.add(conflictFilename); + } + for (int i = 0; i < conflictFilenames.length; ++i) { + String conflictFilename = conflictFilenames[i]; + expected.add("Normal merge conflict for '" + conflictFilename + + "':"); + expected.add("{local}: modified file"); + expected.add("{remote}: modified file"); + Path filePath = getFullPath(conflictFilename); + expected.add("Hit return to start merge resolution tool (" + + TOOL_NAME + "): " + filePath); + expected.add(conflictFilename + " seems unchanged."); + expected.add("Was the merge successful [y/n]?"); + if (i < conflictFilenames.length - 1) { + // expected.add( + // "\tContinue merging other unresolved paths [y/n]?"); + } + } + return expected.toArray(new String[0]); + } + + private static String[] getExpectedDeletedConflictOutput( + String[] conflictFilenames) { + List<String> expected = new ArrayList<>(); + expected.add("Merging:"); + for (String mergeConflictFilename : conflictFilenames) { + expected.add(mergeConflictFilename); + } + for (int i = 0; i < conflictFilenames.length; ++i) { + String conflictFilename = conflictFilenames[i]; + expected.add(conflictFilename + " seems unchanged."); + expected.add("{local}: deleted"); + expected.add("{remote}: modified file"); + expected.add("Use (m)odified or (d)eleted file, or (a)bort?"); + } + return expected.toArray(new String[0]); + } + + private static String getEchoCommand() { + /* + * use 'MERGED' placeholder, as both 'LOCAL' and 'REMOTE' will be + * replaced with full paths to a temporary file during some of the tests + */ + return "(echo \"$MERGED\")"; + } +} diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ToolTestCase.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ToolTestCase.java new file mode 100644 index 0000000000..5f6b38c25d --- /dev/null +++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ToolTestCase.java @@ -0,0 +1,250 @@ +/* + * Copyright (C) 2022, Simeon Andreev <simeon.danailov.andreev@gmail.com> and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.pgm; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.diff.DiffEntry; +import org.eclipse.jgit.lib.CLIRepositoryTestCase; +import org.eclipse.jgit.pgm.opt.CmdLineParser; +import org.eclipse.jgit.pgm.opt.SubcommandHandler; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.treewalk.FileTreeIterator; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.util.SystemReader; +import org.junit.Assume; +import org.junit.Before; +import org.kohsuke.args4j.Argument; +import org.kohsuke.args4j.CmdLineException; + +/** + * Base test case for the {@code difftool} and {@code mergetool} commands. + */ +public abstract class ToolTestCase extends CLIRepositoryTestCase { + + public static class GitCliJGitWrapperParser { + @Argument(index = 0, metaVar = "metaVar_command", required = true, handler = SubcommandHandler.class) + TextBuiltin subcommand; + + @Argument(index = 1, metaVar = "metaVar_arg") + List<String> arguments = new ArrayList<>(); + } + + protected static final String TOOL_NAME = "some_tool"; + + private static final String TEST_BRANCH_NAME = "test_branch"; + + private Git git; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + git = new Git(db); + git.commit().setMessage("initial commit").call(); + git.branchCreate().setName(TEST_BRANCH_NAME).call(); + } + + protected String[] runAndCaptureUsingInitRaw(String... args) + throws Exception { + InputStream inputStream = null; // no input stream + return runAndCaptureUsingInitRaw(inputStream, args); + } + + protected String[] runAndCaptureUsingInitRaw( + List<String> expectedErrorOutput, String... args) throws Exception { + InputStream inputStream = null; // no input stream + return runAndCaptureUsingInitRaw(inputStream, expectedErrorOutput, + args); + } + + protected String[] runAndCaptureUsingInitRaw(InputStream inputStream, + String... args) throws Exception { + List<String> expectedErrorOutput = Collections.emptyList(); + return runAndCaptureUsingInitRaw(inputStream, expectedErrorOutput, + args); + } + + protected String[] runAndCaptureUsingInitRaw(InputStream inputStream, + List<String> expectedErrorOutput, String... args) + throws CmdLineException, Exception, IOException { + CLIGitCommand.Result result = new CLIGitCommand.Result(); + + GitCliJGitWrapperParser bean = new GitCliJGitWrapperParser(); + CmdLineParser clp = new CmdLineParser(bean); + clp.parseArgument(args); + + TextBuiltin cmd = bean.subcommand; + cmd.initRaw(db, null, inputStream, result.out, result.err); + cmd.execute(bean.arguments.toArray(new String[bean.arguments.size()])); + if (cmd.getOutputWriter() != null) { + cmd.getOutputWriter().flush(); + } + if (cmd.getErrorWriter() != null) { + cmd.getErrorWriter().flush(); + } + + List<String> errLines = result.errLines().stream() + .filter(l -> !l.isBlank()) // we care only about error messages + .collect(Collectors.toList()); + assertEquals("Expected no standard error output from tool", + expectedErrorOutput.toString(), errLines.toString()); + + return result.outLines().toArray(new String[0]); + } + + protected String[] createMergeConflict() throws Exception { + // create files on initial branch + git.checkout().setName(TEST_BRANCH_NAME).call(); + writeTrashFile("dir1/a", "Hello world a"); + writeTrashFile("dir2/b", "Hello world b"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("files a & b added").call(); + // create another branch and change files + git.branchCreate().setName("branch_1").call(); + git.checkout().setName("branch_1").call(); + writeTrashFile("dir1/a", "Hello world a 1"); + writeTrashFile("dir2/b", "Hello world b 1"); + git.add().addFilepattern(".").call(); + RevCommit commit1 = git.commit() + .setMessage("files a & b modified commit 1").call(); + // checkout initial branch + git.checkout().setName(TEST_BRANCH_NAME).call(); + // create another branch and change files + git.branchCreate().setName("branch_2").call(); + git.checkout().setName("branch_2").call(); + writeTrashFile("dir1/a", "Hello world a 2"); + writeTrashFile("dir2/b", "Hello world b 2"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("files a & b modified commit 2").call(); + // cherry-pick conflicting changes + git.cherryPick().include(commit1).call(); + String[] conflictingFilenames = { "dir1/a", "dir2/b" }; + return conflictingFilenames; + } + + protected String[] createDeletedConflict() throws Exception { + // create files on initial branch + git.checkout().setName(TEST_BRANCH_NAME).call(); + writeTrashFile("dir1/a", "Hello world a"); + writeTrashFile("dir2/b", "Hello world b"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("files a & b added").call(); + // create another branch and change files + git.branchCreate().setName("branch_1").call(); + git.checkout().setName("branch_1").call(); + writeTrashFile("dir1/a", "Hello world a 1"); + writeTrashFile("dir2/b", "Hello world b 1"); + git.add().addFilepattern(".").call(); + RevCommit commit1 = git.commit() + .setMessage("files a & b modified commit 1").call(); + // checkout initial branch + git.checkout().setName(TEST_BRANCH_NAME).call(); + // create another branch and change files + git.branchCreate().setName("branch_2").call(); + git.checkout().setName("branch_2").call(); + git.rm().addFilepattern("dir1/a").call(); + git.rm().addFilepattern("dir2/b").call(); + git.commit().setMessage("files a & b deleted commit 2").call(); + // cherry-pick conflicting changes + git.cherryPick().include(commit1).call(); + String[] conflictingFilenames = { "dir1/a", "dir2/b" }; + return conflictingFilenames; + } + + protected String[] createUnstagedChanges() throws Exception { + writeTrashFile("dir1/a", "Hello world a"); + writeTrashFile("dir2/b", "Hello world b"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("files a & b").call(); + writeTrashFile("dir1/a", "New Hello world a"); + writeTrashFile("dir2/b", "New Hello world b"); + String[] conflictingFilenames = { "dir1/a", "dir2/b" }; + return conflictingFilenames; + } + + protected String[] createStagedChanges() throws Exception { + String[] conflictingFilenames = createUnstagedChanges(); + git.add().addFilepattern(".").call(); + return conflictingFilenames; + } + + protected List<DiffEntry> getRepositoryChanges(RevCommit commit) + throws Exception { + TreeWalk tw = new TreeWalk(db); + tw.addTree(commit.getTree()); + FileTreeIterator modifiedTree = new FileTreeIterator(db); + tw.addTree(modifiedTree); + List<DiffEntry> changes = DiffEntry.scan(tw); + return changes; + } + + protected Path getFullPath(String repositoryFilename) { + Path dotGitPath = db.getDirectory().toPath(); + Path repositoryRoot = dotGitPath.getParent(); + Path repositoryFilePath = repositoryRoot.resolve(repositoryFilename); + return repositoryFilePath; + } + + protected static InputStream createInputStream(String[] inputLines) { + return createInputStream(Arrays.asList(inputLines)); + } + + protected static InputStream createInputStream(List<String> inputLines) { + String input = String.join(System.lineSeparator(), inputLines); + InputStream inputStream = new ByteArrayInputStream(input.getBytes()); + return inputStream; + } + + protected static void assertArrayOfLinesEquals(String failMessage, + String[] expected, String[] actual) { + assertEquals(failMessage, toString(expected), toString(actual)); + } + + protected static void assertArrayOfMatchingLines(String failMessage, + Pattern[] expected, String[] actual) { + assertEquals(failMessage + System.lineSeparator() + + "Expected and actual lines count don't match. Expected: " + + Arrays.asList(expected) + ", actual: " + + Arrays.asList(actual), expected.length, actual.length); + int n = expected.length; + for (int i = 0; i < n; ++i) { + Pattern expectedPattern = expected[i]; + String actualLine = actual[i]; + Matcher matcher = expectedPattern.matcher(actualLine); + boolean matches = matcher.matches(); + assertTrue(failMessage + System.lineSeparator() + "Line " + i + " '" + + actualLine + "' doesn't match expected pattern: " + + expectedPattern + System.lineSeparator() + "Expected: " + + Arrays.asList(expected) + ", actual: " + + Arrays.asList(actual), + matches); + } + } + + protected static void assumeLinuxPlatform() { + Assume.assumeTrue("This test can run only in Linux tests", + SystemReader.getInstance().isLinux()); + } +} 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 8c44764c63..ea1d1e3faa 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 @@ -25,6 +25,7 @@ org.eclipse.jgit.pgm.LsRemote org.eclipse.jgit.pgm.LsTree org.eclipse.jgit.pgm.Merge org.eclipse.jgit.pgm.MergeBase +org.eclipse.jgit.pgm.MergeTool org.eclipse.jgit.pgm.Push org.eclipse.jgit.pgm.ReceivePack org.eclipse.jgit.pgm.Reflog diff --git a/org.eclipse.jgit.pgm/build.properties b/org.eclipse.jgit.pgm/build.properties index 4b38114d09..302dded85a 100644 --- a/org.eclipse.jgit.pgm/build.properties +++ b/org.eclipse.jgit.pgm/build.properties @@ -6,4 +6,4 @@ bin.includes = META-INF/,\ .,\ plugin.properties,\ about.html,\ - resources/log4j.properties + resources/simplelogger.properties diff --git a/org.eclipse.jgit.pgm/pom.xml b/org.eclipse.jgit.pgm/pom.xml index 5cac06f3d6..e541e1a9ba 100644 --- a/org.eclipse.jgit.pgm/pom.xml +++ b/org.eclipse.jgit.pgm/pom.xml @@ -103,12 +103,7 @@ <dependency> <groupId>org.slf4j</groupId> - <artifactId>slf4j-log4j12</artifactId> - </dependency> - - <dependency> - <groupId>log4j</groupId> - <artifactId>log4j</artifactId> + <artifactId>slf4j-simple</artifactId> </dependency> <dependency> diff --git a/org.eclipse.jgit.pgm/resources/log4j.properties b/org.eclipse.jgit.pgm/resources/log4j.properties deleted file mode 100644 index 1496c5a2cf..0000000000 --- a/org.eclipse.jgit.pgm/resources/log4j.properties +++ /dev/null @@ -1,6 +0,0 @@ -log4j.rootLogger=WARN, stderr - -log4j.appender.stderr=org.apache.log4j.ConsoleAppender -log4j.appender.stderr.Target=System.err -log4j.appender.stderr.layout=org.apache.log4j.PatternLayout -log4j.appender.stderr.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
\ No newline at end of file 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 d51daafde3..b14531a1bd 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 @@ -58,9 +58,11 @@ couldNotCreateBranch=Could not create branch {0}: {1} dateInfo=Date: {0} deletedBranch=Deleted branch {0} deletedRemoteBranch=Deleted remote branch {0} -diffToolHelpSetToFollowing='git difftool --tool=<tool>' may be set to one of the following:\n{0}\n\tuser-defined:\n{1}\nThe following tools are valid, but not currently available:\n{2}\nSome of the tools listed above only work in a windowed\nenvironment. If run in a terminal-only session, they will fail. -diffToolLaunch=Viewing ({0}/{1}): '{2}'\nLaunch '{3}' [Y/n]? -diffToolDied=external diff died, stopping at {0} +diffToolHelpSetToFollowing=''git difftool --tool=<tool>'' may be set to one of the following:\n{0}\n\tuser-defined:\n{1}\nThe following tools are valid, but not currently available:\n{2}\nSome of the tools listed above only work in a windowed\nenvironment. If run in a terminal-only session, they will fail. +diffToolLaunch=Viewing ({0}/{1}): ''{2}''\nLaunch ''{3}'' [Y/n]? +diffToolDied=external diff died, stopping at path ''{0}'' due to exception: {1} +diffToolPromptToolName=This message is displayed because 'diff.tool' is not configured.\nSee 'git difftool --tool-help' or 'git help config' for more details.\n'git difftool' will now attempt to use one of the following tools:\n{0}\n +diffToolUnknownToolName=Unknown diff tool '{0}' doesNotExist={0} does not exist dontOverwriteLocalChanges=error: Your local changes to the following file would be overwritten by merge: everythingUpToDate=Everything up-to-date @@ -91,6 +93,24 @@ listeningOn=Listening on {0} logNoSignatureVerifier="No signature verifier available" mergeConflict=CONFLICT(content): Merge conflict in {0} mergeCheckoutConflict=error: Your local changes to the following files would be overwritten by merge: +mergeToolHelpSetToFollowing=''git mergetool --tool=<tool>'' may be set to one of the following:\n{0}\n\tuser-defined:\n{1}\nThe following tools are valid, but not currently available:\n{2}\nSome of the tools listed above only work in a windowed\nenvironment. If run in a terminal-only session, they will fail. +mergeToolLaunch=Hit return to start merge resolution tool ({0}): +mergeToolDied=local or remote cannot be found in cache, stopping at {0} +mergeToolNoFiles=No files need merging +mergeToolMerging=Merging:\n{0} +mergeToolUnknownConflict=\nUnknown merge conflict for ''{0}'': +mergeToolNormalConflict=\nNormal merge conflict for ''{0}'':\n '{'local'}': modified file\n '{'remote'}': modified file +mergeToolMergeFailed=merge of {0} failed +mergeToolExecutionError=excution error +mergeToolFileUnchanged=\n{0} seems unchanged. +mergeToolDeletedConflict=\nDeleted merge conflict for ''{0}'': +mergeToolDeletedConflictByUs= {local}: deleted\n {remote}: modified file +mergeToolDeletedConflictByThem= {local}: modified file\n {remote}: deleted +mergeToolContinueUnresolvedPaths=\nContinue merging other unresolved paths [y/n]? +mergeToolWasMergeSuccessfull=Was the merge successful [y/n]? +mergeToolDeletedMergeDecision=Use (m)odified or (d)eleted file, or (a)bort? +mergeToolPromptToolName=This message is displayed because 'merge.tool' is not configured.\nSee 'git mergetool --tool-help' or 'git help config' for more details.\n'git mergetool' will now attempt to use one of the following tools:\n{0}\n +mergeToolUnknownToolName=Unknown merge tool '{0}' mergeFailed=Automatic merge failed; fix conflicts and then commit the result mergeCheckoutFailed=Please, commit your changes or stash them before you can merge. mergeMadeBy=Merge made by the ''{0}'' strategy. @@ -229,6 +249,7 @@ unmergedPaths=Unmerged paths: unsupportedOperation=Unsupported operation: {0} untrackedFiles=Untracked files: updating=Updating {0}..{1} +usage_Abbrev=Instead of using the default number of hexadecimal digits (which will vary according to the number of objects in the repository with a default of 7) of the abbreviated object name, use <n> digits, or as many digits as needed to form a unique object name. An <n> of 0 will suppress long format, only showing the closest tag. usage_Aggressive=This option will cause gc to more aggressively optimize the repository at the expense of taking much more time usage_AlwaysFallback=Show uniquely abbreviated commit object as fallback usage_bareClone=Make a bare Git repository. That is, instead of creating [DIRECTORY] and placing the administrative files in [DIRECTORY]/.git, make the [DIRECTORY] itself the $GIT_DIR. @@ -254,6 +275,7 @@ usage_DisplayTheVersionOfJgit=Display the version of jgit usage_Gc=Cleanup unnecessary files and optimize the local repository usage_Glog=View commit history as a graph usage_DiffGuiTool=When git-difftool is invoked with the -g or --gui option the default diff tool will be read from the configured diff.guitool variable instead of diff.tool. +usage_MergeGuiTool=When git-mergetool is invoked with the -g or --gui option the default merge tool will be read from the configured merge.guitool variable instead of merge.tool. usage_noGui=The --no-gui option can be used to override -g or --gui setting. usage_IndexPack=Build pack index file for an existing packed archive usage_LFSDirectory=Directory to store large objects @@ -302,6 +324,7 @@ usage_Status=Show the working tree status usage_StopTrackingAFile=Stop tracking a file usage_TextHashFunctions=Scan repository to compute maximum number of collisions for hash functions usage_ToolForDiff=Use the diff tool specified by <tool>. Run git difftool --tool-help for the list of valid <tool> settings.\nIf a diff tool is not specified, git difftool will use the configuration variable diff.tool. +usage_ToolForMerge=Use the merge resolution program specified by <tool>. Run git mergetool --tool-help for the list of valid <tool> settings.\nIf a merge resolution program is not specified, git mergetool will use the configuration variable merge.tool. usage_UpdateRemoteRepositoryFromLocalRefs=Update remote repository from local refs usage_UseAll=Use all refs found in refs/ usage_UseTags=Use any tag including lightweight tags @@ -349,6 +372,7 @@ usage_date=date format, one of default, rfc, local, iso, short, raw (as defined usage_detectRenames=detect renamed files usage_diffAlgorithm=the diff algorithm to use. Currently supported are: 'myers', 'histogram' usage_DiffTool=git difftool is a Git command that allows you to compare and edit files between revisions using common diff tools.\ngit difftool is a frontend to git diff and accepts the same options and arguments. +usage_MergeTool=git-mergetool - Run merge conflict resolution tools to resolve merge conflicts.\nUse git mergetool to run one of several merge utilities to resolve merge conflicts. It is typically run after git merge. usage_directoriesToExport=directories to export usage_disableTheServiceInAllRepositories=disable the service in all repositories usage_displayAListOfAllRegisteredJgitCommands=Display a list of all registered jgit commands diff --git a/org.eclipse.jgit.pgm/resources/simplelogger.properties b/org.eclipse.jgit.pgm/resources/simplelogger.properties new file mode 100644 index 0000000000..98c19ce01d --- /dev/null +++ b/org.eclipse.jgit.pgm/resources/simplelogger.properties @@ -0,0 +1,6 @@ +org.slf4j.simpleLogger.defaultLogLevel=warn +org.slf4j.simpleLogger.logFile=System.err +org.slf4j.simpleLogger.showDateTime=true +org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss +org.slf4j.simpleLogger.showThreadName=true +org.slf4j.simpleLogger.showLogName=true diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Blame.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Blame.java index 2b49cf73d4..1a3a2f6f4b 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Blame.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Blame.java @@ -15,6 +15,7 @@ package org.eclipse.jgit.pgm; import static java.lang.Integer.valueOf; import static java.lang.Long.valueOf; +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_ABBREV_STRING_LENGTH; import static org.eclipse.jgit.lib.Constants.OBJECT_ID_STRING_LENGTH; import java.io.IOException; @@ -116,7 +117,8 @@ class Blame extends TextBuiltin { boolean autoAbbrev = abbrev == 0; if (abbrev == 0) { - abbrev = db.getConfig().getInt("core", "abbrev", 7); //$NON-NLS-1$ //$NON-NLS-2$ + abbrev = db.getConfig().getInt("core", "abbrev", //$NON-NLS-1$ //$NON-NLS-2$ + OBJECT_ID_ABBREV_STRING_LENGTH); } if (!showBlankBoundary) { root = db.getConfig().getBoolean("blame", "blankboundary", false); //$NON-NLS-1$ //$NON-NLS-2$ diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Describe.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Describe.java index 8aa119a358..116db037d4 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Describe.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Describe.java @@ -44,6 +44,9 @@ class Describe extends TextBuiltin { @Option(name = "--match", usage = "usage_Match", metaVar = "metaVar_pattern") private List<String> patterns = new ArrayList<>(); + @Option(name = "--abbrev", usage = "usage_Abbrev") + private Integer abbrev; + /** {@inheritDoc} */ @Override protected void run() { @@ -57,6 +60,9 @@ class Describe extends TextBuiltin { cmd.setTags(useTags); cmd.setAlways(always); cmd.setMatch(patterns.toArray(new String[0])); + if (abbrev != null) { + cmd.setAbbrev(abbrev.intValue()); + } String result = null; try { result = cmd.call(); diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java index d26842c641..87c71795ec 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java @@ -1,5 +1,6 @@ /* - * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com> + * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com> + * Copyright (C) 2019, Tim Neumann <tim.neumann@advantest.com> * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -11,24 +12,40 @@ package org.eclipse.jgit.pgm; import static org.eclipse.jgit.lib.Constants.HEAD; +import static org.eclipse.jgit.treewalk.TreeWalk.OperationType.CHECKOUT_OP; import java.io.BufferedOutputStream; import java.io.BufferedReader; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; +import java.nio.charset.Charset; import java.text.MessageFormat; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.TimeUnit; +import org.eclipse.jgit.diff.ContentSource; +import org.eclipse.jgit.diff.ContentSource.Pair; import org.eclipse.jgit.diff.DiffEntry; +import org.eclipse.jgit.diff.DiffEntry.Side; import org.eclipse.jgit.diff.DiffFormatter; +import org.eclipse.jgit.dircache.DirCacheCheckout; +import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata; import org.eclipse.jgit.dircache.DirCacheIterator; import org.eclipse.jgit.errors.AmbiguousObjectException; +import org.eclipse.jgit.errors.CorruptObjectException; import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.NoWorkTreeException; import org.eclipse.jgit.errors.RevisionSyntaxException; import org.eclipse.jgit.internal.diffmergetool.DiffTools; import org.eclipse.jgit.internal.diffmergetool.ExternalDiffTool; +import org.eclipse.jgit.internal.diffmergetool.FileElement; +import org.eclipse.jgit.internal.diffmergetool.PromptContinueHandler; +import org.eclipse.jgit.internal.diffmergetool.ToolException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.CoreConfig.EolStreamType; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.Repository; @@ -36,11 +53,17 @@ import org.eclipse.jgit.lib.TextProgressMonitor; import org.eclipse.jgit.lib.internal.BooleanTriState; import org.eclipse.jgit.pgm.internal.CLIText; import org.eclipse.jgit.pgm.opt.PathTreeFilterHandler; +import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.AbstractTreeIterator; import org.eclipse.jgit.treewalk.CanonicalTreeParser; import org.eclipse.jgit.treewalk.FileTreeIterator; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.WorkingTreeIterator; +import org.eclipse.jgit.treewalk.WorkingTreeOptions; +import org.eclipse.jgit.treewalk.filter.PathFilterGroup; import org.eclipse.jgit.treewalk.filter.TreeFilter; -import org.eclipse.jgit.util.StringUtils; +import org.eclipse.jgit.util.FS.ExecutionResult; +import org.eclipse.jgit.util.SystemReader; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.Option; @@ -56,9 +79,13 @@ class DiffTool extends TextBuiltin { @Argument(index = 1, metaVar = "metaVar_treeish") private AbstractTreeIterator newTree; + private Optional<String> toolName = Optional.empty(); + @Option(name = "--tool", aliases = { "-t" }, metaVar = "metaVar_tool", usage = "usage_ToolForDiff") - private String toolName; + void setToolName(String name) { + toolName = Optional.of(name); + } @Option(name = "--cached", aliases = { "--staged" }, usage = "usage_cached") private boolean cached; @@ -78,16 +105,16 @@ class DiffTool extends TextBuiltin { @Option(name = "--tool-help", usage = "usage_toolHelp") private boolean toolHelp; - private BooleanTriState gui = BooleanTriState.UNSET; + private boolean gui = false; @Option(name = "--gui", aliases = { "-g" }, usage = "usage_DiffGuiTool") void setGui(@SuppressWarnings("unused") boolean on) { - gui = BooleanTriState.TRUE; + gui = true; } @Option(name = "--no-gui", usage = "usage_noGui") void noGui(@SuppressWarnings("unused") boolean on) { - gui = BooleanTriState.FALSE; + gui = false; } private BooleanTriState trustExitCode = BooleanTriState.UNSET; @@ -105,11 +132,15 @@ class DiffTool extends TextBuiltin { @Option(name = "--", metaVar = "metaVar_paths", handler = PathTreeFilterHandler.class) private TreeFilter pathFilter = TreeFilter.ALL; + private BufferedReader inputReader; + @Override protected void init(Repository repository, String gitDir) { super.init(repository, gitDir); diffFmt = new DiffFormatter(new BufferedOutputStream(outs)); diffTools = new DiffTools(repository); + inputReader = new BufferedReader(new InputStreamReader(ins, + SystemReader.getInstance().getDefaultCharset())); } @Override @@ -118,23 +149,12 @@ class DiffTool extends TextBuiltin { if (toolHelp) { showToolHelp(); } else { - boolean showPrompt = diffTools.isInteractive(); - if (prompt != BooleanTriState.UNSET) { - showPrompt = prompt == BooleanTriState.TRUE; - } - String toolNamePrompt = toolName; - if (showPrompt) { - if (StringUtils.isEmptyOrNull(toolNamePrompt)) { - toolNamePrompt = diffTools.getDefaultToolName(gui); - } - } // get the changed files List<DiffEntry> files = getFiles(); if (files.size() > 0) { - compare(files, showPrompt, toolNamePrompt); + compare(files); } } - outw.flush(); } catch (RevisionSyntaxException | IOException e) { throw die(e.getMessage(), e); } finally { @@ -142,76 +162,131 @@ class DiffTool extends TextBuiltin { } } - private void compare(List<DiffEntry> files, boolean showPrompt, - String toolNamePrompt) throws IOException { - for (int fileIndex = 0; fileIndex < files.size(); fileIndex++) { - DiffEntry ent = files.get(fileIndex); - String mergedFilePath = ent.getNewPath(); - if (mergedFilePath.equals(DiffEntry.DEV_NULL)) { - mergedFilePath = ent.getOldPath(); - } - // check if user wants to launch compare - boolean launchCompare = true; - if (showPrompt) { - launchCompare = isLaunchCompare(fileIndex + 1, files.size(), - mergedFilePath, toolNamePrompt); + private void informUserNoTool(List<String> tools) { + try { + StringBuilder toolNames = new StringBuilder(); + for (String name : tools) { + toolNames.append(name + " "); //$NON-NLS-1$ } - if (launchCompare) { - switch (ent.getChangeType()) { - case MODIFY: - outw.println("M\t" + ent.getNewPath() //$NON-NLS-1$ - + " (" + ent.getNewId().name() + ")" //$NON-NLS-1$ //$NON-NLS-2$ - + "\t" + ent.getOldPath() //$NON-NLS-1$ - + " (" + ent.getOldId().name() + ")"); //$NON-NLS-1$ //$NON-NLS-2$ - int ret = diffTools.compare(ent.getNewPath(), - ent.getOldPath(), ent.getNewId().name(), - ent.getOldId().name(), toolName, prompt, gui, - trustExitCode); - if (ret != 0) { - throw die(MessageFormat.format( - CLIText.get().diffToolDied, mergedFilePath)); + outw.println(MessageFormat.format( + CLIText.get().diffToolPromptToolName, toolNames)); + outw.flush(); + } catch (IOException e) { + throw new IllegalStateException("Cannot output text", e); //$NON-NLS-1$ + } + } + + private class CountingPromptContinueHandler + implements PromptContinueHandler { + private final int fileIndex; + + private final int fileCount; + + private final String fileName; + + public CountingPromptContinueHandler(int fileIndex, int fileCount, + String fileName) { + this.fileIndex = fileIndex; + this.fileCount = fileCount; + this.fileName = fileName; + } + + @SuppressWarnings("boxing") + @Override + public boolean prompt(String toolToLaunchName) { + try { + boolean launchCompare = true; + outw.println(MessageFormat.format(CLIText.get().diffToolLaunch, + fileIndex, fileCount, fileName, toolToLaunchName) + + " "); //$NON-NLS-1$ + outw.flush(); + BufferedReader br = inputReader; + String line = null; + if ((line = br.readLine()) != null) { + if (!line.equalsIgnoreCase("Y")) { //$NON-NLS-1$ + launchCompare = false; } - break; - default: - break; } - } else { - break; + return launchCompare; + } catch (IOException e) { + throw new IllegalStateException("Cannot output text", e); //$NON-NLS-1$ } } } - @SuppressWarnings("boxing") - private boolean isLaunchCompare(int fileIndex, int fileCount, - String fileName, String toolNamePrompt) throws IOException { - boolean launchCompare = true; - outw.println(MessageFormat.format(CLIText.get().diffToolLaunch, - fileIndex, fileCount, fileName, toolNamePrompt)); - outw.flush(); - BufferedReader br = new BufferedReader(new InputStreamReader(ins)); - String line = null; - if ((line = br.readLine()) != null) { - if (!line.equalsIgnoreCase("Y")) { //$NON-NLS-1$ - launchCompare = false; + private void compare(List<DiffEntry> files) throws IOException { + ContentSource.Pair sourcePair = new ContentSource.Pair(source(oldTree), + source(newTree)); + try { + for (int fileIndex = 0; fileIndex < files.size(); fileIndex++) { + DiffEntry ent = files.get(fileIndex); + + String filePath = ent.getNewPath(); + if (filePath.equals(DiffEntry.DEV_NULL)) { + filePath = ent.getOldPath(); + } + + try { + FileElement local = createFileElement( + FileElement.Type.LOCAL, sourcePair, Side.OLD, ent); + FileElement remote = createFileElement( + FileElement.Type.REMOTE, sourcePair, Side.NEW, ent); + + PromptContinueHandler promptContinueHandler = new CountingPromptContinueHandler( + fileIndex + 1, files.size(), filePath); + + Optional<ExecutionResult> optionalResult = diffTools + .compare(local, remote, toolName, prompt, gui, + trustExitCode, promptContinueHandler, + this::informUserNoTool); + + if (optionalResult.isPresent()) { + ExecutionResult result = optionalResult.get(); + // TODO: check how to return the exit-code of the tool + // to jgit / java runtime ? + // int rc =... + Charset defaultCharset = SystemReader.getInstance() + .getDefaultCharset(); + outw.println( + new String(result.getStdout().toByteArray(), + defaultCharset)); + outw.flush(); + errw.println( + new String(result.getStderr().toByteArray(), + defaultCharset)); + errw.flush(); + } + } catch (ToolException e) { + outw.println(e.getResultStdout()); + outw.flush(); + errw.println(e.getMessage()); + errw.flush(); + throw die(MessageFormat.format( + CLIText.get().diffToolDied, filePath, e), e); + } } + } finally { + sourcePair.close(); } - return launchCompare; } private void showToolHelp() throws IOException { + Map<String, ExternalDiffTool> predefTools = diffTools + .getPredefinedTools(true); StringBuilder availableToolNames = new StringBuilder(); - for (String name : diffTools.getAvailableTools().keySet()) { - availableToolNames.append(String.format("\t\t%s\n", name)); //$NON-NLS-1$ - } StringBuilder notAvailableToolNames = new StringBuilder(); - for (String name : diffTools.getNotAvailableTools().keySet()) { - notAvailableToolNames.append(String.format("\t\t%s\n", name)); //$NON-NLS-1$ + for (String name : predefTools.keySet()) { + if (predefTools.get(name).isAvailable()) { + availableToolNames.append(MessageFormat.format("\t\t{0}\n", name)); //$NON-NLS-1$ + } else { + notAvailableToolNames.append(MessageFormat.format("\t\t{0}\n", name)); //$NON-NLS-1$ + } } StringBuilder userToolNames = new StringBuilder(); Map<String, ExternalDiffTool> userTools = diffTools .getUserDefinedTools(); for (String name : userTools.keySet()) { - userToolNames.append(String.format("\t\t%s.cmd %s\n", //$NON-NLS-1$ + userToolNames.append(MessageFormat.format("\t\t{0}.cmd {1}\n", //$NON-NLS-1$ name, userTools.get(name).getCommand())); } outw.println(MessageFormat.format( @@ -252,4 +327,54 @@ class DiffTool extends TextBuiltin { return files; } + private FileElement createFileElement(FileElement.Type elementType, + Pair pair, Side side, DiffEntry entry) throws NoWorkTreeException, + CorruptObjectException, IOException, ToolException { + String entryPath = side == Side.NEW ? entry.getNewPath() + : entry.getOldPath(); + FileElement fileElement = new FileElement(entryPath, elementType, + db.getWorkTree()); + if (!pair.isWorkingTreeSource(side) && !fileElement.isNullPath()) { + try (RevWalk revWalk = new RevWalk(db); + TreeWalk treeWalk = new TreeWalk(db, + revWalk.getObjectReader())) { + treeWalk.setFilter( + PathFilterGroup.createFromStrings(entryPath)); + if (side == Side.NEW) { + newTree.reset(); + treeWalk.addTree(newTree); + } else { + oldTree.reset(); + treeWalk.addTree(oldTree); + } + if (treeWalk.next()) { + final EolStreamType eolStreamType = treeWalk + .getEolStreamType(CHECKOUT_OP); + final String filterCommand = treeWalk.getFilterCommand( + Constants.ATTR_FILTER_TYPE_SMUDGE); + WorkingTreeOptions opt = db.getConfig() + .get(WorkingTreeOptions.KEY); + CheckoutMetadata checkoutMetadata = new CheckoutMetadata( + eolStreamType, filterCommand); + DirCacheCheckout.getContent(db, entryPath, + checkoutMetadata, pair.open(side, entry), opt, + new FileOutputStream( + fileElement.createTempFile(null))); + } else { + throw new ToolException("Cannot find path '" + entryPath //$NON-NLS-1$ + + "' in staging area!", //$NON-NLS-1$ + null); + } + } + } + return fileElement; + } + + private ContentSource source(AbstractTreeIterator iterator) { + if (iterator instanceof WorkingTreeIterator) { + return ContentSource.create((WorkingTreeIterator) iterator); + } + return ContentSource.create(db.newObjectReader()); + } + } 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 ca4877fb34..27a3d90fad 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 @@ -10,6 +10,8 @@ package org.eclipse.jgit.pgm; +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_ABBREV_STRING_LENGTH; + import java.io.IOException; import java.text.MessageFormat; import java.util.Map; @@ -144,8 +146,10 @@ class Merge extends TextBuiltin { case FAST_FORWARD: ObjectId oldHeadId = oldHead.getObjectId(); if (oldHeadId != null) { - String oldId = oldHeadId.abbreviate(7).name(); - String newId = result.getNewHead().abbreviate(7).name(); + String oldId = oldHeadId + .abbreviate(OBJECT_ID_ABBREV_STRING_LENGTH).name(); + String newId = result.getNewHead() + .abbreviate(OBJECT_ID_ABBREV_STRING_LENGTH).name(); outw.println(MessageFormat.format(CLIText.get().updating, oldId, newId)); } diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java new file mode 100644 index 0000000000..a382fab757 --- /dev/null +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java @@ -0,0 +1,483 @@ +/* + * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com> + * Copyright (C) 2019, Tim Neumann <tim.neumann@advantest.com> + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.pgm; + +import static org.eclipse.jgit.treewalk.TreeWalk.OperationType.CHECKOUT_OP; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.Status; +import org.eclipse.jgit.api.StatusCommand; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.diff.ContentSource; +import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.dircache.DirCacheCheckout; +import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata; +import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.dircache.DirCacheIterator; +import org.eclipse.jgit.errors.NoWorkTreeException; +import org.eclipse.jgit.errors.RevisionSyntaxException; +import org.eclipse.jgit.internal.diffmergetool.ExternalMergeTool; +import org.eclipse.jgit.internal.diffmergetool.FileElement; +import org.eclipse.jgit.internal.diffmergetool.FileElement.Type; +import org.eclipse.jgit.internal.diffmergetool.MergeTools; +import org.eclipse.jgit.internal.diffmergetool.ToolException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.CoreConfig.EolStreamType; +import org.eclipse.jgit.lib.IndexDiff.StageState; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.internal.BooleanTriState; +import org.eclipse.jgit.pgm.internal.CLIText; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.WorkingTreeOptions; +import org.eclipse.jgit.treewalk.filter.PathFilterGroup; +import org.eclipse.jgit.util.FS.ExecutionResult; +import org.eclipse.jgit.util.SystemReader; +import org.kohsuke.args4j.Argument; +import org.kohsuke.args4j.Option; +import org.kohsuke.args4j.spi.RestOfArgumentsHandler; + +@Command(name = "mergetool", common = true, usage = "usage_MergeTool") +class MergeTool extends TextBuiltin { + private MergeTools mergeTools; + + private Optional<String> toolName = Optional.empty(); + + @Option(name = "--tool", aliases = { + "-t" }, metaVar = "metaVar_tool", usage = "usage_ToolForMerge") + void setToolName(String name) { + toolName = Optional.of(name); + } + + private BooleanTriState prompt = BooleanTriState.UNSET; + + @Option(name = "--prompt", usage = "usage_prompt") + void setPrompt(@SuppressWarnings("unused") boolean on) { + prompt = BooleanTriState.TRUE; + } + + @Option(name = "--no-prompt", aliases = { "-y" }, usage = "usage_noPrompt") + void noPrompt(@SuppressWarnings("unused") boolean on) { + prompt = BooleanTriState.FALSE; + } + + @Option(name = "--tool-help", usage = "usage_toolHelp") + private boolean toolHelp; + + private boolean gui = false; + + @Option(name = "--gui", aliases = { "-g" }, usage = "usage_MergeGuiTool") + void setGui(@SuppressWarnings("unused") boolean on) { + gui = true; + } + + @Option(name = "--no-gui", usage = "usage_noGui") + void noGui(@SuppressWarnings("unused") boolean on) { + gui = false; + } + + @Argument(required = false, index = 0, metaVar = "metaVar_paths") + @Option(name = "--", metaVar = "metaVar_paths", handler = RestOfArgumentsHandler.class) + protected List<String> filterPaths; + + private BufferedReader inputReader; + + @Override + protected void init(Repository repository, String gitDir) { + super.init(repository, gitDir); + mergeTools = new MergeTools(repository); + inputReader = new BufferedReader( + new InputStreamReader(ins, + SystemReader.getInstance().getDefaultCharset())); + } + + enum MergeResult { + SUCCESSFUL, FAILED, ABORTED + } + + @Override + protected void run() { + try { + if (toolHelp) { + showToolHelp(); + } else { + // get the changed files + Map<String, StageState> files = getFiles(); + if (files.size() > 0) { + merge(files); + } else { + outw.println(CLIText.get().mergeToolNoFiles); + } + } + outw.flush(); + } catch (Exception e) { + throw die(e.getMessage(), e); + } + } + + private void informUserNoTool(List<String> tools) { + try { + StringBuilder toolNames = new StringBuilder(); + for (String name : tools) { + toolNames.append(name + " "); //$NON-NLS-1$ + } + outw.println(MessageFormat + .format(CLIText.get().mergeToolPromptToolName, toolNames)); + outw.flush(); + } catch (IOException e) { + throw new IllegalStateException("Cannot output text", e); //$NON-NLS-1$ + } + } + + private void merge(Map<String, StageState> files) throws Exception { + // sort file names + List<String> mergedFilePaths = new ArrayList<>(files.keySet()); + Collections.sort(mergedFilePaths); + // show the files + StringBuilder mergedFiles = new StringBuilder(); + for (String mergedFilePath : mergedFilePaths) { + mergedFiles.append(MessageFormat.format("{0}\n", mergedFilePath)); //$NON-NLS-1$ + } + outw.println(MessageFormat.format(CLIText.get().mergeToolMerging, + mergedFiles)); + outw.flush(); + boolean showPrompt = mergeTools.isInteractive(); + if (prompt != BooleanTriState.UNSET) { + showPrompt = prompt == BooleanTriState.TRUE; + } + // merge the files + MergeResult mergeResult = MergeResult.SUCCESSFUL; + for (String mergedFilePath : mergedFilePaths) { + // if last merge failed... + if (mergeResult == MergeResult.FAILED) { + // check if user wants to continue + if (showPrompt && !isContinueUnresolvedPaths()) { + mergeResult = MergeResult.ABORTED; + } + } + // aborted ? + if (mergeResult == MergeResult.ABORTED) { + break; + } + // get file stage state and merge + StageState fileState = files.get(mergedFilePath); + if (fileState == StageState.BOTH_MODIFIED) { + mergeResult = mergeModified(mergedFilePath, showPrompt); + } else if ((fileState == StageState.DELETED_BY_US) + || (fileState == StageState.DELETED_BY_THEM)) { + mergeResult = mergeDeleted(mergedFilePath, + fileState == StageState.DELETED_BY_US); + } else { + outw.println(MessageFormat.format( + CLIText.get().mergeToolUnknownConflict, + mergedFilePath)); + mergeResult = MergeResult.ABORTED; + } + } + } + + private MergeResult mergeModified(String mergedFilePath, boolean showPrompt) + throws Exception { + outw.println(MessageFormat.format(CLIText.get().mergeToolNormalConflict, + mergedFilePath)); + outw.flush(); + boolean isMergeSuccessful = true; + ContentSource baseSource = ContentSource.create(db.newObjectReader()); + ContentSource localSource = ContentSource.create(db.newObjectReader()); + ContentSource remoteSource = ContentSource.create(db.newObjectReader()); + // temporary directory if mergetool.writeToTemp == true + File tempDir = mergeTools.createTempDirectory(); + // the parent directory for temp files (can be same as tempDir or just + // the worktree dir) + File tempFilesParent = tempDir != null ? tempDir : db.getWorkTree(); + try { + FileElement base = null; + FileElement local = null; + FileElement remote = null; + FileElement merged = new FileElement(mergedFilePath, Type.MERGED, + db.getWorkTree()); + DirCache cache = db.readDirCache(); + try (RevWalk revWalk = new RevWalk(db); + TreeWalk treeWalk = new TreeWalk(db, + revWalk.getObjectReader())) { + treeWalk.setFilter( + PathFilterGroup.createFromStrings(mergedFilePath)); + DirCacheIterator cacheIter = new DirCacheIterator(cache); + treeWalk.addTree(cacheIter); + while (treeWalk.next()) { + if (treeWalk.isSubtree()) { + treeWalk.enterSubtree(); + continue; + } + final EolStreamType eolStreamType = treeWalk + .getEolStreamType(CHECKOUT_OP); + final String filterCommand = treeWalk.getFilterCommand( + Constants.ATTR_FILTER_TYPE_SMUDGE); + WorkingTreeOptions opt = db.getConfig() + .get(WorkingTreeOptions.KEY); + CheckoutMetadata checkoutMetadata = new CheckoutMetadata( + eolStreamType, filterCommand); + DirCacheEntry entry = treeWalk + .getTree(DirCacheIterator.class).getDirCacheEntry(); + if (entry == null) { + continue; + } + ObjectId id = entry.getObjectId(); + switch (entry.getStage()) { + case DirCacheEntry.STAGE_1: + base = new FileElement(mergedFilePath, Type.BASE); + DirCacheCheckout.getContent(db, mergedFilePath, + checkoutMetadata, + baseSource.open(mergedFilePath, id), opt, + new FileOutputStream( + base.createTempFile(tempFilesParent))); + break; + case DirCacheEntry.STAGE_2: + local = new FileElement(mergedFilePath, Type.LOCAL); + DirCacheCheckout.getContent(db, mergedFilePath, + checkoutMetadata, + localSource.open(mergedFilePath, id), opt, + new FileOutputStream( + local.createTempFile(tempFilesParent))); + break; + case DirCacheEntry.STAGE_3: + remote = new FileElement(mergedFilePath, Type.REMOTE); + DirCacheCheckout.getContent(db, mergedFilePath, + checkoutMetadata, + remoteSource.open(mergedFilePath, id), opt, + new FileOutputStream(remote + .createTempFile(tempFilesParent))); + break; + } + } + } + if ((local == null) || (remote == null)) { + throw die(MessageFormat.format(CLIText.get().mergeToolDied, + mergedFilePath)); + } + long modifiedBefore = merged.getFile().lastModified(); + try { + // TODO: check how to return the exit-code of the + // tool to jgit / java runtime ? + // int rc =... + Optional<ExecutionResult> optionalResult = mergeTools.merge( + local, remote, merged, base, tempDir, toolName, prompt, + gui, this::promptForLaunch, this::informUserNoTool); + if (optionalResult.isPresent()) { + ExecutionResult result = optionalResult.get(); + Charset defaultCharset = SystemReader.getInstance() + .getDefaultCharset(); + outw.println(new String(result.getStdout().toByteArray(), + defaultCharset)); + outw.flush(); + errw.println(new String(result.getStderr().toByteArray(), + defaultCharset)); + errw.flush(); + } else { + return MergeResult.ABORTED; + } + } catch (ToolException e) { + isMergeSuccessful = false; + outw.println(e.getResultStdout()); + outw.flush(); + errw.println(e.getMessage()); + errw.println(MessageFormat.format( + CLIText.get().mergeToolMergeFailed, mergedFilePath)); + errw.flush(); + if (e.isCommandExecutionError()) { + throw die(CLIText.get().mergeToolExecutionError, e); + } + } + // if merge was successful check file modified + if (isMergeSuccessful) { + long modifiedAfter = merged.getFile().lastModified(); + if (modifiedBefore == modifiedAfter) { + outw.println(MessageFormat.format( + CLIText.get().mergeToolFileUnchanged, + mergedFilePath)); + isMergeSuccessful = !showPrompt || isMergeSuccessful(); + } + } + // if automatically or manually successful + // -> add the file to the index + if (isMergeSuccessful) { + addFile(mergedFilePath); + } + } finally { + baseSource.close(); + localSource.close(); + remoteSource.close(); + } + return isMergeSuccessful ? MergeResult.SUCCESSFUL : MergeResult.FAILED; + } + + private MergeResult mergeDeleted(String mergedFilePath, boolean deletedByUs) + throws Exception { + outw.println(MessageFormat.format(CLIText.get().mergeToolFileUnchanged, + mergedFilePath)); + if (deletedByUs) { + outw.println(CLIText.get().mergeToolDeletedConflictByUs); + } else { + outw.println(CLIText.get().mergeToolDeletedConflictByThem); + } + int mergeDecision = getDeletedMergeDecision(); + if (mergeDecision == 1) { + // add modified file + addFile(mergedFilePath); + } else if (mergeDecision == -1) { + // remove deleted file + rmFile(mergedFilePath); + } else { + return MergeResult.ABORTED; + } + return MergeResult.SUCCESSFUL; + } + + private void addFile(String fileName) throws Exception { + try (Git git = new Git(db)) { + git.add().addFilepattern(fileName).call(); + } + } + + private void rmFile(String fileName) throws Exception { + try (Git git = new Git(db)) { + git.rm().addFilepattern(fileName).call(); + } + } + + private boolean hasUserAccepted(String message) throws IOException { + boolean yes = true; + outw.print(message + " "); //$NON-NLS-1$ + outw.flush(); + BufferedReader br = inputReader; + String line = null; + while ((line = br.readLine()) != null) { + if (line.equalsIgnoreCase("y")) { //$NON-NLS-1$ + yes = true; + break; + } else if (line.equalsIgnoreCase("n")) { //$NON-NLS-1$ + yes = false; + break; + } + outw.print(message); + outw.flush(); + } + return yes; + } + + private boolean isContinueUnresolvedPaths() throws IOException { + return hasUserAccepted(CLIText.get().mergeToolContinueUnresolvedPaths); + } + + private boolean isMergeSuccessful() throws IOException { + return hasUserAccepted(CLIText.get().mergeToolWasMergeSuccessfull); + } + + private boolean promptForLaunch(String toolNamePrompt) { + try { + boolean launch = true; + outw.print(MessageFormat.format(CLIText.get().mergeToolLaunch, + toolNamePrompt) + " "); //$NON-NLS-1$ + outw.flush(); + BufferedReader br = inputReader; + String line = null; + if ((line = br.readLine()) != null) { + if (!line.equalsIgnoreCase("y") && !line.equalsIgnoreCase("")) { //$NON-NLS-1$ //$NON-NLS-2$ + launch = false; + } + } + return launch; + } catch (IOException e) { + throw new IllegalStateException("Cannot output text", e); //$NON-NLS-1$ + } + } + + private int getDeletedMergeDecision() throws IOException { + int ret = 0; // abort + final String message = CLIText.get().mergeToolDeletedMergeDecision + + " "; //$NON-NLS-1$ + outw.print(message); + outw.flush(); + BufferedReader br = inputReader; + String line = null; + while ((line = br.readLine()) != null) { + if (line.equalsIgnoreCase("m")) { //$NON-NLS-1$ + ret = 1; // modified + break; + } else if (line.equalsIgnoreCase("d")) { //$NON-NLS-1$ + ret = -1; // deleted + break; + } else if (line.equalsIgnoreCase("a")) { //$NON-NLS-1$ + break; + } + outw.print(message); + outw.flush(); + } + return ret; + } + + private void showToolHelp() throws IOException { + Map<String, ExternalMergeTool> predefTools = mergeTools + .getPredefinedTools(true); + StringBuilder availableToolNames = new StringBuilder(); + StringBuilder notAvailableToolNames = new StringBuilder(); + for (String name : predefTools.keySet()) { + if (predefTools.get(name).isAvailable()) { + availableToolNames.append(MessageFormat.format("\t\t{0}\n", name)); //$NON-NLS-1$ + } else { + notAvailableToolNames.append(MessageFormat.format("\t\t{0}\n", name)); //$NON-NLS-1$ + } + } + StringBuilder userToolNames = new StringBuilder(); + Map<String, ExternalMergeTool> userTools = mergeTools + .getUserDefinedTools(); + for (String name : userTools.keySet()) { + userToolNames.append(MessageFormat.format("\t\t{0}.cmd {1}\n", //$NON-NLS-1$ + name, userTools.get(name).getCommand())); + } + outw.println(MessageFormat.format( + CLIText.get().mergeToolHelpSetToFollowing, availableToolNames, + userToolNames, notAvailableToolNames)); + } + + private Map<String, StageState> getFiles() throws RevisionSyntaxException, + NoWorkTreeException, GitAPIException { + Map<String, StageState> files = new TreeMap<>(); + try (Git git = new Git(db)) { + StatusCommand statusCommand = git.status(); + if (filterPaths != null && filterPaths.size() > 0) { + for (String path : filterPaths) { + statusCommand.addPath(path); + } + } + Status status = statusCommand.call(); + files = status.getConflictingStageState(); + } + return files; + } + +} diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Reflog.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Reflog.java index 030119ea34..c63532df60 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Reflog.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Reflog.java @@ -9,6 +9,8 @@ */ package org.eclipse.jgit.pgm; +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_ABBREV_STRING_LENGTH; + import java.io.IOException; import java.util.Collection; @@ -45,7 +47,8 @@ class Reflog extends TextBuiltin { private String toString(ReflogEntry entry, int i) { final StringBuilder s = new StringBuilder(); - s.append(entry.getNewId().abbreviate(7).name()); + s.append(entry.getNewId().abbreviate(OBJECT_ID_ABBREV_STRING_LENGTH) + .name()); s.append(" "); //$NON-NLS-1$ s.append(ref == null ? Constants.HEAD : Repository.shortenRefName(ref)); s.append("@{" + i + "}:"); //$NON-NLS-1$ //$NON-NLS-2$ 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 7fe5b0fa45..e06f150e51 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 @@ -139,6 +139,8 @@ public class CLIText extends TranslationBundle { /***/ public String diffToolHelpSetToFollowing; /***/ public String diffToolLaunch; /***/ public String diffToolDied; + /***/ public String diffToolPromptToolName; + /***/ public String diffToolUnknownToolName; /***/ public String doesNotExist; /***/ public String dontOverwriteLocalChanges; /***/ public String everythingUpToDate; @@ -169,6 +171,24 @@ public class CLIText extends TranslationBundle { /***/ public String logNoSignatureVerifier; /***/ public String mergeCheckoutConflict; /***/ public String mergeConflict; + /***/ public String mergeToolHelpSetToFollowing; + /***/ public String mergeToolLaunch; + /***/ public String mergeToolDied; + /***/ public String mergeToolNoFiles; + /***/ public String mergeToolMerging; + /***/ public String mergeToolUnknownConflict; + /***/ public String mergeToolNormalConflict; + /***/ public String mergeToolMergeFailed; + /***/ public String mergeToolExecutionError; + /***/ public String mergeToolFileUnchanged; + /***/ public String mergeToolDeletedConflict; + /***/ public String mergeToolDeletedConflictByUs; + /***/ public String mergeToolDeletedConflictByThem; + /***/ public String mergeToolContinueUnresolvedPaths; + /***/ public String mergeToolWasMergeSuccessfull; + /***/ public String mergeToolDeletedMergeDecision; + /***/ public String mergeToolPromptToolName; + /***/ public String mergeToolUnknownToolName; /***/ public String mergeFailed; /***/ public String mergeCheckoutFailed; /***/ public String mergeMadeBy; diff --git a/org.eclipse.jgit.ssh.apache.agent/BUILD b/org.eclipse.jgit.ssh.apache.agent/BUILD index 0c8cf838d4..f2e4d55165 100644 --- a/org.eclipse.jgit.ssh.apache.agent/BUILD +++ b/org.eclipse.jgit.ssh.apache.agent/BUILD @@ -17,6 +17,6 @@ java_library( "//lib:slf4j-api", "//lib:sshd-osgi", "//org.eclipse.jgit:jgit", - "//org.eclipse.jgit.ssh.apache:ssh-apache" + "//org.eclipse.jgit.ssh.apache:ssh-apache", ], ) diff --git a/org.eclipse.jgit.ssh.apache.agent/README.md b/org.eclipse.jgit.ssh.apache.agent/README.md new file mode 100644 index 0000000000..6d62a2fd81 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache.agent/README.md @@ -0,0 +1,82 @@ +# JGit SSH agent transport support for Apache MINA sshd + +This bundle provides optional support for communicating with an SSH agent +for SSH or SFTP authentication. It is an OSGi fragment for bundle +[org.eclipse.jgit.ssh.apache](../org.eclipse.jgit.ssh.apache/README.md), +and it provides transports for local communication with SSH agents. + +## Supported SSH agent transports + +### Linux, OS X, BSD + +On Linux, OS X, and BSD, the only transport mechanism supported is the usual +communication via a Unix domain socket. This is the only protocol the OpenSSH +SSH agent supports. A Unix domain socket appears as a special file in the file +system; this file name is typically available in the environment variable +`SSH_AUTH_SOCK`. + +The SSH config `IdentityAgent` can be set to this socket filename to specify +exactly which Unix domain socket to use, or it can be set to `SSH_AUTH_SOCK` +to use the value from that environment variable. If `IdentityAgent` is not set +at all, JGit uses `SSH_AUTH_SOCK` by default. If the variable is not set, no +SSH agent will be used. `IdentityAgent` can also be set to `none` to not use +any SSH agent. + +### Windows + +On Windows, two different transports are supported: + +* A transport over a Windows named pipe. This is used by Win32-OpenSSH, and is available for Pageant since version 0.75. +* A Pageant-specific legacy transport via shared memory; useful for Pageant and GPG's gpg-agent. + +Possible settings of `IdentityAgent` to select a particular transport are + +* `//./pipe/openssh-ssh-agent`: the Windows named pipe of Win32-OpenSSH. +* `//./pageant`: the shared-memory mechanism of Pageant. +* `none`: do not use any SSH agent. +* `//./pipe/<any_valid_pipe_name>`: use a specific Windows named pipe. + +The default transport on Windows if `IdentityAgent` is not set at all is the +Pageant shared-memory transport. Environment variable `SSH_AUTH_SOCK` needs +not be set for Pageant, and _must not_ be set for Win32-OpenSSH. + +It is also possible to use a named pipe as transport for Pageant (as of +version 0.75). Unfortunately, Pageant unnecessarily cryptographically +obfuscates the pipe name, so it is not possible for JGit to determine it +automatically. The pipe name is `pageant.<user name>.<sha256>`, for instance +`pageant.myself.c5687736ba755a70b000955cb191698aed7db221c2b0710199eb1f5298922ab5`. +A user can look up the name by starting Pageant and then running the +command `dir \\.\pipe\\` in a command shell. Once the name is known, setting +`IdentityAgent` to the pipe name as +`//./pipe/pageant.myself.c5687736ba755a70b000955cb191698aed7db221c2b0710199eb1f5298922ab5` +makes JGit use this Windows named pipe for communication with Pageant. + +(You can use forward slashes in the `~/.ssh/config` file. SSH config file +parsing has its own rules about backslashes in config files; which are +treated as escape characters in some contexts. With backslashes one would +have to write, e.g., `\\\\.\pipe\openssh-ssh-agent`.) + +With these two transport mechanisms, Pageant and Win32-OpenSSH are supported. +As for GPG: the gpg-agent can be configured to emulate ssh-agent (presumably +via a WinSockets2 "Unix domain socket" on Windows) or to emulate Pageant +(using the shared memory mechanism). Running gpg-agent with the +`enable-ssh-support` option is +[reported not to work on Windows](https://dev.gnupg.org/T3883), though. But +the PuTTY emulation in gpg-agent (option `enable-putty-support`) _should_ work, +so it should be possible to use gpg-agent instead of Pageant. + +Neither Pageant (as of version 0.76) nor Win32-OpenSSH (as of version 8.6) +support the `confirm` or lifetime constraints for `AddKeysToAgent`. gpg-agent +apparently does, even when communicating over the Pageant shared memory +mechanism. + +The ssh-agent from git bash on Windows is currently not supported. It would +need a connector handling Cygwin socket files and the Cygwin handshake over +a TCP stream socket bound to the loopback interface. The Cygwin socket file +_is_ exposed in the Windows file system at %TEMP%\ssh-XXXXXXXXXX\agent.<number>, +but it does not have a fixed name (the X's and the number are variable and +change each time ssh-agent is started). + +## Implementation + +The implementation of all transports uses JNA.
\ No newline at end of file diff --git a/org.eclipse.jgit.ssh.apache.agent/pom.xml b/org.eclipse.jgit.ssh.apache.agent/pom.xml index d48fd00f84..6b4ca30fd1 100644 --- a/org.eclipse.jgit.ssh.apache.agent/pom.xml +++ b/org.eclipse.jgit.ssh.apache.agent/pom.xml @@ -132,7 +132,6 @@ </configuration> </plugin> - <!-- New in 6.0; uncomment in 6.1 <plugin> <groupId>com.github.siom79.japicmp</groupId> <artifactId>japicmp-maven-plugin</artifactId> @@ -155,6 +154,9 @@ <includes> <include>org.eclipse.jgit.*</include> </includes> + <excludes> + <exclude>*.internal.*</exclude> + </excludes> <accessModifier>public</accessModifier> <breakBuildOnModifications>false</breakBuildOnModifications> <breakBuildOnBinaryIncompatibleModifications>false</breakBuildOnBinaryIncompatibleModifications> @@ -174,11 +176,9 @@ </execution> </executions> </plugin> - --> </plugins> </build> - <!-- New in 6.0, uncomment in 6.1 <reporting> <plugins> <plugin> @@ -210,6 +210,9 @@ <includes> <include>org.eclipse.jgit.*</include> </includes> + <excludes> + <exclude>*.internal.*</exclude> + </excludes> <accessModifier>public</accessModifier> <breakBuildOnModifications>false</breakBuildOnModifications> <breakBuildOnBinaryIncompatibleModifications>false</breakBuildOnBinaryIncompatibleModifications> @@ -223,5 +226,4 @@ </plugin> </plugins> </reporting> - --> </project> diff --git a/org.eclipse.jgit.ssh.apache.agent/resources/org/eclipse/jgit/internal/transport/sshd/agent/connector/Texts.properties b/org.eclipse.jgit.ssh.apache.agent/resources/org/eclipse/jgit/internal/transport/sshd/agent/connector/Texts.properties index 6fce083668..a3b4e91cad 100644 --- a/org.eclipse.jgit.ssh.apache.agent/resources/org/eclipse/jgit/internal/transport/sshd/agent/connector/Texts.properties +++ b/org.eclipse.jgit.ssh.apache.agent/resources/org/eclipse/jgit/internal/transport/sshd/agent/connector/Texts.properties @@ -2,9 +2,11 @@ errCloseMappedFile=Cannot close mapped file: {0} - {1} errLastError=System message for error {0} could not be retrieved, got {1} errReleaseSharedMemory=Cannot release shared memory: {0} - {1} errUnknown=unknown error +errUnknownIdentityAgent=IdentityAgent ''{0}'' unknown logErrorLoadLibrary=Cannot load socket library; SSH agent support is switched off msgCloseFailed=Cannot close SSH agent socket {0} msgConnectFailed=Could not connect to SSH agent via socket ''{0}'' +msgConnectPipeFailed=Could not connect to SSH agent via pipe ''{0}'' msgNoMappedFile=Could not create file mapping: {0} - {1} msgNoSharedMemory=Could not initialize shared memory: {0} - {1} msgPageantUnavailable=Could not connect to Pageant @@ -15,3 +17,4 @@ msgSharedMemoryFailed=Could not set up shared memory for communicating with Page msgShortRead=Short read from SSH agent, expected {0} bytes, got {1} bytes; last read() returned {2} pageant=Pageant unixDefaultAgent=ssh-agent +winOpenSsh=Win32 OpenSSH diff --git a/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/Factory.java b/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/Factory.java index d7409b0c3c..1cee1be13e 100644 --- a/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/Factory.java +++ b/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/Factory.java @@ -11,11 +11,15 @@ package org.eclipse.jgit.internal.transport.sshd.agent.connector; import java.io.File; import java.io.IOException; +import java.text.MessageFormat; import java.util.Collection; import java.util.Collections; +import java.util.List; +import java.util.Locale; import org.eclipse.jgit.transport.sshd.agent.Connector; import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory; +import org.eclipse.jgit.util.StringUtils; import org.eclipse.jgit.util.SystemReader; /** @@ -29,7 +33,20 @@ public class Factory implements ConnectorFactory { public Connector create(String identityAgent, File homeDir) throws IOException { if (SystemReader.getInstance().isWindows()) { - return new PageantConnector(); + if (StringUtils.isEmptyOrNull(identityAgent)) { + // Default. + return new PageantConnector(); + } + String winPath = identityAgent.replace('/', '\\'); + if (PageantConnector.DESCRIPTOR.getIdentityAgent() + .equalsIgnoreCase(winPath)) { + return new PageantConnector(); + } + if (winPath.toLowerCase(Locale.ROOT).startsWith("\\\\.\\pipe\\")) { //$NON-NLS-1$ + return new WinPipeConnector(winPath); + } + throw new IOException(MessageFormat.format( + Texts.get().errUnknownIdentityAgent, identityAgent)); } return new UnixDomainSocketConnector(identityAgent); } @@ -55,7 +72,11 @@ public class Factory implements ConnectorFactory { */ @Override public Collection<ConnectorDescriptor> getSupportedConnectors() { - return Collections.singleton(getDefaultConnector()); + if (SystemReader.getInstance().isWindows()) { + return List.of(PageantConnector.DESCRIPTOR, + WinPipeConnector.DESCRIPTOR); + } + return Collections.singleton(UnixDomainSocketConnector.DESCRIPTOR); } @Override diff --git a/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/LibraryHolder.java b/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/LibraryHolder.java index b09b55f817..0a592d0500 100644 --- a/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/LibraryHolder.java +++ b/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/LibraryHolder.java @@ -53,6 +53,10 @@ class LibraryHolder { kernel = Kernel32.INSTANCE; } + String systemError() { + return systemError("[{0}] - {1}"); //$NON-NLS-1$ + } + String systemError(String pattern) { int lastError = kernel.GetLastError(); String msg; diff --git a/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/PageantConnector.java b/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/PageantConnector.java index b0e3bce724..19684ecb93 100644 --- a/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/PageantConnector.java +++ b/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/PageantConnector.java @@ -26,7 +26,10 @@ public class PageantConnector extends AbstractConnector { @Override public String getIdentityAgent() { - return "pageant"; //$NON-NLS-1$ + // This must be an absolute Windows path name to avoid that + // OpenSshConfigFile treats it as a relative path name. Use an UNC + // name on localhost, like for pipes. + return "\\\\.\\pageant"; //$NON-NLS-1$ } @Override diff --git a/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/Sockets.java b/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/Sockets.java index 3d95bdb51c..52cf5f22f2 100644 --- a/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/Sockets.java +++ b/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/Sockets.java @@ -24,11 +24,6 @@ public final class Sockets { } /** - * Default SSH agent socket environment variable name. - */ - public static final String ENV_SSH_AUTH_SOCK = "SSH_AUTH_SOCK"; //$NON-NLS-1$ - - /** * Domain for Unix domain sockets. */ public static final int AF_UNIX = 1; diff --git a/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/Texts.java b/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/Texts.java index fb45b30dd2..f387c76ad8 100644 --- a/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/Texts.java +++ b/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/Texts.java @@ -31,9 +31,11 @@ public final class Texts extends TranslationBundle { /***/ public String errLastError; /***/ public String errReleaseSharedMemory; /***/ public String errUnknown; + /***/ public String errUnknownIdentityAgent; /***/ public String logErrorLoadLibrary; /***/ public String msgCloseFailed; /***/ public String msgConnectFailed; + /***/ public String msgConnectPipeFailed; /***/ public String msgNoMappedFile; /***/ public String msgNoSharedMemory; /***/ public String msgPageantUnavailable; @@ -44,5 +46,6 @@ public final class Texts extends TranslationBundle { /***/ public String msgShortRead; /***/ public String pageant; /***/ public String unixDefaultAgent; + /***/ public String winOpenSsh; } diff --git a/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/UnixDomainSocketConnector.java b/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/UnixDomainSocketConnector.java index 3b75f3a7da..95ac34f940 100644 --- a/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/UnixDomainSocketConnector.java +++ b/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/UnixDomainSocketConnector.java @@ -11,10 +11,10 @@ package org.eclipse.jgit.internal.transport.sshd.agent.connector; import static org.eclipse.jgit.internal.transport.sshd.agent.connector.Sockets.AF_UNIX; import static org.eclipse.jgit.internal.transport.sshd.agent.connector.Sockets.DEFAULT_PROTOCOL; -import static org.eclipse.jgit.internal.transport.sshd.agent.connector.Sockets.ENV_SSH_AUTH_SOCK; import static org.eclipse.jgit.internal.transport.sshd.agent.connector.Sockets.SOCK_STREAM; import static org.eclipse.jgit.internal.transport.sshd.agent.connector.UnixSockets.FD_CLOEXEC; import static org.eclipse.jgit.internal.transport.sshd.agent.connector.UnixSockets.F_SETFD; +import static org.eclipse.jgit.transport.SshConstants.ENV_SSH_AUTH_SOCKET; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -46,7 +46,7 @@ public class UnixDomainSocketConnector extends AbstractConnector { @Override public String getIdentityAgent() { - return ENV_SSH_AUTH_SOCK; + return ENV_SSH_AUTH_SOCKET; } @Override @@ -91,8 +91,9 @@ public class UnixDomainSocketConnector extends AbstractConnector { public UnixDomainSocketConnector(String socketFile) { super(); String file = socketFile; - if (StringUtils.isEmptyOrNull(file)) { - file = SystemReader.getInstance().getenv(ENV_SSH_AUTH_SOCK); + if (StringUtils.isEmptyOrNull(file) + || ENV_SSH_AUTH_SOCKET.equals(file)) { + file = SystemReader.getInstance().getenv(ENV_SSH_AUTH_SOCKET); } this.socketFile = file; } diff --git a/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/WinPipeConnector.java b/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/WinPipeConnector.java new file mode 100644 index 0000000000..81c653722f --- /dev/null +++ b/org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/WinPipeConnector.java @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.internal.transport.sshd.agent.connector; + +import java.io.IOException; +import java.text.MessageFormat; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.sshd.common.SshException; +import org.eclipse.jgit.transport.sshd.agent.AbstractConnector; +import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory.ConnectorDescriptor; +import org.eclipse.jgit.util.StringUtils; + +import com.sun.jna.LastErrorException; +import com.sun.jna.platform.win32.WinBase; +import com.sun.jna.platform.win32.WinError; +import com.sun.jna.platform.win32.WinNT; +import com.sun.jna.platform.win32.WinNT.HANDLE; +import com.sun.jna.ptr.IntByReference; + +/** + * A connector based on JNA using Windows' named pipes to communicate with an + * ssh agent. This is used by Microsoft's Win32-OpenSSH port. + */ +public class WinPipeConnector extends AbstractConnector { + + // Pipe names are, like other file names, case-insensitive on Windows. + private static final String CANONICAL_PIPE_NAME = "\\\\.\\pipe\\openssh-ssh-agent"; //$NON-NLS-1$ + + /** + * {@link ConnectorDescriptor} for the {@link PageantConnector}. + */ + public static final ConnectorDescriptor DESCRIPTOR = new ConnectorDescriptor() { + + @Override + public String getIdentityAgent() { + return CANONICAL_PIPE_NAME; + } + + @Override + public String getDisplayName() { + return Texts.get().winOpenSsh; + } + }; + + private static final int FILE_SHARE_NONE = 0; + + private static final int FILE_ATTRIBUTE_NONE = 0; + + private final String pipeName; + + private final AtomicBoolean connected = new AtomicBoolean(); + + // It's a byte pipe, so the normal Windows file mechanisms can be used. + // Would one of the standard Java File I/O abstractions work? + private volatile HANDLE fileHandle; + + /** + * Creates a {@link WinPipeConnector} for the given named pipe. + * + * @param pipeName + * to connect to + */ + public WinPipeConnector(String pipeName) { + this.pipeName = pipeName.replace('/', '\\'); + } + + @Override + public boolean connect() throws IOException { + if (StringUtils.isEmptyOrNull(pipeName)) { + return false; + } + HANDLE file = fileHandle; + synchronized (this) { + if (connected.get()) { + return true; + } + LibraryHolder libs = LibraryHolder.getLibrary(); + if (libs == null) { + return false; + } + file = libs.kernel.CreateFile(pipeName, + WinNT.GENERIC_READ | WinNT.GENERIC_WRITE, FILE_SHARE_NONE, + null, WinNT.OPEN_EXISTING, FILE_ATTRIBUTE_NONE, null); + if (file == null || WinBase.INVALID_HANDLE_VALUE.equals(file)) { + int errorCode = libs.kernel.GetLastError(); + if (errorCode == WinError.ERROR_FILE_NOT_FOUND + && CANONICAL_PIPE_NAME.equalsIgnoreCase(pipeName)) { + // OpenSSH agent not running. Don't throw. + return false; + } + LastErrorException cause = new LastErrorException( + libs.systemError()); + throw new IOException(MessageFormat + .format(Texts.get().msgConnectPipeFailed, pipeName), + cause); + } + connected.set(true); + } + fileHandle = file; + return connected.get(); + } + + @Override + public synchronized void close() throws IOException { + HANDLE file = fileHandle; + if (connected.getAndSet(false) && fileHandle != null) { + fileHandle = null; + LibraryHolder libs = LibraryHolder.getLibrary(); + boolean success = libs.kernel.CloseHandle(file); + if (!success) { + LastErrorException cause = new LastErrorException( + libs.systemError()); + throw new IOException(MessageFormat + .format(Texts.get().msgCloseFailed, pipeName), cause); + } + } + } + + @Override + public byte[] rpc(byte command, byte[] message) throws IOException { + prepareMessage(command, message); + HANDLE file = fileHandle; + if (!connected.get() || file == null) { + // No translation, internal error + throw new IllegalStateException("Not connected to SSH agent"); //$NON-NLS-1$ + } + LibraryHolder libs = LibraryHolder.getLibrary(); + writeFully(libs, file, message); + // Now receive the reply + byte[] lengthBuf = new byte[4]; + readFully(libs, file, lengthBuf); + int length = toLength(command, lengthBuf); + byte[] payload = new byte[length]; + readFully(libs, file, payload); + return payload; + } + + private void writeFully(LibraryHolder libs, HANDLE file, byte[] message) + throws IOException { + byte[] buf = message; + int toWrite = buf.length; + try { + while (toWrite > 0) { + IntByReference written = new IntByReference(); + boolean success = libs.kernel.WriteFile(file, buf, buf.length, + written, null); + if (!success) { + throw new LastErrorException(libs.systemError()); + } + int actuallyWritten = written.getValue(); + toWrite -= actuallyWritten; + if (actuallyWritten > 0 && toWrite > 0) { + buf = Arrays.copyOfRange(buf, actuallyWritten, buf.length); + } + } + } catch (LastErrorException e) { + throw new IOException(MessageFormat.format( + Texts.get().msgSendFailed, Integer.toString(message.length), + Integer.toString(toWrite)), e); + } + } + + private void readFully(LibraryHolder libs, HANDLE file, byte[] data) + throws IOException { + int n = 0; + int offset = 0; + while (offset < data.length && (n = read(libs, file, data, offset, + data.length - offset)) > 0) { + offset += n; + } + if (offset < data.length) { + throw new SshException(MessageFormat.format( + Texts.get().msgShortRead, Integer.toString(data.length), + Integer.toString(offset), Integer.toString(n))); + } + } + + private int read(LibraryHolder libs, HANDLE file, byte[] buffer, int offset, + int length) throws IOException { + try { + int toRead = length; + IntByReference read = new IntByReference(); + if (offset == 0) { + boolean success = libs.kernel.ReadFile(file, buffer, toRead, + read, null); + if (!success) { + throw new LastErrorException(libs.systemError()); + } + return read.getValue(); + } + byte[] data = new byte[length]; + boolean success = libs.kernel.ReadFile(file, buffer, toRead, read, + null); + if (!success) { + throw new LastErrorException(libs.systemError()); + } + int actuallyRead = read.getValue(); + if (actuallyRead > 0) { + System.arraycopy(data, 0, buffer, offset, actuallyRead); + } + return actuallyRead; + } catch (LastErrorException e) { + throw new IOException(MessageFormat.format( + Texts.get().msgReadFailed, Integer.toString(length)), e); + } + } +} diff --git a/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF index 9e4a8f7d50..0d046c86bb 100644 --- a/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF @@ -7,20 +7,20 @@ Bundle-Version: 7.0.0.qualifier Bundle-Vendor: %Bundle-Vendor Bundle-Localization: plugin Bundle-RequiredExecutionEnvironment: JavaSE-11 -Import-Package: org.apache.sshd.client.config.hosts;version="[2.7.0,2.8.0)", - org.apache.sshd.common;version="[2.7.0,2.8.0)", - org.apache.sshd.common.auth;version="[2.7.0,2.8.0)", - org.apache.sshd.common.config.keys;version="[2.7.0,2.8.0)", - org.apache.sshd.common.helpers;version="[2.7.0,2.8.0)", - org.apache.sshd.common.kex;version="[2.7.0,2.8.0)", - org.apache.sshd.common.keyprovider;version="[2.7.0,2.8.0)", - org.apache.sshd.common.session;version="[2.7.0,2.8.0)", - org.apache.sshd.common.signature;version="[2.7.0,2.8.0)", - org.apache.sshd.common.util.net;version="[2.7.0,2.8.0)", - org.apache.sshd.common.util.security;version="[2.7.0,2.8.0)", - org.apache.sshd.core;version="[2.7.0,2.8.0)", - org.apache.sshd.server;version="[2.7.0,2.8.0)", - org.apache.sshd.server.forward;version="[2.7.0,2.8.0)", +Import-Package: org.apache.sshd.client.config.hosts;version="[2.8.0,2.9.0)", + org.apache.sshd.common;version="[2.8.0,2.9.0)", + org.apache.sshd.common.auth;version="[2.8.0,2.9.0)", + org.apache.sshd.common.config.keys;version="[2.8.0,2.9.0)", + org.apache.sshd.common.helpers;version="[2.8.0,2.9.0)", + org.apache.sshd.common.kex;version="[2.8.0,2.9.0)", + org.apache.sshd.common.keyprovider;version="[2.8.0,2.9.0)", + org.apache.sshd.common.session;version="[2.8.0,2.9.0)", + org.apache.sshd.common.signature;version="[2.8.0,2.9.0)", + org.apache.sshd.common.util.net;version="[2.8.0,2.9.0)", + org.apache.sshd.common.util.security;version="[2.8.0,2.9.0)", + org.apache.sshd.core;version="[2.8.0,2.9.0)", + org.apache.sshd.server;version="[2.8.0,2.9.0)", + org.apache.sshd.server.forward;version="[2.8.0,2.9.0)", org.eclipse.jgit.api;version="[7.0.0,7.1.0)", org.eclipse.jgit.api.errors;version="[7.0.0,7.1.0)", org.eclipse.jgit.internal.transport.sshd.proxy;version="[7.0.0,7.1.0)", diff --git a/org.eclipse.jgit.ssh.apache.test/build.properties b/org.eclipse.jgit.ssh.apache.test/build.properties index 406c5a768f..35d7145160 100644 --- a/org.eclipse.jgit.ssh.apache.test/build.properties +++ b/org.eclipse.jgit.ssh.apache.test/build.properties @@ -3,5 +3,4 @@ output.. = bin/ bin.includes = META-INF/,\ .,\ plugin.properties -additional.bundles = org.apache.log4j,\ - org.slf4j.binding.log4j12 +additional.bundles = org.slf4j.binding.simple diff --git a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshTest.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshTest.java index ccaf98ced0..a8fcca7b8e 100644 --- a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshTest.java +++ b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018, 2020 Thomas Wolf <thomas.wolf@paranor.ch> and others + * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -107,6 +107,32 @@ public class ApacheSshTest extends SshTestBase { "IdentityFile " + privateKey1.getAbsolutePath()); } + /** + * Test for SSHD-1231. If authentication is attempted first with an RSA key, + * which is rejected, and then with some other key type (here ed25519), + * authentication fails in bug SSHD-1231. + * + * @throws Exception + * on errors + * @see <a href= + * "https://issues.apache.org/jira/browse/SSHD-1231">SSHD-1231</a> + */ + @Test + public void testWrongKeyFirst() throws Exception { + File userKey = new File(getTemporaryDirectory(), "userkey"); + copyTestResource("id_ed25519", userKey); + File publicKey = new File(getTemporaryDirectory(), "userkey.pub"); + copyTestResource("id_ed25519.pub", publicKey); + server.setTestUserPublicKey(publicKey.toPath()); + cloneWith("ssh://git/doesntmatter", defaultCloneDir, null, // + "Host git", // + "HostName localhost", // + "Port " + testPort, // + "User " + TEST_USER, // + "IdentityFile " + privateKey1.getAbsolutePath(), // RSA + "IdentityFile " + userKey.getAbsolutePath()); + } + @Test public void testHashedKnownHosts() throws Exception { assertTrue("Failed to delete known_hosts", knownHosts.delete()); @@ -763,4 +789,76 @@ public class ApacheSshTest extends SshTestBase { session.disconnect(); } } + + private void verifyAuthLog(String message, String first) { + assertTrue(message.contains(System.lineSeparator())); + String[] lines = message.split(System.lineSeparator()); + int pubkeyIndex = -1; + int passwordIndex = -1; + for (int i = 0; i < lines.length; i++) { + String line = lines[i]; + if (i == 0) { + assertTrue(line.contains(first)); + } + if (line.contains("publickey:")) { + if (pubkeyIndex < 0) { + pubkeyIndex = i; + assertTrue(line.contains("/userkey")); + } + } else if (line.contains("password:")) { + if (passwordIndex < 0) { + passwordIndex = i; + assertTrue(line.contains("attempt 1")); + } + } + } + assertTrue(pubkeyIndex > 0 && passwordIndex > 0); + assertTrue(pubkeyIndex < passwordIndex); + } + + @Test + public void testAuthFailureMessageCancel() throws Exception { + File userKey = new File(getTemporaryDirectory(), "userkey"); + copyTestResource("id_ed25519", userKey); + File publicKey = new File(getTemporaryDirectory(), "userkey.pub"); + copyTestResource("id_ed25519.pub", publicKey); + // Don't set this as the user's key; we do want to try with a wrong key. + server.enablePasswordAuthentication(); + TestCredentialsProvider provider = new TestCredentialsProvider( + "wrongpass"); + TransportException e = assertThrows(TransportException.class, + () -> cloneWith("ssh://git/doesntmatter", defaultCloneDir, + provider, // + "Host git", // + "HostName localhost", // + "Port " + testPort, // + "User " + TEST_USER, // + "IdentityFile " + userKey.getAbsolutePath(), // + "PreferredAuthentications publickey,password")); + verifyAuthLog(e.getMessage(), "canceled"); + } + + @Test + public void testAuthFailureMessage() throws Exception { + File userKey = new File(getTemporaryDirectory(), "userkey"); + copyTestResource("id_ed25519", userKey); + File publicKey = new File(getTemporaryDirectory(), "userkey.pub"); + copyTestResource("id_ed25519.pub", publicKey); + // Don't set this as the user's key; we do want to try with a wrong key. + server.enablePasswordAuthentication(); + // Enough passwords not to cancel authentication + TestCredentialsProvider provider = new TestCredentialsProvider( + "wrongpass", "wrongpass", "wrongpass"); + TransportException e = assertThrows(TransportException.class, + () -> cloneWith("ssh://git/doesntmatter", defaultCloneDir, + provider, // + "Host git", // + "HostName localhost", // + "Port " + testPort, // + "User " + TEST_USER, // + "IdentityFile " + userKey.getAbsolutePath(), // + "PreferredAuthentications publickey,password")); + verifyAuthLog(e.getMessage(), "log in"); + } + } diff --git a/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF b/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF index 3bec647dfa..f24ff80218 100644 --- a/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF @@ -35,54 +35,57 @@ Export-Package: org.eclipse.jgit.internal.transport.sshd;version="7.0.0";x-inter org.apache.sshd.client.keyverifier", org.eclipse.jgit.transport.sshd.agent;version="7.0.0" Import-Package: net.i2p.crypto.eddsa;version="[0.3.0,0.4.0)", - org.apache.sshd.agent;version="[2.7.0,2.8.0)", - org.apache.sshd.client;version="[2.7.0,2.8.0)", - org.apache.sshd.client.auth;version="[2.7.0,2.8.0)", - org.apache.sshd.client.auth.keyboard;version="[2.7.0,2.8.0)", - org.apache.sshd.client.auth.password;version="[2.7.0,2.8.0)", - org.apache.sshd.client.auth.pubkey;version="[2.7.0,2.8.0)", - org.apache.sshd.client.channel;version="[2.7.0,2.8.0)", - org.apache.sshd.client.config.hosts;version="[2.7.0,2.8.0)", - org.apache.sshd.client.config.keys;version="[2.7.0,2.8.0)", - org.apache.sshd.client.future;version="[2.7.0,2.8.0)", - org.apache.sshd.client.keyverifier;version="[2.7.0,2.8.0)", - org.apache.sshd.client.session;version="[2.7.0,2.8.0)", - org.apache.sshd.client.session.forward;version="[2.7.0,2.8.0)", - org.apache.sshd.common;version="[2.7.0,2.8.0)", - org.apache.sshd.common.auth;version="[2.7.0,2.8.0)", - org.apache.sshd.common.channel;version="[2.7.0,2.8.0)", - org.apache.sshd.common.compression;version="[2.7.0,2.8.0)", - org.apache.sshd.common.config.keys;version="[2.7.0,2.8.0)", - org.apache.sshd.common.config.keys.loader;version="[2.7.0,2.8.0)", - org.apache.sshd.common.config.keys.loader.openssh.kdf;version="[2.7.0,2.8.0)", - org.apache.sshd.common.digest;version="[2.7.0,2.8.0)", - org.apache.sshd.common.forward;version="[2.7.0,2.8.0)", - org.apache.sshd.common.future;version="[2.7.0,2.8.0)", - org.apache.sshd.common.helpers;version="[2.7.0,2.8.0)", - org.apache.sshd.common.io;version="[2.7.0,2.8.0)", - org.apache.sshd.common.kex;version="[2.7.0,2.8.0)", - org.apache.sshd.common.kex.extension;version="[2.7.0,2.8.0)", - org.apache.sshd.common.kex.extension.parser;version="[2.7.0,2.8.0)", - org.apache.sshd.common.keyprovider;version="[2.7.0,2.8.0)", - org.apache.sshd.common.mac;version="[2.7.0,2.8.0)", - org.apache.sshd.common.random;version="[2.7.0,2.8.0)", - org.apache.sshd.common.session;version="[2.7.0,2.8.0)", - org.apache.sshd.common.session.helpers;version="[2.7.0,2.8.0)", - org.apache.sshd.common.signature;version="[2.7.0,2.8.0)", - org.apache.sshd.common.util;version="[2.7.0,2.8.0)", - org.apache.sshd.common.util.buffer;version="[2.7.0,2.8.0)", - org.apache.sshd.common.util.closeable;version="[2.7.0,2.8.0)", - org.apache.sshd.common.util.io;version="[2.7.0,2.8.0)", - org.apache.sshd.common.util.io.functors;version="[2.7.0,2.8.0)", - org.apache.sshd.common.util.io.resource;version="[2.7.0,2.8.0)", - org.apache.sshd.common.util.logging;version="[2.7.0,2.8.0)", - org.apache.sshd.common.util.net;version="[2.7.0,2.8.0)", - org.apache.sshd.common.util.security;version="[2.7.0,2.8.0)", - org.apache.sshd.core;version="[2.7.0,2.8.0)", - org.apache.sshd.server.auth;version="[2.7.0,2.8.0)", - org.apache.sshd.sftp;version="[2.7.0,2.8.0)", - org.apache.sshd.sftp.client;version="[2.7.0,2.8.0)", - org.apache.sshd.sftp.common;version="[2.7.0,2.8.0)", + org.apache.sshd.agent;version="[2.8.0,2.9.0)", + org.apache.sshd.client;version="[2.8.0,2.9.0)", + org.apache.sshd.client.auth;version="[2.8.0,2.9.0)", + org.apache.sshd.client.auth.keyboard;version="[2.8.0,2.9.0)", + org.apache.sshd.client.auth.password;version="[2.8.0,2.9.0)", + org.apache.sshd.client.auth.pubkey;version="[2.8.0,2.9.0)", + org.apache.sshd.client.channel;version="[2.8.0,2.9.0)", + org.apache.sshd.client.config.hosts;version="[2.8.0,2.9.0)", + org.apache.sshd.client.config.keys;version="[2.8.0,2.9.0)", + org.apache.sshd.client.future;version="[2.8.0,2.9.0)", + org.apache.sshd.client.keyverifier;version="[2.8.0,2.9.0)", + org.apache.sshd.client.session;version="[2.8.0,2.9.0)", + org.apache.sshd.client.session.forward;version="[2.8.0,2.9.0)", + org.apache.sshd.common;version="[2.8.0,2.9.0)", + org.apache.sshd.common.auth;version="[2.8.0,2.9.0)", + org.apache.sshd.common.channel;version="[2.8.0,2.9.0)", + org.apache.sshd.common.compression;version="[2.8.0,2.9.0)", + org.apache.sshd.common.config.keys;version="[2.8.0,2.9.0)", + org.apache.sshd.common.config.keys.loader;version="[2.8.0,2.9.0)", + org.apache.sshd.common.config.keys.loader.openssh.kdf;version="[2.8.0,2.9.0)", + org.apache.sshd.common.config.keys.u2f;version="[2.8.0,2.9.0)", + org.apache.sshd.common.digest;version="[2.8.0,2.9.0)", + org.apache.sshd.common.forward;version="[2.8.0,2.9.0)", + org.apache.sshd.common.future;version="[2.8.0,2.9.0)", + org.apache.sshd.common.helpers;version="[2.8.0,2.9.0)", + org.apache.sshd.common.io;version="[2.8.0,2.9.0)", + org.apache.sshd.common.kex;version="[2.8.0,2.9.0)", + org.apache.sshd.common.kex.extension;version="[2.8.0,2.9.0)", + org.apache.sshd.common.kex.extension.parser;version="[2.8.0,2.9.0)", + org.apache.sshd.common.keyprovider;version="[2.8.0,2.9.0)", + org.apache.sshd.common.mac;version="[2.8.0,2.9.0)", + org.apache.sshd.common.random;version="[2.8.0,2.9.0)", + org.apache.sshd.common.session;version="[2.8.0,2.9.0)", + org.apache.sshd.common.session.helpers;version="[2.8.0,2.9.0)", + org.apache.sshd.common.signature;version="[2.8.0,2.9.0)", + org.apache.sshd.common.util;version="[2.8.0,2.9.0)", + org.apache.sshd.common.util.buffer;version="[2.8.0,2.9.0)", + org.apache.sshd.common.util.buffer.keys;version="[2.8.0,2.9.0)", + org.apache.sshd.common.util.closeable;version="[2.8.0,2.9.0)", + org.apache.sshd.common.util.io;version="[2.8.0,2.9.0)", + org.apache.sshd.common.util.io.der;version="[2.8.0,2.9.0)", + org.apache.sshd.common.util.io.functors;version="[2.8.0,2.9.0)", + org.apache.sshd.common.util.io.resource;version="[2.8.0,2.9.0)", + org.apache.sshd.common.util.logging;version="[2.8.0,2.9.0)", + org.apache.sshd.common.util.net;version="[2.8.0,2.9.0)", + org.apache.sshd.common.util.security;version="[2.8.0,2.9.0)", + org.apache.sshd.core;version="[2.8.0,2.9.0)", + org.apache.sshd.server.auth;version="[2.8.0,2.9.0)", + org.apache.sshd.sftp;version="[2.8.0,2.9.0)", + org.apache.sshd.sftp.client;version="[2.8.0,2.9.0)", + org.apache.sshd.sftp.common;version="[2.8.0,2.9.0)", org.eclipse.jgit.annotations;version="[7.0.0,7.1.0)", org.eclipse.jgit.errors;version="[7.0.0,7.1.0)", org.eclipse.jgit.fnmatch;version="[7.0.0,7.1.0)", diff --git a/org.eclipse.jgit.ssh.apache/README.md b/org.eclipse.jgit.ssh.apache/README.md index cba87ac9cc..f06b2f6071 100644 --- a/org.eclipse.jgit.ssh.apache/README.md +++ b/org.eclipse.jgit.ssh.apache/README.md @@ -43,6 +43,53 @@ FetchCommand fetch = git.fetch() .call(); ``` +## Support for SSH agents + +There exist two IETF draft RFCs for communication with an SSH agent: + +* an older [SSH1 protocol](https://tools.ietf.org/html/draft-ietf-secsh-agent-02) that can deal only with DSA and RSA keys, and +* a newer [SSH2 protocol](https://tools.ietf.org/html/draft-miller-ssh-agent-04) (from OpenSSH). + +JGit only supports the newer OpenSSH protocol. + +Communication with an SSH agent can occur over any transport protocol, and different +SSH agents may use different transports for local communication. JGit provides some +transports via the [org.eclipse.jgit.ssh.apache.agent](../org.eclipse.jgit.ssh.apache.agent/README.md) +fragment, which are discovered from `org.eclipse.jgit.ssh.apache` also via the `ServiceLoader` mechanism; +the SPI (service provider interface) is `org.eclipse.jgit.transport.sshd.agent.ConnectorFactory`. + +If such a `ConnectorFactory` implementation is found, JGit may use an SSH agent. If none +is available, JGit cannot communicate with an SSH agent, and will not attempt to use one. + +### SSH configurations for SSH agents + +There are several SSH properties that can be used in the `~/.ssh/config` file to configure +the use of an SSH agent. For the details, see the [OpenBSD ssh-config documentation](https://man.openbsd.org/ssh_config.5). + +* **AddKeysToAgent** can be set to `no`, `yes`, or `ask`. If set to `yes`, keys will be added + to the agent if they're not yet in the agent. If set to `ask`, the user will be prompted + before doing so, and can opt out of adding the key. JGit also supports the additional + settings `confirm` and key lifetimes. +* **IdentityAgent** can be set to choose which SSH agent to use, if there are several running. + It can also be set to `none` to explicitly switch off using an SSH agent at all. +* **IdentitiesOnly** if set to `yes` and an SSH agent is used, only keys from the agent that are + also listed in an `IdentityFile` property will be considered. (It'll also switch off trying + default key names, such as `~/.ssh/id_rsa` or `~/.ssh/id_ed25519`; only keys listed explicitly + will be used.) + +### Limitations + +As mentioned above JGit only implements the newer OpenSSH protocol. OpenSSH fully implements this, +but some other SSH agents only offer partial implementations. In particular on Windows, neither +Pageant nor Win32-OpenSSH implement the `confirm` or lifetime constraints for `AddKeysToAgent`. With +such SSH agents, these settings should not be used in `~/.ssh/config`. GPG's gpg-agent can be run +with option `enable_putty_support` and can then be used as a Pageant replacement. gpg-agent appears +to support these key constraints. + +OpenSSH does not implement ed448 keys, and neither does Apache MINA sshd, and hence such keys are +not supported in JGit if its built-in SSH implementation is used. ed448 or other unsupported keys +provided by an SSH agent are ignored. + ## Using a different SSH implementation To use a different SSH implementation: diff --git a/org.eclipse.jgit.ssh.apache/pom.xml b/org.eclipse.jgit.ssh.apache/pom.xml index 024dae8524..3ea8b63ab7 100644 --- a/org.eclipse.jgit.ssh.apache/pom.xml +++ b/org.eclipse.jgit.ssh.apache/pom.xml @@ -154,6 +154,9 @@ <includes> <include>org.eclipse.jgit.*</include> </includes> + <excludes> + <exclude>*.internal.*</exclude> + </excludes> <accessModifier>public</accessModifier> <breakBuildOnModifications>false</breakBuildOnModifications> <breakBuildOnBinaryIncompatibleModifications>false</breakBuildOnBinaryIncompatibleModifications> @@ -207,6 +210,9 @@ <includes> <include>org.eclipse.jgit.*</include> </includes> + <excludes> + <exclude>*.internal.*</exclude> + </excludes> <accessModifier>public</accessModifier> <breakBuildOnModifications>false</breakBuildOnModifications> <breakBuildOnBinaryIncompatibleModifications>false</breakBuildOnBinaryIncompatibleModifications> diff --git a/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties b/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties index 2bba736aad..c676221800 100644 --- a/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties +++ b/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties @@ -1,5 +1,23 @@ authenticationCanceled=SSH authentication canceled: no password given authenticationOnClosedSession=Authentication canceled: session is already closing or closed +authGssApiAttempt={0}: trying mechanism OID {1} +authGssApiExhausted={0}: no more mechanisms to try +authGssApiFailure={0}: server refused authentication; mechanism {1} +authGssApiNotTried={0}: not tried +authGssApiPartialSuccess={0}: partial success with mechanism OID {1}, continue with authentication methods {2} +authPasswordAttempt={0}: attempt {1} +authPasswordChangeAttempt={0}: attempt {1} with password change +authPasswordExhausted={0}: no more attempts +authPasswordFailure={0}: server refused (wrong password) +authPasswordNotTried={0}: not tried +authPasswordPartialSuccess={0}: partial success, continue with authentication methods {1} +authPubkeyAttempt={0}: trying {1} key {2} with signature type {3} +authPubkeyAttemptAgent={0}: trying {1} key {2} from SSH agent with signature type {3} +authPubkeyExhausted={0}: no more keys to try +authPubkeyFailure={0}: server refused {1} key {2} +authPubkeyNoKeys={0}: no keys to try +authPubkeyPartialSuccess={0}: partial success for {1} key {2}, continue with authentication methods {3} +cannotReadPublicKey=Cannot read public key from file {0} closeListenerFailed=Ssh session close listener failed configInvalidPath=Invalid path in ssh config key {0}: {1} configInvalidPattern=Invalid pattern in ssh config key {0}: {1} @@ -77,6 +95,8 @@ proxySocksPasswordTooLong=Password for proxy {0} must be at most 255 bytes long, proxySocksUnexpectedMessage=Unexpected message received from SOCKS5 proxy {0}; client state {1}: {2} proxySocksUnexpectedVersion=Expected SOCKS version 5, got {0} proxySocksUsernameTooLong=User name for proxy {0} must be at most 255 bytes long, is {1} bytes: {2} +pubkeyAuthAddKeyToAgentError=Could not add {0} key with fingerprint {1} to the SSH agent +pubkeyAuthAddKeyToAgentQuestion=Add the {0} key with fingerprint {1} to the SSH agent? pubkeyAuthWrongCommand=Public key authentication received unknown SSH command {0} from {1} ({2}) pubkeyAuthWrongKey=Public key authentication received wrong key; sent {0}, got back {1} from {2} ({3}) pubkeyAuthWrongSignatureAlgorithm=Public key authentication requested signature type {0} but got back {1} from {2} ({3}) @@ -85,9 +105,13 @@ serverIdTooLong=Server identification is longer than 255 characters (including l serverIdWithNul=Server identification contains a NUL character: {0} sessionCloseFailed=Closing the session failed sessionWithoutUsername=SSH session created without user name; cannot authenticate +sshAgentEdDSAFormatError=Cannot add ed25519 key to the SSH agent because it is encoded as {0} instead of PKCS#8 +sshAgentPayloadLengthError=Expected {0,choice,0#no bytes|1#one byte|1<{0} bytes} but got {1} sshAgentReplyLengthError=Invalid SSH agent reply message length {0} after command {1} sshAgentReplyUnexpected=Unexpected reply from ssh-agent: {0} sshAgentShortReadBuffer=Short read from SSH agent +sshAgentUnknownKey=SSH agent delivered a key that cannot be handled +sshAgentWrongKeyLength=SSH agent delivered illogical key length {0} at offset {1} in buffer of length {2} sshAgentWrongNumberOfKeys=Invalid number of SSH agent keys: {0} sshClosingDown=Apache MINA sshd session factory is closing down; cannot create new ssh sessions on this factory sshCommandTimeout={0} timed out after {1} seconds while opening the channel diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/AuthenticationLogger.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/AuthenticationLogger.java new file mode 100644 index 0000000000..add79b35c9 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/AuthenticationLogger.java @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.internal.transport.sshd; + +import static org.eclipse.jgit.internal.transport.sshd.CachingKeyPairProvider.getKeyId; + +import java.security.KeyPair; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.apache.sshd.client.auth.password.PasswordAuthenticationReporter; +import org.apache.sshd.client.auth.password.UserAuthPassword; +import org.apache.sshd.client.auth.pubkey.PublicKeyAuthenticationReporter; +import org.apache.sshd.client.auth.pubkey.UserAuthPublicKey; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.config.keys.KeyUtils; + +/** + * Provides a log of authentication attempts for a {@link ClientSession}. + */ +public class AuthenticationLogger { + + private final List<String> messages = new ArrayList<>(); + + // We're interested in this log only in the failure case, so we don't need + // to log authentication success. + + private final PublicKeyAuthenticationReporter pubkeyLogger = new PublicKeyAuthenticationReporter() { + + private boolean hasAttempts; + + @Override + public void signalAuthenticationAttempt(ClientSession session, + String service, KeyPair identity, String signature) + throws Exception { + hasAttempts = true; + String message; + if (identity.getPrivate() == null) { + // SSH agent key + message = MessageFormat.format( + SshdText.get().authPubkeyAttemptAgent, + UserAuthPublicKey.NAME, KeyUtils.getKeyType(identity), + getKeyId(session, identity), signature); + } else { + message = MessageFormat.format( + SshdText.get().authPubkeyAttempt, + UserAuthPublicKey.NAME, KeyUtils.getKeyType(identity), + getKeyId(session, identity), signature); + } + messages.add(message); + } + + @Override + public void signalAuthenticationExhausted(ClientSession session, + String service) throws Exception { + String message; + if (hasAttempts) { + message = MessageFormat.format( + SshdText.get().authPubkeyExhausted, + UserAuthPublicKey.NAME); + } else { + message = MessageFormat.format(SshdText.get().authPubkeyNoKeys, + UserAuthPublicKey.NAME); + } + messages.add(message); + hasAttempts = false; + } + + @Override + public void signalAuthenticationFailure(ClientSession session, + String service, KeyPair identity, boolean partial, + List<String> serverMethods) throws Exception { + String message; + if (partial) { + message = MessageFormat.format( + SshdText.get().authPubkeyPartialSuccess, + UserAuthPublicKey.NAME, KeyUtils.getKeyType(identity), + getKeyId(session, identity), serverMethods); + } else { + message = MessageFormat.format( + SshdText.get().authPubkeyFailure, + UserAuthPublicKey.NAME, KeyUtils.getKeyType(identity), + getKeyId(session, identity)); + } + messages.add(message); + } + }; + + private final PasswordAuthenticationReporter passwordLogger = new PasswordAuthenticationReporter() { + + private int attempts; + + @Override + public void signalAuthenticationAttempt(ClientSession session, + String service, String oldPassword, boolean modified, + String newPassword) throws Exception { + attempts++; + String message; + if (modified) { + message = MessageFormat.format( + SshdText.get().authPasswordChangeAttempt, + UserAuthPassword.NAME, Integer.valueOf(attempts)); + } else { + message = MessageFormat.format( + SshdText.get().authPasswordAttempt, + UserAuthPassword.NAME, Integer.valueOf(attempts)); + } + messages.add(message); + } + + @Override + public void signalAuthenticationExhausted(ClientSession session, + String service) throws Exception { + String message; + if (attempts > 0) { + message = MessageFormat.format( + SshdText.get().authPasswordExhausted, + UserAuthPassword.NAME); + } else { + message = MessageFormat.format( + SshdText.get().authPasswordNotTried, + UserAuthPassword.NAME); + } + messages.add(message); + attempts = 0; + } + + @Override + public void signalAuthenticationFailure(ClientSession session, + String service, String password, boolean partial, + List<String> serverMethods) throws Exception { + String message; + if (partial) { + message = MessageFormat.format( + SshdText.get().authPasswordPartialSuccess, + UserAuthPassword.NAME, serverMethods); + } else { + message = MessageFormat.format( + SshdText.get().authPasswordFailure, + UserAuthPassword.NAME); + } + messages.add(message); + } + }; + + private final GssApiWithMicAuthenticationReporter gssLogger = new GssApiWithMicAuthenticationReporter() { + + private boolean hasAttempts; + + @Override + public void signalAuthenticationAttempt(ClientSession session, + String service, String mechanism) { + hasAttempts = true; + String message = MessageFormat.format( + SshdText.get().authGssApiAttempt, + GssApiWithMicAuthFactory.NAME, mechanism); + messages.add(message); + } + + @Override + public void signalAuthenticationExhausted(ClientSession session, + String service) { + String message; + if (hasAttempts) { + message = MessageFormat.format( + SshdText.get().authGssApiExhausted, + GssApiWithMicAuthFactory.NAME); + } else { + message = MessageFormat.format( + SshdText.get().authGssApiNotTried, + GssApiWithMicAuthFactory.NAME); + } + messages.add(message); + hasAttempts = false; + } + + @Override + public void signalAuthenticationFailure(ClientSession session, + String service, String mechanism, boolean partial, + List<String> serverMethods) { + String message; + if (partial) { + message = MessageFormat.format( + SshdText.get().authGssApiPartialSuccess, + GssApiWithMicAuthFactory.NAME, mechanism, + serverMethods); + } else { + message = MessageFormat.format( + SshdText.get().authGssApiFailure, + GssApiWithMicAuthFactory.NAME, mechanism); + } + messages.add(message); + } + }; + + /** + * Creates a new {@link AuthenticationLogger} and configures the + * {@link ClientSession} to report authentication attempts through this + * instance. + * + * @param session + * to configure + */ + public AuthenticationLogger(ClientSession session) { + session.setPublicKeyAuthenticationReporter(pubkeyLogger); + session.setPasswordAuthenticationReporter(passwordLogger); + session.setAttribute( + GssApiWithMicAuthenticationReporter.GSS_AUTHENTICATION_REPORTER, + gssLogger); + // TODO: keyboard-interactive? sshd 2.8.0 has no callback + // interface for it. + } + + /** + * Retrieves the log messages for the authentication attempts. + * + * @return the messages as an unmodifiable list + */ + public List<String> getLog() { + return Collections.unmodifiableList(messages); + } + + /** + * Drops all previously recorded log messages. + */ + public void clear() { + messages.clear(); + } +} diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/CachingKeyPairProvider.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/CachingKeyPairProvider.java index 79b3637caa..cbd6a64140 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/CachingKeyPairProvider.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/CachingKeyPairProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> and others + * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -11,6 +11,7 @@ package org.eclipse.jgit.internal.transport.sshd; import static java.text.MessageFormat.format; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; @@ -19,18 +20,24 @@ import java.security.GeneralSecurityException; import java.security.InvalidKeyException; import java.security.KeyPair; import java.security.PrivateKey; +import java.security.PublicKey; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.NoSuchElementException; import java.util.concurrent.CancellationException; import javax.security.auth.DestroyFailedException; +import org.apache.sshd.common.AttributeRepository.AttributeKey; +import org.apache.sshd.client.session.ClientSession; import org.apache.sshd.common.NamedResource; import org.apache.sshd.common.config.keys.FilePasswordProvider; +import org.apache.sshd.common.config.keys.KeyUtils; import org.apache.sshd.common.keyprovider.FileKeyPairProvider; import org.apache.sshd.common.session.SessionContext; import org.apache.sshd.common.util.io.resource.IoResource; @@ -43,6 +50,14 @@ import org.eclipse.jgit.transport.sshd.KeyCache; public class CachingKeyPairProvider extends FileKeyPairProvider implements Iterable<KeyPair> { + /** + * An attribute set on the {@link SessionContext} recording loaded keys by + * fingerprint. This enables us to provide nicer output by showing key + * paths, if possible. Users can identify key identities used easier by + * filename than by fingerprint. + */ + public static final AttributeKey<Map<String, Path>> KEY_PATHS_BY_FINGERPRINT = new AttributeKey<>(); + private final KeyCache cache; /** @@ -78,6 +93,33 @@ public class CachingKeyPairProvider extends FileKeyPairProvider return () -> iterator(session); } + static String getKeyId(ClientSession session, KeyPair identity) { + String fingerprint = KeyUtils.getFingerPrint(identity.getPublic()); + Map<String, Path> registered = session + .getAttribute(KEY_PATHS_BY_FINGERPRINT); + if (registered != null) { + Path path = registered.get(fingerprint); + if (path != null) { + Path home = session + .resolveAttribute(JGitSshClient.HOME_DIRECTORY); + if (home != null && path.startsWith(home)) { + try { + path = home.relativize(path); + String pathString = path.toString(); + if (!pathString.isEmpty()) { + return "~" + File.separator + pathString; //$NON-NLS-1$ + } + } catch (IllegalArgumentException e) { + // Cannot be relativized. Ignore, and work with the + // original path + } + } + return path.toString(); + } + } + return fingerprint; + } + private KeyPair loadKey(SessionContext session, Path path) throws IOException, GeneralSecurityException { if (!Files.exists(path)) { @@ -123,13 +165,23 @@ public class CachingKeyPairProvider extends FileKeyPairProvider SshdText.get().identityFileUnsupportedFormat, path)); } KeyPair result = keys.next(); + PublicKey pk = result.getPublic(); + if (pk != null) { + Map<String, Path> registered = session + .getAttribute(KEY_PATHS_BY_FINGERPRINT); + if (registered == null) { + registered = new HashMap<>(); + session.setAttribute(KEY_PATHS_BY_FINGERPRINT, registered); + } + registered.put(KeyUtils.getFingerPrint(pk), path); + } if (keys.hasNext()) { log.warn(format(SshdText.get().identityFileMultipleKeys, path)); keys.forEachRemaining(k -> { - PrivateKey pk = k.getPrivate(); - if (pk != null) { + PrivateKey priv = k.getPrivate(); + if (priv != null) { try { - pk.destroy(); + priv.destroy(); } catch (DestroyFailedException e) { // Ignore } diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthentication.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthentication.java index c3cac0c1df..df01db316b 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthentication.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthentication.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> and others + * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -18,6 +18,7 @@ import java.net.SocketAddress; import java.net.UnknownHostException; import java.util.Collection; import java.util.Iterator; +import java.util.List; import org.apache.sshd.client.auth.AbstractUserAuth; import org.apache.sshd.client.session.ClientSession; @@ -71,7 +72,10 @@ public class GssApiWithMicAuthentication extends AbstractUserAuth { if (context != null) { close(false); } + GssApiWithMicAuthenticationReporter reporter = session.getAttribute( + GssApiWithMicAuthenticationReporter.GSS_AUTHENTICATION_REPORTER); if (!nextMechanism.hasNext()) { + reporter.signalAuthenticationExhausted(session, service); return false; } state = ProtocolState.STARTED; @@ -79,6 +83,7 @@ public class GssApiWithMicAuthentication extends AbstractUserAuth { // RFC 4462 states that SPNEGO must not be used with ssh while (GssApiMechanisms.SPNEGO.equals(currentMechanism)) { if (!nextMechanism.hasNext()) { + reporter.signalAuthenticationExhausted(session, service); return false; } currentMechanism = nextMechanism.next(); @@ -102,6 +107,10 @@ public class GssApiWithMicAuthentication extends AbstractUserAuth { state = ProtocolState.FAILED; return false; } + if (reporter != null) { + reporter.signalAuthenticationAttempt(session, service, + currentMechanism.toString()); + } Buffer buffer = session .createBuffer(SshConstants.SSH_MSG_USERAUTH_REQUEST); buffer.putString(session.getUsername()); @@ -246,4 +255,26 @@ public class GssApiWithMicAuthentication extends AbstractUserAuth { return false; } + @Override + public void signalAuthMethodSuccess(ClientSession session, String service, + Buffer buffer) throws Exception { + GssApiWithMicAuthenticationReporter reporter = session.getAttribute( + GssApiWithMicAuthenticationReporter.GSS_AUTHENTICATION_REPORTER); + if (reporter != null) { + reporter.signalAuthenticationSuccess(session, service, + currentMechanism.toString()); + } + } + + @Override + public void signalAuthMethodFailure(ClientSession session, String service, + boolean partial, List<String> serverMethods, Buffer buffer) + throws Exception { + GssApiWithMicAuthenticationReporter reporter = session.getAttribute( + GssApiWithMicAuthenticationReporter.GSS_AUTHENTICATION_REPORTER); + if (reporter != null) { + reporter.signalAuthenticationFailure(session, service, + currentMechanism.toString(), partial, serverMethods); + } + } } diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthenticationReporter.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthenticationReporter.java new file mode 100644 index 0000000000..201a131650 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthenticationReporter.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.internal.transport.sshd; + +import java.util.List; + +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.AttributeRepository.AttributeKey; + +/** + * Callback interface for recording authentication state in + * {@link GssApiWithMicAuthentication}. + */ +public interface GssApiWithMicAuthenticationReporter { + + /** + * An {@link AttributeKey} for a {@link ClientSession} holding the + * {@link GssApiWithMicAuthenticationReporter}. + */ + static final AttributeKey<GssApiWithMicAuthenticationReporter> GSS_AUTHENTICATION_REPORTER = new AttributeKey<>(); + + /** + * Called when a new authentication attempt is made. + * + * @param session + * the {@link ClientSession} + * @param service + * the name of the requesting SSH service name + * @param mechanism + * the OID of the mechanism used + */ + default void signalAuthenticationAttempt(ClientSession session, + String service, String mechanism) { + // nothing + } + + /** + * Called when there are no more mechanisms to try. + * + * @param session + * the {@link ClientSession} + * @param service + * the name of the requesting SSH service name + */ + default void signalAuthenticationExhausted(ClientSession session, + String service) { + // nothing + } + + /** + * Called when authentication was succeessful. + * + * @param session + * the {@link ClientSession} + * @param service + * the name of the requesting SSH service name + * @param mechanism + * the OID of the mechanism used + */ + default void signalAuthenticationSuccess(ClientSession session, + String service, String mechanism) { + // nothing + } + + /** + * Called when the authentication was not successful. + * + * @param session + * the {@link ClientSession} + * @param service + * the name of the requesting SSH service name + * @param mechanism + * the OID of the mechanism used + * @param partial + * {@code true} if authentication was partially successful, + * meaning one continues with additional authentication methods + * given by {@code serverMethods} + * @param serverMethods + * the {@link List} of authentication methods that can continue + */ + default void signalAuthenticationFailure(ClientSession session, + String service, String mechanism, boolean partial, + List<String> serverMethods) { + // nothing + } +} diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java index e2dbb4c466..5100bc9e54 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java @@ -42,7 +42,6 @@ import org.apache.sshd.common.io.IoSession; import org.apache.sshd.common.io.IoWriteFuture; import org.apache.sshd.common.kex.BuiltinDHFactories; import org.apache.sshd.common.kex.DHFactory; -import org.apache.sshd.common.kex.KexProposalOption; import org.apache.sshd.common.kex.KeyExchangeFactory; import org.apache.sshd.common.kex.extension.KexExtensionHandler; import org.apache.sshd.common.kex.extension.KexExtensionHandler.AvailabilityPhase; @@ -199,24 +198,6 @@ public class JGitClientSession extends ClientSessionImpl { } } - @Override - protected Map<KexProposalOption, String> setNegotiationResult( - Map<KexProposalOption, String> guess) { - Map<KexProposalOption, String> result = super.setNegotiationResult( - guess); - // This should be doable with a SessionListener, too, but I don't see - // how to add a listener in time to catch the negotiation end for sure - // given that the super-constructor already starts KEX. - // - // TODO: This override can be removed once we use sshd 2.8.0. - if (log.isDebugEnabled()) { - result.forEach((option, value) -> log.debug( - "setNegotiationResult({}) Kex: {} = {}", this, //$NON-NLS-1$ - option.getDescription(), value)); - } - return result; - } - Set<String> getAllAvailableSignatureAlgorithms() { Set<String> allAvailable = new HashSet<>(); BuiltinSignatures.VALUES.forEach(s -> allAvailable.add(s.getName())); diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPasswordAuthentication.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPasswordAuthentication.java index ff8caaacc0..33c3c608f6 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPasswordAuthentication.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPasswordAuthentication.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> and others + * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -11,13 +11,11 @@ package org.eclipse.jgit.internal.transport.sshd; import static org.apache.sshd.core.CoreModuleProperties.PASSWORD_PROMPTS; -import org.apache.sshd.client.auth.keyboard.UserInteraction; import org.apache.sshd.client.auth.password.UserAuthPassword; import org.apache.sshd.client.session.ClientSession; /** - * A password authentication handler that uses the {@link JGitUserInteraction} - * to ask the user for the password. It also respects the + * A password authentication handler that respects the * {@code NumberOfPasswordPrompts} ssh config. */ public class JGitPasswordAuthentication extends UserAuthPassword { @@ -35,30 +33,11 @@ public class JGitPasswordAuthentication extends UserAuthPassword { } @Override - protected boolean sendAuthDataRequest(ClientSession session, String service) - throws Exception { + protected String resolveAttemptedPassword(ClientSession session, + String service) throws Exception { if (++attempts > maxAttempts) { - return false; + return null; } - UserInteraction interaction = session.getUserInteraction(); - if (!interaction.isInteractionAllowed(session)) { - return false; - } - String password = getPassword(session, interaction); - if (password == null) { - throw new AuthenticationCanceledException(); - } - // sendPassword takes a buffer as first argument, but actually doesn't - // use it and creates its own buffer... - sendPassword(null, session, password, password); - return true; - } - - private String getPassword(ClientSession session, - UserInteraction interaction) { - String[] results = interaction.interactive(session, null, null, "", //$NON-NLS-1$ - new String[] { SshdText.get().passwordPrompt }, - new boolean[] { false }); - return (results == null || results.length == 0) ? null : results[0]; + return super.resolveAttemptedPassword(session, service); } } diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPublicKeyAuthentication.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPublicKeyAuthentication.java index c082a9a963..e1036c6283 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPublicKeyAuthentication.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPublicKeyAuthentication.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018, 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others + * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -12,25 +12,68 @@ package org.eclipse.jgit.internal.transport.sshd; import static java.text.MessageFormat.format; import static org.eclipse.jgit.transport.SshConstants.PUBKEY_ACCEPTED_ALGORITHMS; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.PublicKey; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.stream.Collectors; +import org.apache.sshd.agent.SshAgent; +import org.apache.sshd.agent.SshAgentFactory; +import org.apache.sshd.agent.SshAgentKeyConstraint; import org.apache.sshd.client.auth.pubkey.KeyAgentIdentity; import org.apache.sshd.client.auth.pubkey.PublicKeyIdentity; import org.apache.sshd.client.auth.pubkey.UserAuthPublicKey; +import org.apache.sshd.client.auth.pubkey.UserAuthPublicKeyIterator; import org.apache.sshd.client.config.hosts.HostConfigEntry; import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.FactoryManager; import org.apache.sshd.common.NamedFactory; +import org.apache.sshd.common.config.keys.AuthorizedKeyEntry; +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.config.keys.PublicKeyEntryResolver; +import org.apache.sshd.common.config.keys.u2f.SecurityKeyPublicKey; import org.apache.sshd.common.signature.Signature; +import org.apache.sshd.common.signature.SignatureFactoriesManager; +import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile; +import org.eclipse.jgit.transport.CredentialItem; +import org.eclipse.jgit.transport.CredentialsProvider; +import org.eclipse.jgit.transport.SshConstants; +import org.eclipse.jgit.transport.URIish; import org.eclipse.jgit.util.StringUtils; /** * Custom {@link UserAuthPublicKey} implementation for handling SSH config - * PubkeyAcceptedAlgorithms. + * PubkeyAcceptedAlgorithms and interaction with the SSH agent. */ public class JGitPublicKeyAuthentication extends UserAuthPublicKey { + private SshAgent agent; + + private HostConfigEntry hostConfig; + + private boolean addKeysToAgent; + + private boolean askBeforeAdding; + + private String skProvider; + + private SshAgentKeyConstraint[] constraints; + JGitPublicKeyAuthentication(List<NamedFactory<Signature>> factories) { super(factories); } @@ -43,7 +86,7 @@ public class JGitPublicKeyAuthentication extends UserAuthPublicKey { + rawSession.getClass().getCanonicalName()); } JGitClientSession session = (JGitClientSession) rawSession; - HostConfigEntry hostConfig = session.getHostConfigEntry(); + hostConfig = session.getHostConfigEntry(); // Set signature algorithms for public key authentication String pubkeyAlgos = hostConfig.getProperty(PUBKEY_ACCEPTED_ALGORITHMS); if (!StringUtils.isEmptyOrNull(pubkeyAlgos)) { @@ -56,54 +99,304 @@ public class JGitPublicKeyAuthentication extends UserAuthPublicKey { log.debug(PUBKEY_ACCEPTED_ALGORITHMS + ' ' + signatures); } setSignatureFactoriesNames(signatures); - } else { - log.warn(format(SshdText.get().configNoKnownAlgorithms, - PUBKEY_ACCEPTED_ALGORITHMS, pubkeyAlgos)); + super.init(session, service); + return; } + log.warn(format(SshdText.get().configNoKnownAlgorithms, + PUBKEY_ACCEPTED_ALGORITHMS, pubkeyAlgos)); + } + // TODO: remove this once we're on an sshd version that has SSHD-1272 + // fixed + List<NamedFactory<Signature>> localFactories = getSignatureFactories(); + if (localFactories == null || localFactories.isEmpty()) { + setSignatureFactoriesNames(session.getSignatureFactoriesNames()); } - // If we don't set signature factories here, the default ones from the - // session will be used. super.init(session, service); - // In sshd 2.7.0, we end up now with a key iterator that uses keys - // provided by an ssh-agent even if IdentitiesOnly is true. So if - // needed, filter out any KeyAgentIdentity. - if (hostConfig.isIdentitiesOnly()) { - Iterator<PublicKeyIdentity> original = keys; - // The original iterator will already have gotten the identities - // from the agent. Unfortunately there's nothing we can do about - // that; it'll have to be fixed upstream. (As will, ultimately, - // respecting isIdentitiesOnly().) At least we can simply not - // use the keys the agent provided. - // - // See https://issues.apache.org/jira/browse/SSHD-1218 - keys = new Iterator<>() { - - private PublicKeyIdentity value; + } + + @Override + protected Iterator<PublicKeyIdentity> createPublicKeyIterator( + ClientSession session, SignatureFactoriesManager manager) + throws Exception { + agent = getAgent(session); + if (agent != null) { + parseAddKeys(hostConfig); + if (addKeysToAgent) { + skProvider = hostConfig.getProperty(SshConstants.SECURITY_KEY_PROVIDER); + } + } + return new KeyIterator(session, manager); + } + + @Override + protected PublicKeyIdentity resolveAttemptedPublicKeyIdentity( + ClientSession session, String service) throws Exception { + PublicKeyIdentity result = getNextKey(session, service); + // This fixes SSHD-1231. Can be removed once we're using Apache MINA + // sshd > 2.8.0. + // + // See https://issues.apache.org/jira/browse/SSHD-1231 + currentAlgorithms.clear(); + return result; + } + + private PublicKeyIdentity getNextKey(ClientSession session, String service) + throws Exception { + PublicKeyIdentity id = super.resolveAttemptedPublicKeyIdentity(session, + service); + if (addKeysToAgent && id != null && !(id instanceof KeyAgentIdentity)) { + KeyPair key = id.getKeyIdentity(); + if (key != null && key.getPublic() != null + && key.getPrivate() != null) { + // We've just successfully loaded a key that wasn't in the + // agent. Add it to the agent. + // + // Keys are added after loading, as in OpenSSH. The alternative + // might be to add a key only after (partially) successful + // authentication? + PublicKey pk = key.getPublic(); + String fingerprint = KeyUtils.getFingerPrint(pk); + String keyType = KeyUtils.getKeyType(key); + try { + // Check that the key is not in the agent already. + if (agentHasKey(pk)) { + return id; + } + if (askBeforeAdding + && (session instanceof JGitClientSession)) { + CredentialsProvider provider = ((JGitClientSession) session) + .getCredentialsProvider(); + CredentialItem.YesNoType question = new CredentialItem.YesNoType( + format(SshdText + .get().pubkeyAuthAddKeyToAgentQuestion, + keyType, fingerprint)); + boolean result = provider != null + && provider.supports(question) + && provider.get(getUri(), question); + if (!result || !question.getValue()) { + // Don't add the key. + return id; + } + } + SshAgentKeyConstraint[] rules = constraints; + if (pk instanceof SecurityKeyPublicKey && !StringUtils.isEmptyOrNull(skProvider)) { + rules = Arrays.copyOf(rules, rules.length + 1); + rules[rules.length - 1] = + new SshAgentKeyConstraint.FidoProviderExtension(skProvider); + } + // Unfortunately a comment associated with the key is lost + // by Apache MINA sshd, and there is also no way to get the + // original file name for keys loaded from a file. So add it + // without comment. + agent.addIdentity(key, null, rules); + } catch (IOException e) { + // Do not re-throw: we don't want authentication to fail if + // we cannot add the key to the agent. + log.error( + format(SshdText.get().pubkeyAuthAddKeyToAgentError, + keyType, fingerprint), + e); + // Note that as of Win32-OpenSSH 8.6 and Pageant 0.76, + // neither can handle key constraints. Pageant fails + // gracefully, not adding the key and returning + // SSH_AGENT_FAILURE. Win32-OpenSSH closes the connection + // without even returning a failure message, which violates + // the SSH agent protocol and makes all subsequent requests + // to the agent fail. + } + } + } + return id; + } + + private boolean agentHasKey(PublicKey pk) throws IOException { + Iterable<? extends Map.Entry<PublicKey, String>> ids = agent + .getIdentities(); + if (ids == null) { + return false; + } + Iterator<? extends Map.Entry<PublicKey, String>> iter = ids.iterator(); + while (iter.hasNext()) { + if (KeyUtils.compareKeys(iter.next().getKey(), pk)) { + return true; + } + } + return false; + } + + private URIish getUri() { + String uri = SshConstants.SSH_SCHEME + "://"; //$NON-NLS-1$ + String userName = hostConfig.getUsername(); + if (!StringUtils.isEmptyOrNull(userName)) { + uri += userName + '@'; + } + uri += hostConfig.getHost(); + int port = hostConfig.getPort(); + if (port > 0 && port != SshConstants.SSH_DEFAULT_PORT) { + uri += ":" + port; //$NON-NLS-1$ + } + try { + return new URIish(uri); + } catch (URISyntaxException e) { + log.error(e.getLocalizedMessage(), e); + } + return new URIish(); + } + + private SshAgent getAgent(ClientSession session) throws Exception { + FactoryManager manager = Objects.requireNonNull( + session.getFactoryManager(), "No session factory manager"); //$NON-NLS-1$ + SshAgentFactory factory = manager.getAgentFactory(); + if (factory == null) { + return null; + } + return factory.createClient(session, manager); + } + + private void parseAddKeys(HostConfigEntry config) { + String value = config.getProperty(SshConstants.ADD_KEYS_TO_AGENT); + if (StringUtils.isEmptyOrNull(value)) { + addKeysToAgent = false; + return; + } + String[] values = value.split(","); //$NON-NLS-1$ + List<SshAgentKeyConstraint> rules = new ArrayList<>(2); + switch (values[0]) { + case "yes": //$NON-NLS-1$ + addKeysToAgent = true; + break; + case "no": //$NON-NLS-1$ + addKeysToAgent = false; + break; + case "ask": //$NON-NLS-1$ + addKeysToAgent = true; + askBeforeAdding = true; + break; + case "confirm": //$NON-NLS-1$ + addKeysToAgent = true; + rules.add(SshAgentKeyConstraint.CONFIRM); + if (values.length > 1) { + int seconds = OpenSshConfigFile.timeSpec(values[1]); + if (seconds > 0) { + rules.add(new SshAgentKeyConstraint.LifeTime(seconds)); + } + } + break; + default: + int seconds = OpenSshConfigFile.timeSpec(values[0]); + if (seconds > 0) { + addKeysToAgent = true; + rules.add(new SshAgentKeyConstraint.LifeTime(seconds)); + } + break; + } + constraints = rules.toArray(new SshAgentKeyConstraint[0]); + } + + @Override + protected void releaseKeys() throws IOException { + addKeysToAgent = false; + askBeforeAdding = false; + skProvider = null; + constraints = null; + try { + if (agent != null) { + try { + agent.close(); + } finally { + agent = null; + } + } + } finally { + super.releaseKeys(); + } + } + + private class KeyIterator extends UserAuthPublicKeyIterator { + + private Iterable<? extends Map.Entry<PublicKey, String>> agentKeys; + + // If non-null, all the public keys from explicitly given key files. Any + // agent key not matching one of these public keys will be ignored in + // getIdentities(). + private Collection<PublicKey> identityFiles; + + public KeyIterator(ClientSession session, + SignatureFactoriesManager manager) + throws Exception { + super(session, manager); + } + + private List<PublicKey> getExplicitKeys( + Collection<String> explicitFiles) { + if (explicitFiles == null) { + return null; + } + return explicitFiles.stream().map(s -> { + try { + Path p = Paths.get(s + ".pub"); //$NON-NLS-1$ + if (Files.isRegularFile(p, LinkOption.NOFOLLOW_LINKS)) { + return AuthorizedKeyEntry.readAuthorizedKeys(p).get(0) + .resolvePublicKey(null, + PublicKeyEntryResolver.IGNORING); + } + } catch (InvalidPathException | IOException + | GeneralSecurityException e) { + log.warn(format(SshdText.get().cannotReadPublicKey, s), e); + } + return null; + }).filter(Objects::nonNull).collect(Collectors.toList()); + } + + @Override + protected Iterable<KeyAgentIdentity> initializeAgentIdentities( + ClientSession session) throws IOException { + if (agent == null) { + return null; + } + agentKeys = agent.getIdentities(); + if (hostConfig != null && hostConfig.isIdentitiesOnly()) { + identityFiles = getExplicitKeys(hostConfig.getIdentities()); + } + return () -> new Iterator<>() { + + private final Iterator<? extends Map.Entry<PublicKey, String>> iter = agentKeys + .iterator(); + + private Map.Entry<PublicKey, String> next; @Override public boolean hasNext() { - if (value != null) { - return true; - } - PublicKeyIdentity next = null; - while (original.hasNext()) { - next = original.next(); - if (!(next instanceof KeyAgentIdentity)) { - value = next; + while (next == null && iter.hasNext()) { + Map.Entry<PublicKey, String> val = iter.next(); + PublicKey pk = val.getKey(); + // This checks against all explicit keys for any agent + // key, but since identityFiles.size() is typically 1, + // it should be fine. + if (identityFiles == null || identityFiles.stream() + .anyMatch(k -> KeyUtils.compareKeys(k, pk))) { + next = val; return true; } + if (log.isTraceEnabled()) { + log.trace( + "Ignoring SSH agent {} key not in explicit IdentityFile in SSH config: {}", //$NON-NLS-1$ + KeyUtils.getKeyType(pk), + KeyUtils.getFingerPrint(pk)); + } } - return false; + return next != null; } @Override - public PublicKeyIdentity next() { - if (hasNext()) { - PublicKeyIdentity result = value; - value = null; - return result; + public KeyAgentIdentity next() { + if (!hasNext()) { + throw new NoSuchElementException(); } - throw new NoSuchElementException(); + KeyAgentIdentity result = new KeyAgentIdentity(agent, + next.getKey(), next.getValue()); + next = null; + return result; } }; } diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java index 71e8e61585..72f0bdb6ee 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018, 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others + * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -87,6 +87,11 @@ public class JGitSshClient extends SshClient { public static final AttributeKey<String> PREFERRED_AUTHENTICATIONS = new AttributeKey<>(); /** + * An attribute key for the home directory. + */ + public static final AttributeKey<Path> HOME_DIRECTORY = new AttributeKey<>(); + + /** * An attribute key for storing an alternate local address to connect to if * a local forward from a ProxyJump ssh config is present. If set, * {@link #connect(HostConfigEntry, AttributeRepository, SocketAddress)} diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitUserInteraction.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitUserInteraction.java index c51a75bc6f..2a725ea16a 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitUserInteraction.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitUserInteraction.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> and others + * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -120,15 +120,16 @@ public class JGitUserInteraction implements UserInteraction { return null; }).filter(s -> s != null).toArray(String[]::new); } - // TODO What to throw to abort the connection/authentication process? - // In UserAuthKeyboardInteractive.getUserResponses() it's clear that - // returning null is valid and signifies "an error"; we'll try the - // next authentication method. But if the user explicitly canceled, - // then we don't want to try the next methods... - // - // Probably not a serious issue with the typical order of public-key, - // keyboard-interactive, password. - return null; + throw new AuthenticationCanceledException(); + } + + @Override + public String resolveAuthPasswordAttempt(ClientSession session) + throws Exception { + String[] results = interactive(session, null, null, "", //$NON-NLS-1$ + new String[] { SshdText.get().passwordPrompt }, + new boolean[] { false }); + return (results == null || results.length == 0) ? null : results[0]; } @Override diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java index 00ee62d6dd..39332d9fca 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018, 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others + * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -29,7 +29,25 @@ public final class SshdText extends TranslationBundle { // @formatter:off /***/ public String authenticationCanceled; /***/ public String authenticationOnClosedSession; + /***/ public String authGssApiAttempt; + /***/ public String authGssApiExhausted; + /***/ public String authGssApiFailure; + /***/ public String authGssApiNotTried; + /***/ public String authGssApiPartialSuccess; + /***/ public String authPasswordAttempt; + /***/ public String authPasswordChangeAttempt; + /***/ public String authPasswordExhausted; + /***/ public String authPasswordFailure; + /***/ public String authPasswordNotTried; + /***/ public String authPasswordPartialSuccess; + /***/ public String authPubkeyAttempt; + /***/ public String authPubkeyAttemptAgent; + /***/ public String authPubkeyExhausted; + /***/ public String authPubkeyFailure; + /***/ public String authPubkeyNoKeys; + /***/ public String authPubkeyPartialSuccess; /***/ public String closeListenerFailed; + /***/ public String cannotReadPublicKey; /***/ public String configInvalidPath; /***/ public String configInvalidPattern; /***/ public String configInvalidPositive; @@ -98,6 +116,8 @@ public final class SshdText extends TranslationBundle { /***/ public String proxySocksUnexpectedMessage; /***/ public String proxySocksUnexpectedVersion; /***/ public String proxySocksUsernameTooLong; + /***/ public String pubkeyAuthAddKeyToAgentError; + /***/ public String pubkeyAuthAddKeyToAgentQuestion; /***/ public String pubkeyAuthWrongCommand; /***/ public String pubkeyAuthWrongKey; /***/ public String pubkeyAuthWrongSignatureAlgorithm; @@ -106,9 +126,13 @@ public final class SshdText extends TranslationBundle { /***/ public String serverIdWithNul; /***/ public String sessionCloseFailed; /***/ public String sessionWithoutUsername; + /***/ public String sshAgentEdDSAFormatError; + /***/ public String sshAgentPayloadLengthError; /***/ public String sshAgentReplyLengthError; /***/ public String sshAgentReplyUnexpected; /***/ public String sshAgentShortReadBuffer; + /***/ public String sshAgentUnknownKey; + /***/ public String sshAgentWrongKeyLength; /***/ public String sshAgentWrongNumberOfKeys; /***/ public String sshClosingDown; /***/ public String sshCommandTimeout; diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/JGitSshAgentFactory.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/JGitSshAgentFactory.java index 1ed2ab9d78..a0ffd540f2 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/JGitSshAgentFactory.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/JGitSshAgentFactory.java @@ -17,10 +17,14 @@ import java.util.List; import org.apache.sshd.agent.SshAgent; import org.apache.sshd.agent.SshAgentFactory; import org.apache.sshd.agent.SshAgentServer; +import org.apache.sshd.client.config.hosts.HostConfigEntry; import org.apache.sshd.common.FactoryManager; import org.apache.sshd.common.channel.ChannelFactory; import org.apache.sshd.common.session.ConnectionService; +import org.apache.sshd.common.session.Session; import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.internal.transport.sshd.JGitClientSession; +import org.eclipse.jgit.transport.SshConstants; import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory; /** @@ -49,18 +53,24 @@ public class JGitSshAgentFactory implements SshAgentFactory { @Override public List<ChannelFactory> getChannelForwardingFactories( FactoryManager manager) { - // No agent forwarding supported yet. + // No agent forwarding supported. return Collections.emptyList(); } @Override - public SshAgent createClient(FactoryManager manager) throws IOException { - // sshd 2.8.0 will pass us the session here. At that point, we can get - // the HostConfigEntry and extract and handle the IdentityAgent setting. - // For now, pass null to let the ConnectorFactory do its default - // behavior (Pageant on Windows, SSH_AUTH_SOCK on Unixes with the - // jgit-builtin factory). - return new SshAgentClient(factory.create(null, homeDir)); + public SshAgent createClient(Session session, FactoryManager manager) + throws IOException { + String identityAgent = null; + if (session instanceof JGitClientSession) { + HostConfigEntry hostConfig = ((JGitClientSession) session) + .getHostConfigEntry(); + identityAgent = hostConfig.getProperty(SshConstants.IDENTITY_AGENT, + null); + } + if (SshConstants.NONE.equals(identityAgent)) { + return null; + } + return new SshAgentClient(factory.create(identityAgent, homeDir)); } @Override diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/SshAgentClient.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/SshAgentClient.java index 08483e4c20..cbcb4d240e 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/SshAgentClient.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/SshAgentClient.java @@ -11,10 +11,12 @@ package org.eclipse.jgit.internal.transport.sshd.agent; import java.io.IOException; import java.security.KeyPair; +import java.security.PrivateKey; import java.security.PublicKey; import java.text.MessageFormat; import java.util.AbstractMap; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; @@ -22,21 +24,27 @@ import java.util.concurrent.atomic.AtomicBoolean; import org.apache.sshd.agent.SshAgent; import org.apache.sshd.agent.SshAgentConstants; +import org.apache.sshd.agent.SshAgentKeyConstraint; import org.apache.sshd.common.SshException; import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.keyprovider.KeyPairProvider; import org.apache.sshd.common.session.SessionContext; import org.apache.sshd.common.util.buffer.Buffer; import org.apache.sshd.common.util.buffer.BufferException; import org.apache.sshd.common.util.buffer.BufferUtils; import org.apache.sshd.common.util.buffer.ByteArrayBuffer; +import org.apache.sshd.common.util.buffer.keys.BufferPublicKeyParser; +import org.apache.sshd.common.util.io.der.DERParser; import org.eclipse.jgit.internal.transport.sshd.SshdText; import org.eclipse.jgit.transport.sshd.agent.Connector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * A client for an SSH2 agent. This client supports only querying identities and - * signature requests. + * A client for an SSH2 agent. This client supports querying identities, + * signature requests, and adding keys to an agent (with or without + * constraints). Removing keys is not supported, and the older SSH1 protocol is + * not supported. * * @see <a href="https://tools.ietf.org/html/draft-miller-ssh-agent-04">SSH * Agent Protocol, RFC draft</a> @@ -72,11 +80,18 @@ public class SshAgentClient implements SshAgent { } return false; } - boolean connected = connector != null && connector.connect(); - if (!connected) { + boolean connected; + try { + connected = connector != null && connector.connect(); + if (!connected && debugging) { + LOG.debug("No SSH agent"); //$NON-NLS-1$ + } + } catch (IOException e) { + // Agent not running? if (debugging) { - LOG.debug("No SSH agent (SSH_AUTH_SOCK not set)"); //$NON-NLS-1$ + LOG.debug("No SSH agent", e); //$NON-NLS-1$ } + throw e; } return connected; } @@ -127,14 +142,17 @@ public class SshAgentClient implements SshAgent { List<Map.Entry<PublicKey, String>> keys = new ArrayList<>( numberOfKeys); for (int i = 0; i < numberOfKeys; i++) { - PublicKey key = reply.getPublicKey(); + PublicKey key = readKey(reply); String comment = reply.getString(); - if (tracing) { - LOG.trace("Got SSH agent {} key: {} {}", //$NON-NLS-1$ - KeyUtils.getKeyType(key), - KeyUtils.getFingerPrint(key), comment); + if (key != null) { + if (tracing) { + LOG.trace("Got SSH agent {} key: {} {}", //$NON-NLS-1$ + KeyUtils.getKeyType(key), + KeyUtils.getFingerPrint(key), comment); + } + keys.add(new AbstractMap.SimpleImmutableEntry<>(key, + comment)); } - keys.add(new AbstractMap.SimpleImmutableEntry<>(key, comment)); } return keys; } catch (BufferException e) { @@ -216,6 +234,222 @@ public class SshAgentClient implements SshAgent { } } + @Override + public void addIdentity(KeyPair key, String comment, + SshAgentKeyConstraint... constraints) throws IOException { + boolean debugging = LOG.isDebugEnabled(); + if (!open(debugging)) { + return; + } + + // Neither Pageant 0.76 nor Win32-OpenSSH 8.6 support command + // SSH2_AGENTC_ADD_ID_CONSTRAINED. Adding a key with constraints will + // fail. The only work-around for users is not to use "confirm" or "time + // spec" with AddKeysToAgent, and not to use sk-* keys. + // + // With a true OpenSSH SSH agent, key constraints work. + byte cmd = (constraints != null && constraints.length > 0) + ? SshAgentConstants.SSH2_AGENTC_ADD_ID_CONSTRAINED + : SshAgentConstants.SSH2_AGENTC_ADD_IDENTITY; + byte[] message = null; + ByteArrayBuffer msg = new ByteArrayBuffer(); + try { + msg.putInt(0); + msg.putByte(cmd); + String keyType = KeyUtils.getKeyType(key); + if (KeyPairProvider.SSH_ED25519.equals(keyType)) { + // Apache MINA sshd 2.8.0 lacks support for writing ed25519 + // private keys to a buffer. + putEd25519Key(msg, key); + } else { + msg.putKeyPair(key); + } + msg.putString(comment == null ? "" : comment); //$NON-NLS-1$ + if (constraints != null) { + for (SshAgentKeyConstraint constraint : constraints) { + constraint.put(msg); + } + } + if (debugging) { + LOG.debug( + "addIdentity: adding {} key {} to SSH agent; comment {}", //$NON-NLS-1$ + keyType, KeyUtils.getFingerPrint(key.getPublic()), + comment); + } + message = msg.getCompactData(); + } finally { + // The message contains the private key data, so clear intermediary + // data ASAP. + msg.clear(); + } + Buffer reply; + try { + reply = rpc(cmd, message); + } finally { + Arrays.fill(message, (byte) 0); + } + int replyLength = reply.available(); + if (replyLength != 1) { + throw new SshException(MessageFormat.format( + SshdText.get().sshAgentReplyUnexpected, + MessageFormat.format( + SshdText.get().sshAgentPayloadLengthError, + Integer.valueOf(1), Integer.valueOf(replyLength)))); + + } + cmd = reply.getByte(); + if (cmd != SshAgentConstants.SSH_AGENT_SUCCESS) { + throw new SshException( + MessageFormat.format(SshdText.get().sshAgentReplyUnexpected, + SshAgentConstants.getCommandMessageName(cmd))); + } + } + + /** + * Writes an ed25519 {@link KeyPair} to a {@link Buffer}. OpenSSH specifies + * that it expects the 32 public key bytes, followed by 64 bytes formed by + * concatenating the 32 private key bytes with the 32 public key bytes. + * + * @param msg + * {@link Buffer} to write to + * @param key + * {@link KeyPair} to write + * @throws IOException + * if the private key cannot be written + */ + private static void putEd25519Key(Buffer msg, KeyPair key) + throws IOException { + Buffer tmp = new ByteArrayBuffer(36); + tmp.putRawPublicKeyBytes(key.getPublic()); + byte[] publicBytes = tmp.getBytes(); + msg.putString(KeyPairProvider.SSH_ED25519); + msg.putBytes(publicBytes); + // Next is the concatenation of the 32 byte private key value with the + // 32 bytes of the public key. + PrivateKey pk = key.getPrivate(); + String format = pk.getFormat(); + if (!"PKCS#8".equalsIgnoreCase(format)) { //$NON-NLS-1$ + throw new IOException(MessageFormat + .format(SshdText.get().sshAgentEdDSAFormatError, format)); + } + byte[] privateBytes = null; + byte[] encoded = pk.getEncoded(); + try { + privateBytes = asn1Parse(encoded, 32); + byte[] combined = Arrays.copyOf(privateBytes, 64); + Arrays.fill(privateBytes, (byte) 0); + privateBytes = combined; + System.arraycopy(publicBytes, 0, privateBytes, 32, 32); + msg.putBytes(privateBytes); + } finally { + if (privateBytes != null) { + Arrays.fill(privateBytes, (byte) 0); + } + Arrays.fill(encoded, (byte) 0); + } + } + + /** + * Extracts the private key bytes from an encoded ed25519 private key by + * parsing the bytes as ASN.1 according to RFC 5958 (PKCS #8 encoding): + * + * <pre> + * OneAsymmetricKey ::= SEQUENCE { + * version Version, + * privateKeyAlgorithm PrivateKeyAlgorithmIdentifier, + * privateKey PrivateKey, + * ... + * } + * + * Version ::= INTEGER + * PrivateKeyAlgorithmIdentifier ::= AlgorithmIdentifier + * PrivateKey ::= OCTET STRING + * + * AlgorithmIdentifier ::= SEQUENCE { + * algorithm OBJECT IDENTIFIER, + * parameters ANY DEFINED BY algorithm OPTIONAL + * } + * </pre> + * <p> + * and RFC 8410: "... when encoding a OneAsymmetricKey object, the private + * key is wrapped in a CurvePrivateKey object and wrapped by the OCTET + * STRING of the 'privateKey' field." + * </p> + * + * <pre> + * CurvePrivateKey ::= OCTET STRING + * </pre> + * + * @param encoded + * encoded private key to extract the private key bytes from + * @param n + * number of bytes expected + * @return the extracted private key bytes; of length {@code n} + * @throws IOException + * if the private key cannot be extracted + * @see <a href="https://tools.ietf.org/html/rfc5958">RFC 5958</a> + * @see <a href="https://tools.ietf.org/html/rfc8410">RFC 8410</a> + */ + private static byte[] asn1Parse(byte[] encoded, int n) throws IOException { + byte[] privateKey = null; + try (DERParser byteParser = new DERParser(encoded); + DERParser oneAsymmetricKey = byteParser.readObject() + .createParser()) { + oneAsymmetricKey.readObject(); // skip version + oneAsymmetricKey.readObject(); // skip algorithm identifier + privateKey = oneAsymmetricKey.readObject().getValue(); + // The last n bytes of this must be the private key bytes + return Arrays.copyOfRange(privateKey, + privateKey.length - n, privateKey.length); + } finally { + if (privateKey != null) { + Arrays.fill(privateKey, (byte) 0); + } + } + } + + /** + * A safe version of {@link Buffer#getPublicKey()}. Upon return the + * buffers's read position is always after the key blob; any exceptions + * thrown by trying to read the key are logged and <em>not</em> propagated. + * <p> + * This is needed because an SSH agent might contain and deliver keys that + * we cannot handle (for instance ed448 keys). + * </p> + * + * @param buffer + * to read the key from + * @return the {@link PublicKey}, or {@code null} if the key could not be + * read + * @throws BufferException + * if the length of the key blob cannot be read or is corrupted + */ + private static PublicKey readKey(Buffer buffer) throws BufferException { + int endOfBuffer = buffer.wpos(); + int keyLength = buffer.getInt(); + int afterKey = buffer.rpos() + keyLength; + if (keyLength <= 0 || afterKey > endOfBuffer) { + throw new BufferException( + MessageFormat.format(SshdText.get().sshAgentWrongKeyLength, + Integer.toString(keyLength), + Integer.toString(buffer.rpos()), + Integer.toString(endOfBuffer))); + } + // Limit subsequent reads to the public key blob + buffer.wpos(afterKey); + try { + return buffer.getRawPublicKey(BufferPublicKeyParser.DEFAULT); + } catch (Exception e) { + LOG.warn(SshdText.get().sshAgentUnknownKey, e); + return null; + } finally { + // Restore real buffer end + buffer.wpos(endOfBuffer); + // Set the read position to after this key, even if failed + buffer.rpos(afterKey); + } + } + private Buffer rpc(byte command, byte[] message) throws IOException { return new ByteArrayBuffer(connector.rpc(command, message)); } @@ -230,11 +464,6 @@ public class SshAgentClient implements SshAgent { } @Override - public void addIdentity(KeyPair key, String comment) throws IOException { - throw new UnsupportedOperationException(); - } - - @Override public void removeIdentity(PublicKey key) throws IOException { throw new UnsupportedOperationException(); } diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSession.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSession.java index c270b44956..b94ccc6d4f 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSession.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSession.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018, 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others + * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -51,6 +51,9 @@ import org.apache.sshd.sftp.client.SftpClientFactory; import org.apache.sshd.sftp.common.SftpException; import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile; +import org.eclipse.jgit.internal.transport.sshd.AuthenticationCanceledException; +import org.eclipse.jgit.internal.transport.sshd.AuthenticationLogger; import org.eclipse.jgit.internal.transport.sshd.JGitSshClient; import org.eclipse.jgit.internal.transport.sshd.SshdText; import org.eclipse.jgit.transport.FtpChannel; @@ -118,6 +121,7 @@ public class SshdSession implements RemoteSession2 { ClientSession resultSession = null; ClientSession proxySession = null; PortForwardingTracker portForward = null; + AuthenticationLogger authLog = null; try { if (!hops.isEmpty()) { URIish hop = hops.remove(0); @@ -138,7 +142,11 @@ public class SshdSession implements RemoteSession2 { JGitSshClient.LOCAL_FORWARD_ADDRESS, portForward.getBoundAddress()); } - resultSession = connect(hostConfig, context, timeout); + int timeoutInSec = OpenSshConfigFile.timeSpec( + hostConfig.getProperty(SshConstants.CONNECT_TIMEOUT)); + resultSession = connect(hostConfig, context, + timeoutInSec > 0 ? Duration.ofSeconds(timeoutInSec) + : timeout); if (proxySession != null) { final PortForwardingTracker tracker = portForward; final ClientSession pSession = proxySession; @@ -160,6 +168,7 @@ public class SshdSession implements RemoteSession2 { resultSession.addCloseFutureListener(listener); } // Authentication timeout is by default 2 minutes. + authLog = new AuthenticationLogger(resultSession); resultSession.auth().verify(resultSession.getAuthTimeout()); return resultSession; } catch (IOException e) { @@ -168,15 +177,32 @@ public class SshdSession implements RemoteSession2 { close(resultSession, e); if (e instanceof SshException && ((SshException) e) .getDisconnectCode() == SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE) { - // Ensure the user gets to know on which URI the authentication - // was denied. + String message = format(SshdText.get().loginDenied, host, + Integer.toString(port)); throw new TransportException(target, - format(SshdText.get().loginDenied, host, - Integer.toString(port)), - e); + withAuthLog(message, authLog), e); + } else if (e instanceof SshException && e + .getCause() instanceof AuthenticationCanceledException) { + String message = e.getCause().getMessage(); + throw new TransportException(target, + withAuthLog(message, authLog), e.getCause()); } throw e; + } finally { + if (authLog != null) { + authLog.clear(); + } + } + } + + private String withAuthLog(String message, AuthenticationLogger authLog) { + if (authLog != null) { + String log = String.join(System.lineSeparator(), authLog.getLog()); + if (!log.isEmpty()) { + return message + System.lineSeparator() + log; + } } + return message; } private ClientSession connect(HostConfigEntry config, diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java index 58cf8e1ddd..c792c1889c 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018, 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others + * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -13,6 +13,7 @@ import java.io.Closeable; import java.io.File; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.security.KeyPair; import java.time.Duration; @@ -34,7 +35,6 @@ import org.apache.sshd.client.auth.UserAuthFactory; import org.apache.sshd.client.auth.keyboard.UserAuthKeyboardInteractiveFactory; import org.apache.sshd.client.config.hosts.HostConfigEntryResolver; import org.apache.sshd.common.NamedFactory; -import org.apache.sshd.common.SshException; import org.apache.sshd.common.compression.BuiltinCompressions; import org.apache.sshd.common.config.keys.FilePasswordProvider; import org.apache.sshd.common.config.keys.loader.openssh.kdf.BCryptKdfOptions; @@ -44,7 +44,6 @@ import org.apache.sshd.common.signature.Signature; import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile; -import org.eclipse.jgit.internal.transport.sshd.AuthenticationCanceledException; import org.eclipse.jgit.internal.transport.sshd.CachingKeyPairProvider; import org.eclipse.jgit.internal.transport.sshd.GssApiWithMicAuthFactory; import org.eclipse.jgit.internal.transport.sshd.JGitPasswordAuthFactory; @@ -243,6 +242,12 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable { JGitSshClient.PREFERRED_AUTHENTICATIONS, defaultAuths); } + try { + jgitClient.setAttribute(JGitSshClient.HOME_DIRECTORY, + home.getAbsoluteFile().toPath()); + } catch (SecurityException | InvalidPathException e) { + // Ignore + } // Other things? return client; }); @@ -255,13 +260,7 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable { if (e instanceof TransportException) { throw (TransportException) e; } - Throwable cause = e; - if (e instanceof SshException && e - .getCause() instanceof AuthenticationCanceledException) { - // Results in a nicer error message - cause = e.getCause(); - } - throw new TransportException(uri, cause.getMessage(), cause); + throw new TransportException(uri, e.getMessage(), e); } } diff --git a/org.eclipse.jgit.test/BUILD b/org.eclipse.jgit.test/BUILD index c9b5d37265..6f6c88ce8d 100644 --- a/org.eclipse.jgit.test/BUILD +++ b/org.eclipse.jgit.test/BUILD @@ -13,6 +13,7 @@ HELPERS = glob( ) + [PKG + c for c in [ "api/AbstractRemoteCommandTest.java", "diff/AbstractDiffTestCase.java", + "internal/diffmergetool/ExternalToolTestCase.java", "internal/revwalk/ObjectReachabilityTestCase.java", "internal/revwalk/ReachabilityCheckerTestCase.java", "internal/storage/file/GcTestCase.java", @@ -42,6 +43,7 @@ DATA = [ EXCLUDED = [ PKG + "api/SecurityManagerTest.java", PKG + "api/SecurityManagerMissingPermissionsTest.java", + PKG + "lib/CommitTemplateConfigTest.java", ] tests(tests = glob( diff --git a/org.eclipse.jgit.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.test/META-INF/MANIFEST.MF index 2952efbdfc..832a93e5d4 100644 --- a/org.eclipse.jgit.test/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.test/META-INF/MANIFEST.MF @@ -33,6 +33,7 @@ Import-Package: com.googlecode.javaewah;version="[1.1.6,2.0.0)", org.eclipse.jgit.ignore;version="[7.0.0,7.1.0)", org.eclipse.jgit.ignore.internal;version="[7.0.0,7.1.0)", org.eclipse.jgit.internal;version="[7.0.0,7.1.0)", + org.eclipse.jgit.internal.diff;version="[7.0.0,7.1.0)", org.eclipse.jgit.internal.diffmergetool;version="[7.0.0,7.1.0)", org.eclipse.jgit.internal.fsck;version="[7.0.0,7.1.0)", org.eclipse.jgit.internal.revwalk;version="[7.0.0,7.1.0)", diff --git a/org.eclipse.jgit.test/build.properties b/org.eclipse.jgit.test/build.properties index b527a74790..212c8bd4d7 100644 --- a/org.eclipse.jgit.test/build.properties +++ b/org.eclipse.jgit.test/build.properties @@ -7,5 +7,4 @@ bin.includes = META-INF/,\ plugin.properties,\ bin-tst/,\ bin/ -additional.bundles = org.apache.log4j,\ - org.slf4j.binding.log4j12 +additional.bundles = org.slf4j.binding.simple diff --git a/org.eclipse.jgit.test/tests.bzl b/org.eclipse.jgit.test/tests.bzl index 34df07d5e6..e201bdbcb3 100644 --- a/org.eclipse.jgit.test/tests.bzl +++ b/org.eclipse.jgit.test/tests.bzl @@ -36,7 +36,7 @@ def tests(tests): ] if src.endswith("SecurityManagerMissingPermissionsTest.java"): additional_deps = [ - "//lib:log4j", + "//lib:slf4j-simple", ] if src.endswith("JDKHttpConnectionTest.java"): additional_deps = [ @@ -68,6 +68,7 @@ def tests(tests): "//lib:javaewah", "//lib:junit", "//lib:slf4j-api", + "//lib:slf4j-simple", "//org.eclipse.jgit:jgit", "//org.eclipse.jgit.junit:junit", "//org.eclipse.jgit.lfs:jgit-lfs", diff --git a/org.eclipse.jgit.test/tst-rsrc/jgit-s3-config.disabled.properties b/org.eclipse.jgit.test/tst-rsrc/jgit-s3-config.disabled.properties index d540977e94..3f36282b9e 100644 --- a/org.eclipse.jgit.test/tst-rsrc/jgit-s3-config.disabled.properties +++ b/org.eclipse.jgit.test/tst-rsrc/jgit-s3-config.disabled.properties @@ -40,6 +40,15 @@ # * https://docs.aws.amazon.com/AmazonS3/latest/dev/manage-lifecycle-using-console.html # +# AWS API signature version (defaults to 2) +# aws.api.signature.version=4 + +# AWS S3 Region Domain (defaults to s3.amazonaws.com) +# domain: s3-us-east-2.amazonaws.com + +# AWS S3 Region (required if aws.api.signature.version=4, must match domain) +# region: us-east-2 + # Test bucket name test.bucket=jgit.eclipse.org diff --git a/org.eclipse.jgit.test/tst-rsrc/log4j.properties b/org.eclipse.jgit.test/tst-rsrc/log4j.properties deleted file mode 100644 index 856a731ab9..0000000000 --- a/org.eclipse.jgit.test/tst-rsrc/log4j.properties +++ /dev/null @@ -1,14 +0,0 @@ - -# Root logger option -log4j.rootLogger=INFO, stdout - -# Direct log messages to stdout -log4j.appender.stdout=org.apache.log4j.ConsoleAppender -log4j.appender.stdout.Target=System.out -log4j.appender.stdout.layout=org.apache.log4j.PatternLayout -log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n -log4j.appender.fileLogger.bufferedIO = true -log4j.appender.fileLogger.bufferSize = 4096 - -#log4j.logger.org.eclipse.jgit.util.FS = DEBUG -#log4j.logger.org.eclipse.jgit.internal.storage.file.FileSnapshot = DEBUG diff --git a/org.eclipse.jgit.test/tst-rsrc/simplelogger.properties b/org.eclipse.jgit.test/tst-rsrc/simplelogger.properties index 011b2f8bb0..235fea21ac 100644 --- a/org.eclipse.jgit.test/tst-rsrc/simplelogger.properties +++ b/org.eclipse.jgit.test/tst-rsrc/simplelogger.properties @@ -1,9 +1,9 @@ -org.slf4j.simpleLogger.logFile = System.err -org.slf4j.simpleLogger.cacheOutputStream = true -org.slf4j.simpleLogger.defaultLogLevel = info -org.slf4j.simpleLogger.showDateTime = true -org.slf4j.simpleLogger.dateTimeFormat = HH:mm:ss.SSSXXX -org.slf4j.simpleLogger.showThreadName = true +org.slf4j.simpleLogger.defaultLogLevel=info +org.slf4j.simpleLogger.logFile=System.err +org.slf4j.simpleLogger.showDateTime=true +org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss +org.slf4j.simpleLogger.showThreadName=true +org.slf4j.simpleLogger.showLogName=true #org.slf4j.simpleLogger.log.org.eclipse.jgit.util.FS = debug #org.slf4j.simpleLogger.log.org.eclipse.jgit.internal.storage.file.FileSnapshot = debug diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ApplyCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ApplyCommandTest.java index 867310b60c..584d149631 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ApplyCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ApplyCommandTest.java @@ -32,6 +32,7 @@ import org.eclipse.jgit.diff.RawText; import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.ConfigConstants; +import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.IO; import org.junit.Test; @@ -402,7 +403,9 @@ public class ApplyCommandTest extends RepositoryTestCase { public void testAddM1() throws Exception { ApplyResult result = init("M1", false, true); assertEquals(1, result.getUpdatedFiles().size()); - assertTrue(result.getUpdatedFiles().get(0).canExecute()); + if (FS.DETECTED.supportsExecute()) { + assertTrue(FS.DETECTED.canExecute(result.getUpdatedFiles().get(0))); + } checkFile(new File(db.getWorkTree(), "M1"), b.getString(0, b.size(), false)); } @@ -411,7 +414,9 @@ public class ApplyCommandTest extends RepositoryTestCase { public void testModifyM2() throws Exception { ApplyResult result = init("M2", true, true); assertEquals(1, result.getUpdatedFiles().size()); - assertTrue(result.getUpdatedFiles().get(0).canExecute()); + if (FS.DETECTED.supportsExecute()) { + assertTrue(FS.DETECTED.canExecute(result.getUpdatedFiles().get(0))); + } checkFile(new File(db.getWorkTree(), "M2"), b.getString(0, b.size(), false)); } @@ -420,7 +425,10 @@ public class ApplyCommandTest extends RepositoryTestCase { public void testModifyM3() throws Exception { ApplyResult result = init("M3", true, true); assertEquals(1, result.getUpdatedFiles().size()); - assertFalse(result.getUpdatedFiles().get(0).canExecute()); + if (FS.DETECTED.supportsExecute()) { + assertFalse( + FS.DETECTED.canExecute(result.getUpdatedFiles().get(0))); + } checkFile(new File(db.getWorkTree(), "M3"), b.getString(0, b.size(), false)); } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/BranchCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/BranchCommandTest.java index e7ed102f13..87be813c85 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/BranchCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/BranchCommandTest.java @@ -71,6 +71,7 @@ public class BranchCommandTest extends RepositoryTestCase { private Git setUpRepoWithRemote() throws Exception { Repository remoteRepository = createWorkRepository(); + addRepoToClose(remoteRepository); try (Git remoteGit = new Git(remoteRepository)) { // commit something writeTrashFile("Test.txt", "Hello world"); @@ -85,6 +86,7 @@ public class BranchCommandTest extends RepositoryTestCase { rup.forceUpdate(); Repository localRepository = createWorkRepository(); + addRepoToClose(localRepository); Git localGit = new Git(localRepository); StoredConfig config = localRepository.getConfig(); RemoteConfig rc = new RemoteConfig(config, "origin"); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CheckoutCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CheckoutCommandTest.java index e520732513..ab0184bd4a 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CheckoutCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CheckoutCommandTest.java @@ -292,6 +292,7 @@ public class CheckoutCommandTest extends RepositoryTestCase { @Test public void testCheckoutRemoteTrackingWithUpstream() throws Exception { Repository db2 = createRepositoryWithRemote(); + addRepoToClose(db2); Git.wrap(db2).checkout().setCreateBranch(true).setName("test") .setStartPoint("origin/test") @@ -311,6 +312,7 @@ public class CheckoutCommandTest extends RepositoryTestCase { @Test public void testCheckoutRemoteTrackingWithoutLocalBranch() throws Exception { Repository db2 = createRepositoryWithRemote(); + addRepoToClose(db2); // checkout remote tracking branch in second repository // (no local branches exist yet in second repository) @@ -868,7 +870,7 @@ public class CheckoutCommandTest extends RepositoryTestCase { coCommand.setName(crudCommit.getName()).call(); CheckoutResult result = coCommand.getResult(); assertEquals(Status.NONDELETED, result.getStatus()); - assertEquals("[Test.txt, toBeDeleted.txt]", + assertEquals("[toBeDeleted.txt]", result.getRemovedList().toString()); assertEquals("[toBeCreated.txt, toBeModified.txt]", result.getModifiedList().toString()); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java index f4f0ecd689..0d38197d9a 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java @@ -175,7 +175,8 @@ public class CherryPickCommandTest extends RepositoryTestCase { assertEquals(CherryPickStatus.CONFLICTING, result.getStatus()); assertTrue(new File(db.getDirectory(), Constants.MERGE_MSG).exists()); - assertEquals("side\n\nConflicts:\n\ta\n", db.readMergeCommitMsg()); + assertEquals("side\n\n# Conflicts:\n#\ta\n", + db.readMergeCommitMsg()); assertTrue(new File(db.getDirectory(), Constants.CHERRY_PICK_HEAD) .exists()); assertEquals(sideCommit.getId(), db.readCherryPickHead()); @@ -207,7 +208,7 @@ public class CherryPickCommandTest extends RepositoryTestCase { String expected = "<<<<<<< master\na(master)\n=======\na(side)\n>>>>>>> 527460a side\n"; assertEquals(expected, read("a")); assertTrue(new File(db.getDirectory(), Constants.MERGE_MSG).exists()); - assertEquals("side\n\nConflicts:\n\ta\n", db.readMergeCommitMsg()); + assertEquals("side\n\n# Conflicts:\n#\ta\n", db.readMergeCommitMsg()); assertFalse(new File(db.getDirectory(), Constants.CHERRY_PICK_HEAD) .exists()); assertEquals(RepositoryState.SAFE, db.getRepositoryState()); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CloneCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CloneCommandTest.java index de25870bd0..c928d2ad22 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CloneCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CloneCommandTest.java @@ -22,6 +22,7 @@ import java.net.URISyntaxException; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.stream.Stream; import org.eclipse.jgit.api.ListBranchCommand.ListMode; import org.eclipse.jgit.api.errors.GitAPIException; @@ -115,6 +116,49 @@ public class CloneCommandTest extends RepositoryTestCase { } @Test + public void testCloneRepository_refLogForLocalRefs() + throws IOException, JGitInternalException, GitAPIException { + File directory = createTempDirectory("testCloneRepository"); + CloneCommand command = Git.cloneRepository(); + command.setDirectory(directory); + command.setURI(fileUri()); + Git git2 = command.call(); + Repository clonedRepo = git2.getRepository(); + addRepoToClose(clonedRepo); + + List<Ref> clonedRefs = clonedRepo.getRefDatabase().getRefs(); + Stream<Ref> remoteRefs = clonedRefs.stream() + .filter(CloneCommandTest::isRemote); + Stream<Ref> localHeadsRefs = clonedRefs.stream() + .filter(CloneCommandTest::isLocalHead); + + remoteRefs.forEach(ref -> assertFalse( + "Ref " + ref.getName() + + " is remote and should not have a reflog", + hasRefLog(clonedRepo, ref))); + localHeadsRefs.forEach(ref -> assertTrue( + "Ref " + ref.getName() + + " is local head and should have a reflog", + hasRefLog(clonedRepo, ref))); + } + + private static boolean isRemote(Ref ref) { + return ref.getName().startsWith(Constants.R_REMOTES); + } + + private static boolean isLocalHead(Ref ref) { + return !isRemote(ref) && ref.getName().startsWith(Constants.R_HEADS); + } + + private static boolean hasRefLog(Repository repo, Ref ref) { + try { + return repo.getReflogReader(ref.getName()).getLastEntry() != null; + } catch (IOException ioe) { + throw new IllegalStateException(ioe); + } + } + + @Test public void testCloneRepositoryExplicitGitDir() throws IOException, JGitInternalException, GitAPIException { File directory = createTempDirectory("testCloneRepository"); 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 8084505c10..35de73e204 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,6 +46,7 @@ import org.eclipse.jgit.lib.RefUpdate.Result; import org.eclipse.jgit.lib.ReflogEntry; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.StoredConfig; +import org.eclipse.jgit.lib.CommitConfig.CleanupMode; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.storage.file.FileBasedConfig; import org.eclipse.jgit.submodule.SubmoduleWalk; @@ -515,6 +516,62 @@ public class CommitCommandTest extends RepositoryTestCase { } @Test + public void commitMessageVerbatim() throws Exception { + try (Git git = new Git(db)) { + writeTrashFile("file1", "file1"); + git.add().addFilepattern("file1").call(); + RevCommit committed = git.commit().setMessage("#initial commit") + .call(); + + assertEquals("#initial commit", committed.getFullMessage()); + } + } + + @Test + public void commitMessageStrip() throws Exception { + try (Git git = new Git(db)) { + writeTrashFile("file1", "file1"); + git.add().addFilepattern("file1").call(); + RevCommit committed = git.commit().setMessage( + "#Comment\ninitial commit\t\n\n commit body \n \t#another comment") + .setCleanupMode(CleanupMode.STRIP).call(); + + assertEquals("initial commit\n\n commit body", + committed.getFullMessage()); + } + } + + @Test + public void commitMessageDefault() throws Exception { + try (Git git = new Git(db)) { + writeTrashFile("file1", "file1"); + git.add().addFilepattern("file1").call(); + RevCommit committed = git.commit().setMessage( + "#Comment\ninitial commit\t\n\n commit body \n\n\n \t#another comment ") + .setCleanupMode(CleanupMode.DEFAULT).call(); + + assertEquals("initial commit\n\n commit body", + committed.getFullMessage()); + } + } + + @Test + public void commitMessageDefaultWhitespace() throws Exception { + try (Git git = new Git(db)) { + writeTrashFile("file1", "file1"); + git.add().addFilepattern("file1").call(); + RevCommit committed = git.commit().setMessage( + "#Comment\ninitial commit\t\n\n commit body \n\n\n \t#another comment ") + .setCleanupMode(CleanupMode.DEFAULT).setDefaultClean(false) + .call(); + + assertEquals( + "#Comment\ninitial commit\n\n commit body\n\n \t#another comment", + committed.getFullMessage()); + } + } + + @Test public void commitEmptyCommits() throws Exception { try (Git git = new Git(db)) { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DescribeCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DescribeCommandTest.java index b460e3f52e..ab87fa9662 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DescribeCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DescribeCommandTest.java @@ -10,9 +10,10 @@ package org.eclipse.jgit.api; import static java.nio.charset.StandardCharsets.UTF_8; +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_ABBREV_STRING_LENGTH; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import java.io.BufferedWriter; @@ -100,6 +101,12 @@ public class DescribeCommandTest extends RepositoryTestCase { assertEquals("alice-t1-2-g3e563c5", describe(c4, "alice*")); assertEquals("bob-t2-1-g3e563c5", describe(c4, "bob*")); assertEquals("bob-t2-1-g3e563c5", describe(c4, "a*", "b*", "c*")); + + assertEquals("bob-t2", describe(c4, false, true, 0)); + assertEquals("bob-t2-1-g3e56", describe(c4, false, true, 1)); + assertEquals("bob-t2-1-g3e56", describe(c4, false, true, -10)); + assertEquals("bob-t2-1-g3e563c55927905f21e3bc7c00a3d83a31bf4ed3a", + describe(c4, false, true, 50)); } else { assertEquals(null, describe(c2)); assertEquals(null, describe(c3)); @@ -108,6 +115,18 @@ public class DescribeCommandTest extends RepositoryTestCase { assertEquals("3747db3", describe(c2, false, true)); assertEquals("44579eb", describe(c3, false, true)); assertEquals("3e563c5", describe(c4, false, true)); + + assertEquals("3747db3267", describe(c2, false, true, 10)); + assertEquals("44579ebe7f", describe(c3, false, true, 10)); + assertEquals("3e563c5592", describe(c4, false, true, 10)); + + assertEquals("3e56", describe(c4, false, true, -10)); + assertEquals("3e56", describe(c4, false, true, 0)); + assertEquals("3e56", describe(c4, false, true, 2)); + assertEquals("3e563c55927905f21e3bc7c00a3d83a31bf4ed3a", + describe(c4, false, true, 40)); + assertEquals("3e563c55927905f21e3bc7c00a3d83a31bf4ed3a", + describe(c4, false, true, 42)); } // test default target @@ -474,10 +493,15 @@ public class DescribeCommandTest extends RepositoryTestCase { } } + private String describe(ObjectId c1, boolean longDesc, boolean always, + int abbrev) throws GitAPIException, IOException { + return git.describe().setTarget(c1).setTags(describeUseAllTags) + .setLong(longDesc).setAlways(always).setAbbrev(abbrev).call(); + } + private String describe(ObjectId c1, boolean longDesc, boolean always) throws GitAPIException, IOException { - return git.describe().setTarget(c1).setTags(describeUseAllTags) - .setLong(longDesc).setAlways(always).call(); + return describe(c1, longDesc, always, OBJECT_ID_ABBREV_STRING_LENGTH); } private String describe(ObjectId c1) throws GitAPIException, IOException { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/EolRepositoryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/EolRepositoryTest.java index d0dfd1ab92..b937b1f6a9 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/EolRepositoryTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/EolRepositoryTest.java @@ -1,40 +1,11 @@ /* - * Copyright (C) 2015, Ivan Motsch <ivan.motsch@bsiag.com> + * Copyright (C) 2015, 2022 Ivan Motsch <ivan.motsch@bsiag.com> and others * - * 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 + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://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. + * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.api; @@ -173,15 +144,22 @@ public class EolRepositoryTest extends RepositoryTestCase { private DirCache dirCache; + private boolean isDefaultCrLf() { + String eol = mockSystemReader.getProperty("line.separator"); + return "\r\n".equals(eol); + } + @Test public void testDefaultSetup() throws Exception { // for EOL to work, the text attribute must be set setupGitAndDoHardReset(null, null, null, null, "* text=auto"); collectRepositoryState(); assertEquals("text=auto", entryCRLF.attrs); - checkEntryContent(entryCRLF, CONTENT_LF, CONTENT_LF); - checkEntryContent(entryLF, CONTENT_LF, CONTENT_LF); - checkEntryContent(entryMixed, CONTENT_LF, CONTENT_LF); + // eol=native is the default! + String expected = isDefaultCrLf() ? CONTENT_CRLF : CONTENT_LF; + checkEntryContent(entryCRLF, expected, CONTENT_LF); + checkEntryContent(entryLF, expected, CONTENT_LF); + checkEntryContent(entryMixed, expected, CONTENT_LF); } public void checkEntryContent(ActualEntry entry, String fileContent, @@ -199,9 +177,11 @@ public class EolRepositoryTest extends RepositoryTestCase { setupGitAndDoHardReset(AutoCRLF.FALSE, null, null, null, "* text=auto"); collectRepositoryState(); assertEquals("text=auto", entryCRLF.attrs); - checkEntryContent(entryCRLF, CONTENT_LF, CONTENT_LF); - checkEntryContent(entryLF, CONTENT_LF, CONTENT_LF); - checkEntryContent(entryMixed, CONTENT_LF, CONTENT_LF); + // eol=native is the default! + String expected = isDefaultCrLf() ? CONTENT_CRLF : CONTENT_LF; + checkEntryContent(entryCRLF, expected, CONTENT_LF); + checkEntryContent(entryLF, expected, CONTENT_LF); + checkEntryContent(entryMixed, expected, CONTENT_LF); } @Test @@ -250,34 +230,24 @@ public class EolRepositoryTest extends RepositoryTestCase { @Test public void test_ConfigEOL_native_windows() throws Exception { - String origLineSeparator = System.getProperty("line.separator", "\n"); - System.setProperty("line.separator", "\r\n"); - try { - // for EOL to work, the text attribute must be set - setupGitAndDoHardReset(null, EOL.NATIVE, "*.txt text", null, null); - collectRepositoryState(); - assertEquals("text", entryCRLF.attrs); - checkEntryContent(entryCRLF, CONTENT_LF, CONTENT_LF); - checkEntryContent(entryLF, CONTENT_LF, CONTENT_LF); - } finally { - System.setProperty("line.separator", origLineSeparator); - } + mockSystemReader.setWindows(); + // for EOL to work, the text attribute must be set + setupGitAndDoHardReset(null, EOL.NATIVE, "*.txt text", null, null); + collectRepositoryState(); + assertEquals("text", entryCRLF.attrs); + checkEntryContent(entryCRLF, CONTENT_CRLF, CONTENT_LF); + checkEntryContent(entryLF, CONTENT_CRLF, CONTENT_LF); } @Test public void test_ConfigEOL_native_xnix() throws Exception { - String origLineSeparator = System.getProperty("line.separator", "\n"); - System.setProperty("line.separator", "\n"); - try { - // for EOL to work, the text attribute must be set - setupGitAndDoHardReset(null, EOL.NATIVE, "*.txt text", null, null); - collectRepositoryState(); - assertEquals("text", entryCRLF.attrs); - checkEntryContent(entryCRLF, CONTENT_LF, CONTENT_LF); - checkEntryContent(entryLF, CONTENT_LF, CONTENT_LF); - } finally { - System.setProperty("line.separator", origLineSeparator); - } + mockSystemReader.setUnix(); + // for EOL to work, the text attribute must be set + setupGitAndDoHardReset(null, EOL.NATIVE, "*.txt text", null, null); + collectRepositoryState(); + assertEquals("text", entryCRLF.attrs); + checkEntryContent(entryCRLF, CONTENT_LF, CONTENT_LF); + checkEntryContent(entryLF, CONTENT_LF, CONTENT_LF); } @Test @@ -297,9 +267,10 @@ public class EolRepositoryTest extends RepositoryTestCase { setupGitAndDoHardReset(AutoCRLF.FALSE, EOL.NATIVE, "*.txt text", null, null); collectRepositoryState(); assertEquals("text", entryCRLF.attrs); - checkEntryContent(entryCRLF, CONTENT_LF, CONTENT_LF); - checkEntryContent(entryLF, CONTENT_LF, CONTENT_LF); - checkEntryContent(entryMixed, CONTENT_LF, CONTENT_LF); + String expected = isDefaultCrLf() ? CONTENT_CRLF : CONTENT_LF; + checkEntryContent(entryCRLF, expected, CONTENT_LF); + checkEntryContent(entryLF, expected, CONTENT_LF); + checkEntryContent(entryMixed, expected, CONTENT_LF); } @Test @@ -524,9 +495,12 @@ public class EolRepositoryTest extends RepositoryTestCase { // info overrides all collectRepositoryState(); assertEquals("text", entryCRLF.attrs); - checkEntryContent(entryCRLF, CONTENT_LF, CONTENT_LF); - checkEntryContent(entryLF, CONTENT_LF, CONTENT_LF); - checkEntryContent(entryMixed, CONTENT_LF, CONTENT_LF); + // !eol means unspecified, so use the default of core.eol, which is + // native. + String expected = isDefaultCrLf() ? CONTENT_CRLF : CONTENT_LF; + checkEntryContent(entryCRLF, expected, CONTENT_LF); + checkEntryContent(entryLF, expected, CONTENT_LF); + checkEntryContent(entryMixed, expected, CONTENT_LF); } @Test diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/FetchAndPullCommandsRecurseSubmodulesTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/FetchAndPullCommandsRecurseSubmodulesTest.java index 36ba7ce5ac..9ea64efc64 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/FetchAndPullCommandsRecurseSubmodulesTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/FetchAndPullCommandsRecurseSubmodulesTest.java @@ -259,7 +259,8 @@ public class FetchAndPullCommandsRecurseSubmodulesTest extends RepositoryTestCas // the commit that was created try (SubmoduleWalk w = SubmoduleWalk.forIndex(git.getRepository())) { assertTrue(w.next()); - try (Git g = new Git(w.getRepository())) { + try (Repository repository = w.getRepository(); + Git g = new Git(repository)) { g.fetch().setRemote(REMOTE).setRefSpecs(REFSPEC).call(); g.reset().setMode(ResetType.HARD).setRef(commit1.name()).call(); } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/FetchCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/FetchCommandTest.java index 6479d157eb..3ec454cfc3 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/FetchCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/FetchCommandTest.java @@ -77,6 +77,26 @@ public class FetchCommandTest extends RepositoryTestCase { } @Test + public void testFetchHasRefLogForRemoteRef() throws Exception { + // create an initial commit SHA1 for the default branch + ObjectId defaultBranchSha1 = remoteGit.commit() + .setMessage("initial commit").call().getId(); + + git.fetch().setRemote("test") + .setRefSpecs("refs/heads/*:refs/remotes/origin/*").call(); + + List<Ref> allFetchedRefs = git.getRepository().getRefDatabase() + .getRefs(); + assertEquals(allFetchedRefs.size(), 1); + Ref remoteRef = allFetchedRefs.get(0); + + assertTrue(remoteRef.getName().startsWith(Constants.R_REMOTES)); + assertEquals(defaultBranchSha1, remoteRef.getObjectId()); + assertNotNull(git.getRepository().getReflogReader(remoteRef.getName()) + .getLastEntry()); + } + + @Test public void testForcedFetch() throws Exception { remoteGit.commit().setMessage("commit").call(); remoteGit.commit().setMessage("commit2").call(); @@ -96,6 +116,53 @@ public class FetchCommandTest extends RepositoryTestCase { } @Test + public void testFetchSimpleNegativeRefSpec() throws Exception { + remoteGit.commit().setMessage("commit").call(); + + FetchResult res = git.fetch().setRemote("test") + .setRefSpecs("refs/heads/master:refs/heads/test", + "^:refs/heads/test") + .call(); + assertNull(res.getTrackingRefUpdate("refs/heads/test")); + + res = git.fetch().setRemote("test") + .setRefSpecs("refs/heads/master:refs/heads/test", + "^refs/heads/master") + .call(); + assertNull(res.getTrackingRefUpdate("refs/heads/test")); + } + + @Test + public void negativeRefSpecFilterBySource() throws Exception { + remoteGit.commit().setMessage("commit").call(); + remoteGit.branchCreate().setName("test").call(); + remoteGit.commit().setMessage("commit1").call(); + remoteGit.branchCreate().setName("dev").call(); + + FetchResult res = git.fetch().setRemote("test") + .setRefSpecs("refs/*:refs/origins/*", "^refs/*/test") + .call(); + assertNotNull(res.getTrackingRefUpdate("refs/origins/heads/master")); + assertNull(res.getTrackingRefUpdate("refs/origins/heads/test")); + assertNotNull(res.getTrackingRefUpdate("refs/origins/heads/dev")); + } + + @Test + public void negativeRefSpecFilterByDestination() throws Exception { + remoteGit.commit().setMessage("commit").call(); + remoteGit.branchCreate().setName("meta").call(); + remoteGit.commit().setMessage("commit1").call(); + remoteGit.branchCreate().setName("data").call(); + + FetchResult res = git.fetch().setRemote("test") + .setRefSpecs("refs/*:refs/secret/*", "^:refs/secret/*/meta") + .call(); + assertNotNull(res.getTrackingRefUpdate("refs/secret/heads/master")); + assertNull(res.getTrackingRefUpdate("refs/secret/heads/meta")); + assertNotNull(res.getTrackingRefUpdate("refs/secret/heads/data")); + } + + @Test public void fetchAddsBranches() throws Exception { final String branch1 = "b1"; final String branch2 = "b2"; diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/LsRemoteCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/LsRemoteCommandTest.java index 12ec2aae57..05af175cfa 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/LsRemoteCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/LsRemoteCommandTest.java @@ -21,6 +21,8 @@ import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.StoredConfig; +import org.eclipse.jgit.util.SystemReader; import org.junit.Test; public class LsRemoteCommandTest extends RepositoryTestCase { @@ -107,6 +109,20 @@ public class LsRemoteCommandTest extends RepositoryTestCase { } @Test + public void testLsRemoteWithoutLocalRepositoryUrlInsteadOf() + throws Exception { + String uri = fileUri(); + StoredConfig userConfig = SystemReader.getInstance().getUserConfig(); + userConfig.load(); + userConfig.setString("url", uri, "insteadOf", "file:///foo"); + userConfig.save(); + Collection<Ref> refs = Git.lsRemoteRepository().setRemote("file:///foo") + .setHeads(true).call(); + assertNotNull(refs); + assertEquals(2, refs.size()); + } + + @Test public void testLsRemoteWithSymRefs() throws Exception { File directory = createTempDirectory("testRepository"); CloneCommand command = Git.cloneRepository(); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java index bc4e9405ea..917b6c3297 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java @@ -36,6 +36,7 @@ import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.RepositoryState; import org.eclipse.jgit.lib.Sets; +import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.merge.ContentMergeStrategy; import org.eclipse.jgit.merge.MergeStrategy; import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason; @@ -554,7 +555,7 @@ public class MergeCommandTest extends RepositoryTestCase { git.merge().include(sideBranch) .setStrategy(MergeStrategy.RESOLVE).call(); - assertEquals("Merge branch 'side'\n\nConflicts:\n\ta\n", + assertEquals("Merge branch 'side'\n\n# Conflicts:\n#\ta\n", db.readMergeCommitMsg()); } @@ -1787,7 +1788,7 @@ public class MergeCommandTest extends RepositoryTestCase { + dateFormatter.formatDate(third .getAuthorIdent()) + "\n\n\tthird commit\n", db.readSquashCommitMsg()); - assertEquals("\nConflicts:\n\tfile2\n", db.readMergeCommitMsg()); + assertEquals("\n# Conflicts:\n#\tfile2\n", db.readMergeCommitMsg()); Status stat = git.status().call(); assertEquals(Sets.of("file2"), stat.getConflicting()); @@ -1881,6 +1882,7 @@ public class MergeCommandTest extends RepositoryTestCase { @Test public void testRecursiveMergeWithConflict() throws Exception { try (TestRepository<Repository> db_t = new TestRepository<>(db)) { + db.incrementOpen(); BranchBuilder master = db_t.branch("master"); RevCommit m0 = master.commit() .add("f", "1\n2\n3\n4\n5\n6\n7\n8\n9\n").message("m0") @@ -2012,7 +2014,74 @@ public class MergeCommandTest extends RepositoryTestCase { git.merge().include(sideBranch).setStrategy(MergeStrategy.RESOLVE) .setMessage("user message").call(); - assertEquals("user message\n\nConflicts:\n\ta\n", + assertEquals("user message\n\n# Conflicts:\n#\ta\n", + db.readMergeCommitMsg()); + } + } + + @Test + public void testMergeConflictWithMessageAndCommentChar() throws Exception { + try (Git git = new Git(db)) { + writeTrashFile("a", "1\na\n3\n"); + git.add().addFilepattern("a").call(); + RevCommit initialCommit = git.commit().setMessage("initial").call(); + + createBranch(initialCommit, "refs/heads/side"); + checkoutBranch("refs/heads/side"); + + writeTrashFile("a", "1\na(side)\n3\n"); + git.add().addFilepattern("a").call(); + git.commit().setMessage("side").call(); + + checkoutBranch("refs/heads/master"); + + writeTrashFile("a", "1\na(main)\n3\n"); + git.add().addFilepattern("a").call(); + git.commit().setMessage("main").call(); + + StoredConfig config = db.getConfig(); + config.setString("core", null, "commentChar", "^"); + + Ref sideBranch = db.exactRef("refs/heads/side"); + + git.merge().include(sideBranch).setStrategy(MergeStrategy.RESOLVE) + .setMessage("user message").call(); + + assertEquals("user message\n\n^ Conflicts:\n^\ta\n", + db.readMergeCommitMsg()); + } + } + + @Test + public void testMergeConflictWithMessageAndCommentCharAuto() + throws Exception { + try (Git git = new Git(db)) { + writeTrashFile("a", "1\na\n3\n"); + git.add().addFilepattern("a").call(); + RevCommit initialCommit = git.commit().setMessage("initial").call(); + + createBranch(initialCommit, "refs/heads/side"); + checkoutBranch("refs/heads/side"); + + writeTrashFile("a", "1\na(side)\n3\n"); + git.add().addFilepattern("a").call(); + git.commit().setMessage("side").call(); + + checkoutBranch("refs/heads/master"); + + writeTrashFile("a", "1\na(main)\n3\n"); + git.add().addFilepattern("a").call(); + git.commit().setMessage("main").call(); + + StoredConfig config = db.getConfig(); + config.setString("core", null, "commentChar", "auto"); + + Ref sideBranch = db.exactRef("refs/heads/side"); + + git.merge().include(sideBranch).setStrategy(MergeStrategy.RESOLVE) + .setMessage("#user message").call(); + + assertEquals("#user message\n\n; Conflicts:\n;\ta\n", db.readMergeCommitMsg()); } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java index 9af77aa3e8..6a84f0a38d 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java @@ -567,6 +567,7 @@ public class PullCommandTest extends RepositoryTestCase { public void setUp() throws Exception { super.setUp(); dbTarget = createWorkRepository(); + addRepoToClose(dbTarget); source = new Git(db); target = new Git(dbTarget); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandWithRebaseTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandWithRebaseTest.java index cce04f471b..e2d7923cee 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandWithRebaseTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandWithRebaseTest.java @@ -323,6 +323,7 @@ public class PullCommandWithRebaseTest extends RepositoryTestCase { dbTarget = createWorkRepository(); source = new Git(db); target = new Git(dbTarget); + addRepoToClose(dbTarget); // put some file in the source repo sourceFile = new File(db.getWorkTree(), "SomeFile.txt"); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java index a786065561..6f7aa63edc 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java @@ -10,7 +10,9 @@ package org.eclipse.jgit.api; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -19,10 +21,13 @@ import java.io.IOException; import java.net.URISyntaxException; import java.util.Properties; +import org.eclipse.jgit.api.errors.DetachedHeadException; import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.InvalidRefNameException; import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.api.errors.TransportException; import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.errors.NoRemoteRepositoryException; import org.eclipse.jgit.hooks.PrePushHook; import org.eclipse.jgit.junit.JGitTestUtil; import org.eclipse.jgit.junit.RepositoryTestCase; @@ -32,6 +37,7 @@ import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.transport.PushConfig.PushDefault; import org.eclipse.jgit.transport.PushResult; import org.eclipse.jgit.transport.RefLeaseSpec; import org.eclipse.jgit.transport.RefSpec; @@ -50,6 +56,7 @@ public class PushCommandTest extends RepositoryTestCase { // create other repository Repository db2 = createWorkRepository(); + addRepoToClose(db2); final StoredConfig config2 = db2.getConfig(); // this tests that this config can be parsed properly @@ -289,6 +296,770 @@ public class PushCommandTest extends RepositoryTestCase { } /** + * Check that pushing from a detached HEAD without refspec throws a + * DetachedHeadException. + * + * @throws Exception + */ + @Test + public void testPushDefaultDetachedHead() throws Exception { + try (Git git = new Git(db); + Git git2 = new Git(createBareRepository())) { + final StoredConfig config = git.getRepository().getConfig(); + RemoteConfig remoteConfig = new RemoteConfig(config, "test"); + URIish uri = new URIish( + git2.getRepository().getDirectory().toURI().toURL()); + remoteConfig.addURI(uri); + remoteConfig.addFetchRefSpec( + new RefSpec("+refs/heads/*:refs/remotes/origin/*")); + remoteConfig.update(config); + config.save(); + + writeTrashFile("f", "content of f"); + git.add().addFilepattern("f").call(); + RevCommit commit = git.commit().setMessage("adding f").call(); + + git.checkout().setName("not-pushed").setCreateBranch(true).call(); + git.checkout().setName("branchtopush").setCreateBranch(true).call(); + git.checkout().setName(commit.getName()).call(); + String head = git.getRepository().getFullBranch(); + assertTrue(ObjectId.isId(head)); + assertEquals(commit.getName(), head); + assertThrows(DetachedHeadException.class, + () -> git.push().setRemote("test").call()); + } + } + + /** + * Check that push.default=nothing without refspec throws an + * InvalidRefNameException. + * + * @throws Exception + */ + @Test + public void testPushDefaultNothing() throws Exception { + try (Git git = new Git(db); + Git git2 = new Git(createBareRepository())) { + final StoredConfig config = git.getRepository().getConfig(); + RemoteConfig remoteConfig = new RemoteConfig(config, "test"); + URIish uri = new URIish( + git2.getRepository().getDirectory().toURI().toURL()); + remoteConfig.addURI(uri); + remoteConfig.addFetchRefSpec( + new RefSpec("+refs/heads/*:refs/remotes/origin/*")); + remoteConfig.update(config); + config.save(); + + writeTrashFile("f", "content of f"); + git.add().addFilepattern("f").call(); + git.commit().setMessage("adding f").call(); + + git.checkout().setName("not-pushed").setCreateBranch(true).call(); + git.checkout().setName("branchtopush").setCreateBranch(true).call(); + + assertEquals(null, + git2.getRepository().resolve("refs/heads/branchtopush")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/not-pushed")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/master")); + assertThrows(InvalidRefNameException.class, + () -> git.push().setRemote("test") + .setPushDefault(PushDefault.NOTHING).call()); + } + } + + /** + * Check that push.default=matching without refspec pushes all matching + * branches. + * + * @throws Exception + */ + @Test + public void testPushDefaultMatching() throws Exception { + try (Git git = new Git(db); + Git git2 = new Git(createBareRepository())) { + final StoredConfig config = git.getRepository().getConfig(); + RemoteConfig remoteConfig = new RemoteConfig(config, "test"); + URIish uri = new URIish( + git2.getRepository().getDirectory().toURI().toURL()); + remoteConfig.addURI(uri); + remoteConfig.addFetchRefSpec( + new RefSpec("+refs/heads/*:refs/remotes/origin/*")); + remoteConfig.update(config); + config.save(); + + writeTrashFile("f", "content of f"); + git.add().addFilepattern("f").call(); + RevCommit commit = git.commit().setMessage("adding f").call(); + + git.checkout().setName("not-pushed").setCreateBranch(true).call(); + git.checkout().setName("branchtopush").setCreateBranch(true).call(); + + assertEquals(null, + git2.getRepository().resolve("refs/heads/branchtopush")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/not-pushed")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/master")); + // push master and branchtopush + git.push().setRemote("test").setRefSpecs( + new RefSpec("refs/heads/master:refs/heads/master"), + new RefSpec( + "refs/heads/branchtopush:refs/heads/branchtopush")) + .call(); + assertEquals(commit.getId(), + git2.getRepository().resolve("refs/heads/master")); + assertEquals(commit.getId(), + git2.getRepository().resolve("refs/heads/branchtopush")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/not-pushed")); + // Create two different commits on these two branches + writeTrashFile("b", "on branchtopush"); + git.add().addFilepattern("b").call(); + RevCommit bCommit = git.commit().setMessage("on branchtopush") + .call(); + git.checkout().setName("master").call(); + writeTrashFile("m", "on master"); + git.add().addFilepattern("m").call(); + RevCommit mCommit = git.commit().setMessage("on master").call(); + // Now push with mode "matching": should push both branches. + Iterable<PushResult> result = git.push().setRemote("test") + .setPushDefault(PushDefault.MATCHING) + .call(); + int n = 0; + for (PushResult r : result) { + n++; + assertEquals(1, n); + assertEquals(2, r.getRemoteUpdates().size()); + for (RemoteRefUpdate update : r.getRemoteUpdates()) { + assertFalse(update.isMatching()); + assertTrue(update.getSrcRef() + .equals("refs/heads/branchtopush") + || update.getSrcRef().equals("refs/heads/master")); + assertEquals(RemoteRefUpdate.Status.OK, update.getStatus()); + } + } + assertEquals(bCommit.getId(), + git2.getRepository().resolve("refs/heads/branchtopush")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/not-pushed")); + assertEquals(mCommit.getId(), + git2.getRepository().resolve("refs/heads/master")); + assertEquals(bCommit.getId(), git.getRepository() + .resolve("refs/remotes/origin/branchtopush")); + assertEquals(null, git.getRepository() + .resolve("refs/remotes/origin/not-pushed")); + assertEquals(mCommit.getId(), + git.getRepository().resolve("refs/remotes/origin/master")); + } + } + + /** + * Check that push.default=upstream without refspec pushes only the current + * branch to the configured upstream. + * + * @throws Exception + */ + @Test + public void testPushDefaultUpstream() throws Exception { + try (Git git = new Git(db); + Git git2 = new Git(createBareRepository())) { + StoredConfig config = git.getRepository().getConfig(); + RemoteConfig remoteConfig = new RemoteConfig(config, "test"); + URIish uri = new URIish( + git2.getRepository().getDirectory().toURI().toURL()); + remoteConfig.addURI(uri); + remoteConfig.addFetchRefSpec( + new RefSpec("+refs/heads/*:refs/remotes/origin/*")); + remoteConfig.update(config); + config.save(); + + writeTrashFile("f", "content of f"); + git.add().addFilepattern("f").call(); + RevCommit commit = git.commit().setMessage("adding f").call(); + + git.checkout().setName("not-pushed").setCreateBranch(true).call(); + git.checkout().setName("branchtopush").setCreateBranch(true).call(); + + config = git.getRepository().getConfig(); + config.setString("branch", "branchtopush", "remote", "test"); + config.setString("branch", "branchtopush", "merge", + "refs/heads/upstreambranch"); + config.save(); + + assertEquals(null, + git2.getRepository().resolve("refs/heads/branchtopush")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/upstreambranch")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/not-pushed")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/master")); + git.push().setRemote("test").setPushDefault(PushDefault.UPSTREAM) + .call(); + assertEquals(null, + git2.getRepository().resolve("refs/heads/branchtopush")); + assertEquals(commit.getId(), + git2.getRepository().resolve("refs/heads/upstreambranch")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/not-pushed")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/master")); + assertEquals(commit.getId(), git.getRepository() + .resolve("refs/remotes/origin/upstreambranch")); + assertEquals(null, git.getRepository() + .resolve("refs/remotes/origin/branchtopush")); + } + } + + /** + * Check that push.default=upstream without refspec throws an + * InvalidRefNameException if the current branch has no upstream. + * + * @throws Exception + */ + @Test + public void testPushDefaultUpstreamNoTracking() throws Exception { + try (Git git = new Git(db); + Git git2 = new Git(createBareRepository())) { + StoredConfig config = git.getRepository().getConfig(); + RemoteConfig remoteConfig = new RemoteConfig(config, "test"); + URIish uri = new URIish( + git2.getRepository().getDirectory().toURI().toURL()); + remoteConfig.addURI(uri); + remoteConfig.addFetchRefSpec( + new RefSpec("+refs/heads/*:refs/remotes/origin/*")); + remoteConfig.update(config); + config.save(); + + writeTrashFile("f", "content of f"); + git.add().addFilepattern("f").call(); + git.commit().setMessage("adding f").call(); + + git.checkout().setName("not-pushed").setCreateBranch(true).call(); + git.checkout().setName("branchtopush").setCreateBranch(true).call(); + + config = git.getRepository().getConfig(); + config.setString("branch", "branchtopush", "remote", "test"); + config.save(); + + assertThrows(InvalidRefNameException.class, + () -> git.push().setRemote("test") + .setPushDefault(PushDefault.UPSTREAM).call()); + } + } + + /** + * Check that push.default=upstream without refspec throws an + * InvalidRefNameException if the push remote is not the same as the fetch + * remote. + * + * @throws Exception + */ + @Test + public void testPushDefaultUpstreamTriangular() throws Exception { + try (Git git = new Git(db); + Git git2 = new Git(createBareRepository())) { + StoredConfig config = git.getRepository().getConfig(); + RemoteConfig remoteConfig = new RemoteConfig(config, "test"); + URIish uri = new URIish( + git2.getRepository().getDirectory().toURI().toURL()); + remoteConfig.addURI(uri); + remoteConfig.addFetchRefSpec( + new RefSpec("+refs/heads/*:refs/remotes/origin/*")); + remoteConfig.update(config); + config.save(); + + writeTrashFile("f", "content of f"); + git.add().addFilepattern("f").call(); + git.commit().setMessage("adding f").call(); + + git.checkout().setName("not-pushed").setCreateBranch(true).call(); + git.checkout().setName("branchtopush").setCreateBranch(true).call(); + + config = git.getRepository().getConfig(); + // Don't configure a remote; it'll default to "origin". + config.setString("branch", "branchtopush", "merge", + "upstreambranch"); + config.save(); + + assertThrows(InvalidRefNameException.class, + () -> git.push().setRemote("test") + .setPushDefault(PushDefault.UPSTREAM).call()); + } + } + + /** + * Check that push.default=simple without refspec pushes only the current + * branch to the configured upstream name. + * + * @throws Exception + */ + @Test + public void testPushDefaultSimple() throws Exception { + try (Git git = new Git(db); + Git git2 = new Git(createBareRepository())) { + StoredConfig config = git.getRepository().getConfig(); + RemoteConfig remoteConfig = new RemoteConfig(config, "test"); + URIish uri = new URIish( + git2.getRepository().getDirectory().toURI().toURL()); + remoteConfig.addURI(uri); + remoteConfig.addFetchRefSpec( + new RefSpec("+refs/heads/*:refs/remotes/origin/*")); + remoteConfig.update(config); + config.save(); + + writeTrashFile("f", "content of f"); + git.add().addFilepattern("f").call(); + RevCommit commit = git.commit().setMessage("adding f").call(); + + git.checkout().setName("not-pushed").setCreateBranch(true).call(); + git.checkout().setName("branchtopush").setCreateBranch(true).call(); + + config = git.getRepository().getConfig(); + config.setString("branch", "branchtopush", "remote", "test"); + config.setString("branch", "branchtopush", "merge", + "refs/heads/branchtopush"); + config.save(); + + assertEquals(null, + git2.getRepository().resolve("refs/heads/branchtopush")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/not-pushed")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/master")); + git.push().setRemote("test").setPushDefault(PushDefault.SIMPLE) + .call(); + assertEquals(commit.getId(), + git2.getRepository().resolve("refs/heads/branchtopush")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/not-pushed")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/master")); + assertEquals(commit.getId(), git.getRepository() + .resolve("refs/remotes/origin/branchtopush")); + } + } + + /** + * Check that push.default=simple without refspec pushes only the current + * branch to a branch with the same name in a triangular workflow. + * + * @throws Exception + */ + @Test + public void testPushDefaultSimpleTriangular() throws Exception { + try (Git git = new Git(db); + Git git2 = new Git(createBareRepository())) { + StoredConfig config = git.getRepository().getConfig(); + RemoteConfig remoteConfig = new RemoteConfig(config, "test"); + URIish uri = new URIish( + git2.getRepository().getDirectory().toURI().toURL()); + remoteConfig.addURI(uri); + remoteConfig.addFetchRefSpec( + new RefSpec("+refs/heads/*:refs/remotes/origin/*")); + remoteConfig.update(config); + config.save(); + + writeTrashFile("f", "content of f"); + git.add().addFilepattern("f").call(); + RevCommit commit = git.commit().setMessage("adding f").call(); + + git.checkout().setName("not-pushed").setCreateBranch(true).call(); + git.checkout().setName("branchtopush").setCreateBranch(true).call(); + + config = git.getRepository().getConfig(); + // Don't set remote, it'll default to "origin". Configure a + // different branch name; should be ignored. + config.setString("branch", "branchtopush", "merge", + "refs/heads/upstreambranch"); + config.save(); + + assertEquals(null, + git2.getRepository().resolve("refs/heads/branchtopush")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/upstreambranch")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/not-pushed")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/master")); + git.push().setRemote("test").setPushDefault(PushDefault.SIMPLE) + .call(); + assertEquals(commit.getId(), + git2.getRepository().resolve("refs/heads/branchtopush")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/upstreambranch")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/not-pushed")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/master")); + assertEquals(commit.getId(), git.getRepository() + .resolve("refs/remotes/origin/branchtopush")); + } + } + + /** + * Check that push.default=simple without refspec throws an + * InvalidRefNameException if the current branch has no upstream. + * + * @throws Exception + */ + @Test + public void testPushDefaultSimpleNoTracking() throws Exception { + try (Git git = new Git(db); + Git git2 = new Git(createBareRepository())) { + StoredConfig config = git.getRepository().getConfig(); + RemoteConfig remoteConfig = new RemoteConfig(config, "test"); + URIish uri = new URIish( + git2.getRepository().getDirectory().toURI().toURL()); + remoteConfig.addURI(uri); + remoteConfig.addFetchRefSpec( + new RefSpec("+refs/heads/*:refs/remotes/origin/*")); + remoteConfig.update(config); + config.save(); + + writeTrashFile("f", "content of f"); + git.add().addFilepattern("f").call(); + git.commit().setMessage("adding f").call(); + + git.checkout().setName("not-pushed").setCreateBranch(true).call(); + git.checkout().setName("branchtopush").setCreateBranch(true).call(); + + config = git.getRepository().getConfig(); + config.setString("branch", "branchtopush", "remote", "test"); + config.save(); + + assertThrows(InvalidRefNameException.class, + () -> git.push().setRemote("test") + .setPushDefault(PushDefault.SIMPLE).call()); + } + } + + /** + * Check that push.default=simple without refspec throws an + * InvalidRefNameException if the current branch has an upstream with a + * different name. + * + * @throws Exception + */ + @Test + public void testPushDefaultSimpleDifferentTracking() throws Exception { + try (Git git = new Git(db); + Git git2 = new Git(createBareRepository())) { + StoredConfig config = git.getRepository().getConfig(); + RemoteConfig remoteConfig = new RemoteConfig(config, "test"); + URIish uri = new URIish( + git2.getRepository().getDirectory().toURI().toURL()); + remoteConfig.addURI(uri); + remoteConfig.addFetchRefSpec( + new RefSpec("+refs/heads/*:refs/remotes/origin/*")); + remoteConfig.update(config); + config.save(); + + writeTrashFile("f", "content of f"); + git.add().addFilepattern("f").call(); + git.commit().setMessage("adding f").call(); + + git.checkout().setName("not-pushed").setCreateBranch(true).call(); + git.checkout().setName("branchtopush").setCreateBranch(true).call(); + + config = git.getRepository().getConfig(); + config.setString("branch", "branchtopush", "remote", "test"); + config.setString("branch", "branchtopush", "merge", + "refs/heads/upstreambranch"); + config.save(); + + assertThrows(InvalidRefNameException.class, + () -> git.push().setRemote("test") + .setPushDefault(PushDefault.SIMPLE).call()); + } + } + + /** + * Check that if no PushDefault is set, the value is read from the git + * config. + * + * @throws Exception + */ + @Test + public void testPushDefaultFromConfig() throws Exception { + try (Git git = new Git(db); + Git git2 = new Git(createBareRepository())) { + StoredConfig config = git.getRepository().getConfig(); + RemoteConfig remoteConfig = new RemoteConfig(config, "test"); + URIish uri = new URIish( + git2.getRepository().getDirectory().toURI().toURL()); + remoteConfig.addURI(uri); + remoteConfig.addFetchRefSpec( + new RefSpec("+refs/heads/*:refs/remotes/origin/*")); + remoteConfig.update(config); + config.setString("push", null, "default", "upstream"); + config.save(); + + writeTrashFile("f", "content of f"); + git.add().addFilepattern("f").call(); + RevCommit commit = git.commit().setMessage("adding f").call(); + + git.checkout().setName("not-pushed").setCreateBranch(true).call(); + git.checkout().setName("branchtopush").setCreateBranch(true).call(); + + config = git.getRepository().getConfig(); + config.setString("branch", "branchtopush", "remote", "test"); + config.setString("branch", "branchtopush", "merge", + "refs/heads/upstreambranch"); + config.save(); + + assertEquals(null, + git2.getRepository().resolve("refs/heads/branchtopush")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/upstreambranch")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/not-pushed")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/master")); + PushCommand cmd = git.push(); + cmd.setRemote("test").setPushDefault(null).call(); + assertEquals(PushDefault.UPSTREAM, cmd.getPushDefault()); + assertEquals(null, + git2.getRepository().resolve("refs/heads/branchtopush")); + assertEquals(commit.getId(), + git2.getRepository().resolve("refs/heads/upstreambranch")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/not-pushed")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/master")); + assertEquals(commit.getId(), git.getRepository() + .resolve("refs/remotes/origin/upstreambranch")); + assertEquals(null, git.getRepository() + .resolve("refs/remotes/origin/branchtopush")); + } + } + + /** + * Check that if no PushDefault is set and none is set in the git config, it + * defaults to "simple". + * + * @throws Exception + */ + @Test + public void testPushDefaultFromConfigDefault() throws Exception { + try (Git git = new Git(db); + Git git2 = new Git(createBareRepository())) { + StoredConfig config = git.getRepository().getConfig(); + RemoteConfig remoteConfig = new RemoteConfig(config, "test"); + URIish uri = new URIish( + git2.getRepository().getDirectory().toURI().toURL()); + remoteConfig.addURI(uri); + remoteConfig.addFetchRefSpec( + new RefSpec("+refs/heads/*:refs/remotes/origin/*")); + remoteConfig.update(config); + config.save(); + + writeTrashFile("f", "content of f"); + git.add().addFilepattern("f").call(); + RevCommit commit = git.commit().setMessage("adding f").call(); + + git.checkout().setName("not-pushed").setCreateBranch(true).call(); + git.checkout().setName("branchtopush").setCreateBranch(true).call(); + + config = git.getRepository().getConfig(); + config.setString("branch", "branchtopush", "remote", "test"); + config.setString("branch", "branchtopush", "merge", + "refs/heads/branchtopush"); + config.save(); + + assertEquals(null, + git2.getRepository().resolve("refs/heads/branchtopush")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/not-pushed")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/master")); + PushCommand cmd = git.push(); + cmd.setRemote("test").setPushDefault(null).call(); + assertEquals(PushDefault.SIMPLE, cmd.getPushDefault()); + assertEquals(commit.getId(), + git2.getRepository().resolve("refs/heads/branchtopush")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/not-pushed")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/master")); + assertEquals(commit.getId(), git.getRepository() + .resolve("refs/remotes/origin/branchtopush")); + } + } + + /** + * Check that branch.<name>.pushRemote overrides anything else. + * + * @throws Exception + */ + @Test + public void testBranchPushRemote() throws Exception { + try (Git git = new Git(db); + Git git2 = new Git(createBareRepository())) { + StoredConfig config = git.getRepository().getConfig(); + RemoteConfig remoteConfig = new RemoteConfig(config, "test"); + URIish uri = new URIish( + git2.getRepository().getDirectory().toURI().toURL()); + remoteConfig.addURI(uri); + remoteConfig.addFetchRefSpec( + new RefSpec("+refs/heads/*:refs/remotes/origin/*")); + remoteConfig.update(config); + config.setString("remote", null, "pushDefault", "test"); + config.save(); + + writeTrashFile("f", "content of f"); + git.add().addFilepattern("f").call(); + git.commit().setMessage("adding f").call(); + + git.checkout().setName("not-pushed").setCreateBranch(true).call(); + git.checkout().setName("branchtopush").setCreateBranch(true).call(); + + config = git.getRepository().getConfig(); + config.setString("branch", "branchtopush", "remote", "test"); + config.setString("branch", "branchtopush", "pushremote", "origin"); + config.setString("branch", "branchtopush", "merge", + "refs/heads/branchtopush"); + config.save(); + + assertThrows(InvalidRefNameException.class, () -> git.push() + .setPushDefault(PushDefault.UPSTREAM).call()); + } + } + + /** + * Check that remote.pushDefault overrides branch.<name>.remote + * + * @throws Exception + */ + @Test + public void testRemotePushDefault() throws Exception { + try (Git git = new Git(db); + Git git2 = new Git(createBareRepository())) { + StoredConfig config = git.getRepository().getConfig(); + RemoteConfig remoteConfig = new RemoteConfig(config, "test"); + URIish uri = new URIish( + git2.getRepository().getDirectory().toURI().toURL()); + remoteConfig.addURI(uri); + remoteConfig.addFetchRefSpec( + new RefSpec("+refs/heads/*:refs/remotes/origin/*")); + remoteConfig.update(config); + config.setString("remote", null, "pushDefault", "origin"); + config.save(); + + writeTrashFile("f", "content of f"); + git.add().addFilepattern("f").call(); + git.commit().setMessage("adding f").call(); + + git.checkout().setName("not-pushed").setCreateBranch(true).call(); + git.checkout().setName("branchtopush").setCreateBranch(true).call(); + + config = git.getRepository().getConfig(); + config.setString("branch", "branchtopush", "remote", "test"); + config.setString("branch", "branchtopush", "merge", + "refs/heads/branchtopush"); + config.save(); + + assertThrows(InvalidRefNameException.class, () -> git.push() + .setPushDefault(PushDefault.UPSTREAM).call()); + } + } + + /** + * Check that ultimately we fall back to "origin". + * + * @throws Exception + */ + @Test + public void testDefaultRemote() throws Exception { + try (Git git = new Git(db); + Git git2 = new Git(createBareRepository())) { + StoredConfig config = git.getRepository().getConfig(); + RemoteConfig remoteConfig = new RemoteConfig(config, "test"); + URIish uri = new URIish( + git2.getRepository().getDirectory().toURI().toURL()); + remoteConfig.addURI(uri); + remoteConfig.addFetchRefSpec( + new RefSpec("+refs/heads/*:refs/remotes/origin/*")); + remoteConfig.update(config); + config.save(); + + writeTrashFile("f", "content of f"); + git.add().addFilepattern("f").call(); + git.commit().setMessage("adding f").call(); + + git.checkout().setName("not-pushed").setCreateBranch(true).call(); + git.checkout().setName("branchtopush").setCreateBranch(true).call(); + + config = git.getRepository().getConfig(); + config.setString("branch", "branchtopush", "merge", + "refs/heads/branchtopush"); + config.save(); + + PushCommand cmd = git.push().setPushDefault(PushDefault.UPSTREAM); + TransportException e = assertThrows(TransportException.class, + () -> cmd.call()); + assertEquals(NoRemoteRepositoryException.class, + e.getCause().getClass()); + assertEquals("origin", cmd.getRemote()); + } + } + + /** + * Check that a push without specifying a remote or mode or anything can + * succeed if the git config is correct. + * + * @throws Exception + */ + @Test + public void testDefaultPush() throws Exception { + try (Git git = new Git(db); + Git git2 = new Git(createBareRepository())) { + StoredConfig config = git.getRepository().getConfig(); + RemoteConfig remoteConfig = new RemoteConfig(config, "test"); + URIish uri = new URIish( + git2.getRepository().getDirectory().toURI().toURL()); + remoteConfig.addURI(uri); + remoteConfig.addFetchRefSpec( + new RefSpec("+refs/heads/*:refs/remotes/origin/*")); + remoteConfig.update(config); + config.save(); + + writeTrashFile("f", "content of f"); + git.add().addFilepattern("f").call(); + RevCommit commit = git.commit().setMessage("adding f").call(); + + git.checkout().setName("not-pushed").setCreateBranch(true).call(); + git.checkout().setName("branchtopush").setCreateBranch(true).call(); + + config = git.getRepository().getConfig(); + config.setString("branch", "branchtopush", "remote", "test"); + config.save(); + + assertEquals(null, + git2.getRepository().resolve("refs/heads/branchtopush")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/not-pushed")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/master")); + // Should use remote "test", push.default=current + PushCommand cmd = git.push(); + cmd.call(); + assertEquals("test", cmd.getRemote()); + assertEquals(PushDefault.CURRENT, cmd.getPushDefault()); + assertEquals(commit.getId(), + git2.getRepository().resolve("refs/heads/branchtopush")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/not-pushed")); + assertEquals(null, + git2.getRepository().resolve("refs/heads/master")); + assertEquals(commit.getId(), git.getRepository() + .resolve("refs/remotes/origin/branchtopush")); + } + } + + /** * Check that missing refs don't cause errors during push * * @throws Exception @@ -297,6 +1068,7 @@ public class PushCommandTest extends RepositoryTestCase { public void testPushAfterGC() throws Exception { // create other repository Repository db2 = createWorkRepository(); + addRepoToClose(db2); // setup the first repository final StoredConfig config = db.getConfig(); @@ -360,6 +1132,7 @@ public class PushCommandTest extends RepositoryTestCase { // create other repository Repository db2 = createWorkRepository(); + addRepoToClose(db2); // setup the first repository final StoredConfig config = db.getConfig(); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java index 86239023dc..d574e45f6f 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java @@ -30,6 +30,7 @@ import java.util.List; import org.eclipse.jgit.api.MergeResult.MergeStatus; import org.eclipse.jgit.api.RebaseCommand.InteractiveHandler; +import org.eclipse.jgit.api.RebaseCommand.InteractiveHandler2; import org.eclipse.jgit.api.RebaseCommand.Operation; import org.eclipse.jgit.api.RebaseResult.Status; import org.eclipse.jgit.api.errors.InvalidRebaseStepException; @@ -46,6 +47,7 @@ import org.eclipse.jgit.events.ChangeRecorder; import org.eclipse.jgit.events.ListenerHandle; import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.lib.AbbreviatedObjectId; +import org.eclipse.jgit.lib.CommitConfig.CleanupMode; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; @@ -56,6 +58,7 @@ import org.eclipse.jgit.lib.RebaseTodoLine.Action; import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.ReflogEntry; import org.eclipse.jgit.lib.RepositoryState; +import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.merge.MergeStrategy; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevSort; @@ -2927,8 +2930,8 @@ public class RebaseCommandTest extends RepositoryTestCase { } } - @Test - public void testRebaseInteractiveFixupWithBlankLines() throws Exception { + private void simpleFixup(String firstMessage, String secondMessage) + throws Exception { // create file1 on master writeTrashFile(FILE1, FILE1); git.add().addFilepattern(FILE1).call(); @@ -2938,13 +2941,13 @@ public class RebaseCommandTest extends RepositoryTestCase { // create file2 on master writeTrashFile("file2", "file2"); git.add().addFilepattern("file2").call(); - git.commit().setMessage("Add file2").call(); + git.commit().setMessage(firstMessage).call(); assertTrue(new File(db.getWorkTree(), "file2").exists()); // update FILE1 on master writeTrashFile(FILE1, "blah"); git.add().addFilepattern(FILE1).call(); - git.commit().setMessage("updated file1 on master\n\nsome text").call(); + git.commit().setMessage(secondMessage).call(); git.rebase().setUpstream("HEAD~2") .runInteractively(new InteractiveHandler() { @@ -2968,9 +2971,31 @@ public class RebaseCommandTest extends RepositoryTestCase { try (RevWalk walk = new RevWalk(db)) { ObjectId headId = db.resolve(Constants.HEAD); RevCommit headCommit = walk.parseCommit(headId); - assertEquals("Add file2", - headCommit.getFullMessage()); + assertEquals(firstMessage, headCommit.getFullMessage()); } + + } + + @Test + public void testRebaseInteractiveFixupWithBlankLines() throws Exception { + simpleFixup("Add file2", "updated file1 on master\n\nsome text"); + } + + @Test + public void testRebaseInteractiveFixupWithBlankLines2() throws Exception { + simpleFixup("Add file2\n\nBody\n", + "updated file1 on master\n\nsome text"); + } + + @Test + public void testRebaseInteractiveFixupWithHash() throws Exception { + simpleFixup("#Add file2", "updated file1 on master"); + } + + @Test + public void testRebaseInteractiveFixupWithHash2() throws Exception { + simpleFixup("#Add file2\n\nHeader has hash\n", + "#updated file1 on master"); } @Test(expected = InvalidRebaseStepException.class) @@ -3388,6 +3413,99 @@ public class RebaseCommandTest extends RepositoryTestCase { } + @Test + public void testInteractiveRebaseSquashFixupSequence() throws Exception { + // create file1, add and commit + writeTrashFile(FILE1, "file1"); + git.add().addFilepattern(FILE1).call(); + git.commit().setMessage("commit1").call(); + + // modify file1, add and commit + writeTrashFile(FILE1, "modified file1"); + git.add().addFilepattern(FILE1).call(); + git.commit().setMessage("commit2").call(); + + // modify file1, add and commit + writeTrashFile(FILE1, "modified file1 a second time"); + git.add().addFilepattern(FILE1).call(); + // Make it difficult; use git standard comment characters in the commit + // messages + git.commit().setMessage("#commit3").call(); + + // modify file1, add and commit + writeTrashFile(FILE1, "modified file1 a third time"); + git.add().addFilepattern(FILE1).call(); + git.commit().setMessage("@commit4").call(); + + // modify file1, add and commit + writeTrashFile(FILE1, "modified file1 a fourth time"); + git.add().addFilepattern(FILE1).call(); + git.commit().setMessage(";commit5").call(); + + StoredConfig config = git.getRepository().getConfig(); + config.setString("core", null, "commentChar", "auto"); + // With "auto", we should end up with '@' being used as comment + // character (commit4 is skipped, so it should not advance the + // character). + RebaseResult result = git.rebase().setUpstream("HEAD~4") + .runInteractively(new InteractiveHandler2() { + + @Override + public void prepareSteps(List<RebaseTodoLine> steps) { + try { + steps.get(0).setAction(Action.PICK); + steps.get(1).setAction(Action.SQUASH); + steps.get(2).setAction(Action.FIXUP); + steps.get(3).setAction(Action.SQUASH); + } catch (IllegalTodoFileModification e) { + fail("unexpected exception: " + e); + } + } + + @Override + public String modifyCommitMessage(String commit) { + fail("should not be called"); + return commit; + } + + @Override + public ModifyResult editCommitMessage(String message, + CleanupMode mode, char commentChar) { + assertEquals('@', commentChar); + assertEquals("@ This is a combination of 4 commits.\n" + + "@ The first commit's message is:\n" + + "commit2\n" + + "@ This is the 2nd commit message:\n" + + "#commit3\n" + + "@ The 3rd commit message will be skipped:\n" + + "@ @commit4\n" + + "@ This is the 4th commit message:\n" + + ";commit5", message); + return new ModifyResult() { + + @Override + public String getMessage() { + return message; + } + + @Override + public CleanupMode getCleanupMode() { + return mode; + } + + @Override + public boolean shouldAddChangeId() { + return false; + } + }; + } + }).call(); + assertEquals(Status.OK, result.getStatus()); + Iterator<RevCommit> logIterator = git.log().all().call().iterator(); + String actualCommitMsg = logIterator.next().getFullMessage(); + assertEquals("commit2\n#commit3\n;commit5", actualCommitMsg); + } + private File getTodoFile() { File todoFile = new File(db.getDirectory(), GIT_REBASE_TODO); return todoFile; diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RevertCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RevertCommandTest.java index cfa8486ac5..1c7b8d13a8 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RevertCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RevertCommandTest.java @@ -9,6 +9,7 @@ */ package org.eclipse.jgit.api; +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_ABBREV_STRING_LENGTH; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -166,7 +167,9 @@ public class RevertCommandTest extends RepositoryTestCase { checkFile(new File(db.getWorkTree(), "a"), "first\n" + "<<<<<<< master\n" + "second\n" + "third\n" + "=======\n" - + ">>>>>>> " + secondCommit.getId().abbreviate(7).name() + + ">>>>>>> " + + secondCommit.getId() + .abbreviate(OBJECT_ID_ABBREV_STRING_LENGTH).name() + " add second\n"); Iterator<RevCommit> history = git.log().call().iterator(); RevCommit revertCommit = history.next(); @@ -232,7 +235,7 @@ public class RevertCommandTest extends RepositoryTestCase { assertTrue(new File(db.getDirectory(), Constants.MERGE_MSG).exists()); assertEquals("Revert \"" + sideCommit.getShortMessage() + "\"\n\nThis reverts commit " + sideCommit.getId().getName() - + ".\n\nConflicts:\n\ta\n", + + ".\n\n# Conflicts:\n#\ta\n", db.readMergeCommitMsg()); assertTrue(new File(db.getDirectory(), Constants.REVERT_HEAD) .exists()); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/SecurityManagerMissingPermissionsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/SecurityManagerMissingPermissionsTest.java index a07f37009e..d0fbdbd090 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/SecurityManagerMissingPermissionsTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/SecurityManagerMissingPermissionsTest.java @@ -13,17 +13,15 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; -import java.io.StringWriter; +import java.io.PrintStream; import java.nio.file.Files; import java.nio.file.Path; import java.security.Policy; import java.util.Collections; -import org.apache.log4j.Logger; -import org.apache.log4j.PatternLayout; -import org.apache.log4j.WriterAppender; import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.util.FileUtils; import org.junit.After; @@ -38,25 +36,21 @@ public class SecurityManagerMissingPermissionsTest extends RepositoryTestCase { /** * Collects all logging sent to the logging system. */ - private final StringWriter errorOutputWriter = new StringWriter(); - - /** - * Appender to intercept all logging sent to the logging system. - */ - private WriterAppender appender; + private final ByteArrayOutputStream errorOutput = new ByteArrayOutputStream(); private SecurityManager originalSecurityManager; + private PrintStream defaultErrorOutput; + @Override @Before public void setUp() throws Exception { originalSecurityManager = System.getSecurityManager(); - appender = new WriterAppender( - new PatternLayout(PatternLayout.TTCC_CONVERSION_PATTERN), - errorOutputWriter); - - Logger.getRootLogger().addAppender(appender); + // slf4j-simple logs to System.err, redirect it to enable asserting + // logged errors + defaultErrorOutput = System.err; + System.setErr(new PrintStream(errorOutput)); refreshPolicyAllPermission(Policy.getPolicy()); System.setSecurityManager(new SecurityManager()); @@ -85,14 +79,14 @@ public class SecurityManagerMissingPermissionsTest extends RepositoryTestCase { addRepoToClose(git.getRepository()); - assertEquals("", errorOutputWriter.toString()); + assertEquals("", errorOutput.toString()); } @Override @After public void tearDown() throws Exception { System.setSecurityManager(originalSecurityManager); - Logger.getRootLogger().removeAppender(appender); + System.setErr(defaultErrorOutput); super.tearDown(); } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StatusCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StatusCommandTest.java index 5311edb0eb..19281f6c99 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StatusCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StatusCommandTest.java @@ -21,8 +21,10 @@ import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.NoFilepatternException; import org.eclipse.jgit.errors.NoWorkTreeException; import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.Sets; import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.storage.file.FileRepositoryBuilder; import org.eclipse.jgit.util.FS; import org.junit.Test; @@ -181,4 +183,31 @@ public class StatusCommandTest extends RepositoryTestCase { } } + @Test + public void testNestedCommittedGitRepoAndPathFilter() throws Exception { + commitFile("file.txt", "file", "master"); + try (Repository inner = new FileRepositoryBuilder() + .setWorkTree(new File(db.getWorkTree(), "subgit")).build()) { + inner.create(); + writeTrashFile("subgit/sub.txt", "sub"); + try (Git outerGit = new Git(db); Git innerGit = new Git(inner)) { + innerGit.add().addFilepattern("sub.txt").call(); + innerGit.commit().setMessage("Inner commit").call(); + outerGit.add().addFilepattern("subgit").call(); + outerGit.commit().setMessage("Outer commit").call(); + assertTrue(innerGit.status().call().isClean()); + assertTrue(outerGit.status().call().isClean()); + writeTrashFile("subgit/sub.txt", "sub2"); + assertFalse(innerGit.status().call().isClean()); + assertFalse(outerGit.status().call().isClean()); + assertTrue( + outerGit.status().addPath("file.txt").call().isClean()); + assertTrue(outerGit.status().addPath("doesntexist").call() + .isClean()); + assertFalse( + outerGit.status().addPath("subgit").call().isClean()); + } + } + } + } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/AbstractRenameDetectionTestCase.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/AbstractRenameDetectionTestCase.java new file mode 100644 index 0000000000..a8967f27ec --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/AbstractRenameDetectionTestCase.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2022, Google Inc. and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.diff; + +import static org.junit.Assert.assertEquals; + +import org.eclipse.jgit.diff.DiffEntry.ChangeType; +import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.AbbreviatedObjectId; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; +import org.junit.Before; + +public abstract class AbstractRenameDetectionTestCase + extends RepositoryTestCase { + + protected static final String PATH_A = "src/A"; + + protected static final String PATH_B = "src/B"; + + protected static final String PATH_H = "src/H"; + + protected static final String PATH_Q = "src/Q"; + + protected TestRepository<Repository> testDb; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + testDb = new TestRepository<>(db); + } + + protected ObjectId blob(String content) throws Exception { + return testDb.blob(content).copy(); + } + + protected static void assertRename(DiffEntry o, DiffEntry n, int score, + DiffEntry rename) { + assertEquals(ChangeType.RENAME, rename.getChangeType()); + + assertEquals(o.getOldPath(), rename.getOldPath()); + assertEquals(n.getNewPath(), rename.getNewPath()); + + assertEquals(o.getOldMode(), rename.getOldMode()); + assertEquals(n.getNewMode(), rename.getNewMode()); + + assertEquals(o.getOldId(), rename.getOldId()); + assertEquals(n.getNewId(), rename.getNewId()); + + assertEquals(score, rename.getScore()); + } + + protected static void assertCopy(DiffEntry o, DiffEntry n, int score, + DiffEntry copy) { + assertEquals(ChangeType.COPY, copy.getChangeType()); + + assertEquals(o.getOldPath(), copy.getOldPath()); + assertEquals(n.getNewPath(), copy.getNewPath()); + + assertEquals(o.getOldMode(), copy.getOldMode()); + assertEquals(n.getNewMode(), copy.getNewMode()); + + assertEquals(o.getOldId(), copy.getOldId()); + assertEquals(n.getNewId(), copy.getNewId()); + + assertEquals(score, copy.getScore()); + } + + protected static void assertAdd(String newName, ObjectId newId, + FileMode newMode, DiffEntry add) { + assertEquals(DiffEntry.DEV_NULL, add.oldPath); + assertEquals(DiffEntry.A_ZERO, add.oldId); + assertEquals(FileMode.MISSING, add.oldMode); + assertEquals(ChangeType.ADD, add.changeType); + assertEquals(newName, add.newPath); + assertEquals(AbbreviatedObjectId.fromObjectId(newId), add.newId); + assertEquals(newMode, add.newMode); + } + + protected static void assertDelete(String oldName, ObjectId oldId, + FileMode oldMode, DiffEntry delete) { + assertEquals(DiffEntry.DEV_NULL, delete.newPath); + assertEquals(DiffEntry.A_ZERO, delete.newId); + assertEquals(FileMode.MISSING, delete.newMode); + assertEquals(ChangeType.DELETE, delete.changeType); + assertEquals(oldName, delete.oldPath); + assertEquals(AbbreviatedObjectId.fromObjectId(oldId), delete.oldId); + assertEquals(oldMode, delete.oldMode); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/FilteredRenameDetectorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/FilteredRenameDetectorTest.java new file mode 100644 index 0000000000..bfda36db76 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/FilteredRenameDetectorTest.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2022, Simeon Andreev and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.diff; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; + +import java.util.Arrays; +import java.util.List; +import org.eclipse.jgit.internal.diff.FilteredRenameDetector; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.treewalk.filter.PathFilter; +import org.junit.Before; +import org.junit.Test; + +public class FilteredRenameDetectorTest extends AbstractRenameDetectionTestCase { + + private FilteredRenameDetector frd; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + frd = new FilteredRenameDetector(db); + } + + @Test + public void testExactRename() throws Exception { + ObjectId foo = blob("foo"); + ObjectId bar = blob("bar"); + + DiffEntry a = DiffEntry.add(PATH_A, foo); + DiffEntry b = DiffEntry.delete(PATH_Q, foo); + + DiffEntry c = DiffEntry.add(PATH_H, bar); + DiffEntry d = DiffEntry.delete(PATH_B, bar); + + List<DiffEntry> changes = Arrays.asList(a, b, c, d); + PathFilter filter = PathFilter.create(PATH_A); + List<DiffEntry> entries = frd.compute(changes, filter); + assertEquals("Unexpected entries in: " + entries, 1, entries.size()); + assertRename(b, a, 100, entries.get(0)); + } + + @Test + public void testExactRename_multipleFilters() throws Exception { + ObjectId foo = blob("foo"); + ObjectId bar = blob("bar"); + + DiffEntry a = DiffEntry.add(PATH_A, foo); + DiffEntry b = DiffEntry.delete(PATH_Q, foo); + + DiffEntry c = DiffEntry.add(PATH_H, bar); + DiffEntry d = DiffEntry.delete(PATH_B, bar); + + List<DiffEntry> changes = Arrays.asList(a, b, c, d); + List<PathFilter> filters = Arrays.asList(PathFilter.create(PATH_A), + PathFilter.create(PATH_H)); + List<DiffEntry> entries = frd.compute(changes, filters); + assertEquals("Unexpected entries in: " + entries, 2, entries.size()); + assertRename(b, a, 100, entries.get(0)); + assertRename(d, c, 100, entries.get(1)); + } + + @Test + public void testInexactRename() throws Exception { + ObjectId aId = blob("foo\nbar\nbaz\nblarg\n"); + ObjectId bId = blob("foo\nbar\nbaz\nblah\n"); + DiffEntry a = DiffEntry.add(PATH_A, aId); + DiffEntry b = DiffEntry.delete(PATH_Q, bId); + + ObjectId cId = blob("some\nsort\nof\ntext\n"); + ObjectId dId = blob("completely\nunrelated\ntext\n"); + DiffEntry c = DiffEntry.add(PATH_B, cId); + DiffEntry d = DiffEntry.delete(PATH_H, dId); + + List<DiffEntry> changes = Arrays.asList(a, b, c, d); + PathFilter filter = PathFilter.create(PATH_A); + List<DiffEntry> entries = frd.compute(changes, filter); + assertEquals("Unexpected entries: " + entries, 1, entries.size()); + assertRename(b, a, 66, entries.get(0)); + } + + @Test + public void testInexactRename_multipleFilters() throws Exception { + ObjectId aId = blob("foo\nbar\nbaz\nblarg\n"); + ObjectId bId = blob("foo\nbar\nbaz\nblah\n"); + DiffEntry a = DiffEntry.add(PATH_A, aId); + DiffEntry b = DiffEntry.delete(PATH_Q, bId); + + ObjectId cId = blob("some\nsort\nof\ntext\n"); + ObjectId dId = blob("completely\nunrelated\ntext\n"); + DiffEntry c = DiffEntry.add(PATH_B, cId); + DiffEntry d = DiffEntry.delete(PATH_H, dId); + + List<DiffEntry> changes = Arrays.asList(a, b, c, d); + List<PathFilter> filters = Arrays.asList(PathFilter.create(PATH_A), + PathFilter.create(PATH_H)); + List<DiffEntry> entries = frd.compute(changes, filters); + assertEquals("Unexpected entries: " + entries, 2, entries.size()); + assertRename(b, a, 66, entries.get(0)); + assertSame(d, entries.get(1)); + } + + @Test + public void testNoRenames() throws Exception { + ObjectId aId = blob(""); + ObjectId bId = blob("blah1"); + ObjectId cId = blob(""); + ObjectId dId = blob("blah2"); + + DiffEntry a = DiffEntry.add(PATH_A, aId); + DiffEntry b = DiffEntry.delete(PATH_Q, bId); + + DiffEntry c = DiffEntry.add(PATH_H, cId); + DiffEntry d = DiffEntry.delete(PATH_B, dId); + + List<DiffEntry> changes = Arrays.asList(a, b, c, d); + PathFilter filter = PathFilter.create(PATH_A); + List<DiffEntry> entries = frd.compute(changes, filter); + assertEquals("Unexpected entries in: " + entries, 1, entries.size()); + assertSame(a, entries.get(0)); + } + + @Test + public void testNoRenames_multipleFilters() throws Exception { + ObjectId aId = blob(""); + ObjectId bId = blob("blah1"); + ObjectId cId = blob(""); + ObjectId dId = blob("blah2"); + + DiffEntry a = DiffEntry.add(PATH_A, aId); + DiffEntry b = DiffEntry.delete(PATH_Q, bId); + + DiffEntry c = DiffEntry.add(PATH_H, cId); + DiffEntry d = DiffEntry.delete(PATH_B, dId); + + List<DiffEntry> changes = Arrays.asList(a, b, c, d); + List<PathFilter> filters = Arrays.asList(PathFilter.create(PATH_A), + PathFilter.create(PATH_H)); + List<DiffEntry> entries = frd.compute(changes, filters); + assertEquals("Unexpected entries in: " + entries, 2, entries.size()); + assertSame(a, entries.get(0)); + assertSame(c, entries.get(1)); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/RenameDetectorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/RenameDetectorTest.java index 5edb60ce37..ad560e3b8a 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/RenameDetectorTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/RenameDetectorTest.java @@ -18,31 +18,20 @@ import static org.junit.Assert.fail; import java.util.Arrays; import java.util.List; -import org.eclipse.jgit.diff.DiffEntry.ChangeType; -import org.eclipse.jgit.junit.RepositoryTestCase; -import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.lib.AbbreviatedObjectId; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.lib.Repository; import org.junit.Before; import org.junit.Test; -public class RenameDetectorTest extends RepositoryTestCase { - private static final String PATH_A = "src/A"; - private static final String PATH_B = "src/B"; - private static final String PATH_H = "src/H"; - private static final String PATH_Q = "src/Q"; +public class RenameDetectorTest extends AbstractRenameDetectionTestCase { private RenameDetector rd; - private TestRepository<Repository> testDb; - @Override @Before public void setUp() throws Exception { super.setUp(); - testDb = new TestRepository<>(db); rd = new RenameDetector(db); } @@ -675,62 +664,4 @@ public class RenameDetectorTest extends RepositoryTestCase { assertSame(c, entries.get(2)); assertSame(d, entries.get(3)); } - - private ObjectId blob(String content) throws Exception { - return testDb.blob(content).copy(); - } - - private static void assertRename(DiffEntry o, DiffEntry n, int score, - DiffEntry rename) { - assertEquals(ChangeType.RENAME, rename.getChangeType()); - - assertEquals(o.getOldPath(), rename.getOldPath()); - assertEquals(n.getNewPath(), rename.getNewPath()); - - assertEquals(o.getOldMode(), rename.getOldMode()); - assertEquals(n.getNewMode(), rename.getNewMode()); - - assertEquals(o.getOldId(), rename.getOldId()); - assertEquals(n.getNewId(), rename.getNewId()); - - assertEquals(score, rename.getScore()); - } - - private static void assertCopy(DiffEntry o, DiffEntry n, int score, - DiffEntry copy) { - assertEquals(ChangeType.COPY, copy.getChangeType()); - - assertEquals(o.getOldPath(), copy.getOldPath()); - assertEquals(n.getNewPath(), copy.getNewPath()); - - assertEquals(o.getOldMode(), copy.getOldMode()); - assertEquals(n.getNewMode(), copy.getNewMode()); - - assertEquals(o.getOldId(), copy.getOldId()); - assertEquals(n.getNewId(), copy.getNewId()); - - assertEquals(score, copy.getScore()); - } - - private static void assertAdd(String newName, ObjectId newId, - FileMode newMode, DiffEntry add) { - assertEquals(DiffEntry.DEV_NULL, add.oldPath); - assertEquals(DiffEntry.A_ZERO, add.oldId); - assertEquals(FileMode.MISSING, add.oldMode); - assertEquals(ChangeType.ADD, add.changeType); - assertEquals(newName, add.newPath); - assertEquals(AbbreviatedObjectId.fromObjectId(newId), add.newId); - assertEquals(newMode, add.newMode); - } - - private static void assertDelete(String oldName, ObjectId oldId, - FileMode oldMode, DiffEntry delete) { - assertEquals(DiffEntry.DEV_NULL, delete.newPath); - assertEquals(DiffEntry.A_ZERO, delete.newId); - assertEquals(FileMode.MISSING, delete.newMode); - assertEquals(ChangeType.DELETE, delete.changeType); - assertEquals(oldName, delete.oldPath); - assertEquals(AbbreviatedObjectId.fromObjectId(oldId), delete.oldId); - assertEquals(oldMode, delete.oldMode); - } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/BareSuperprojectWriterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/BareSuperprojectWriterTest.java new file mode 100644 index 0000000000..c3b93879b2 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/BareSuperprojectWriterTest.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2021, Google Inc. and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.gitrepo; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.jgit.gitrepo.BareSuperprojectWriter.BareWriterConfig; +import org.eclipse.jgit.gitrepo.RepoCommand.RemoteReader; +import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.junit.Test; + +public class BareSuperprojectWriterTest extends RepositoryTestCase { + + private static final String SHA1_A = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + + @Override + public void setUp() throws Exception { + super.setUp(); + } + + @Test + public void write_setGitModulesContents() throws Exception { + try (Repository bareRepo = createBareRepository()) { + RepoProject repoProject = new RepoProject("subprojectX", "path/to", + "refs/heads/branch-x", "remote", ""); + repoProject.setUrl("http://example.com/a"); + + RemoteReader mockRemoteReader = mock(RemoteReader.class); + when(mockRemoteReader.sha1("http://example.com/a", + "refs/heads/branch-x")) + .thenReturn(ObjectId.fromString(SHA1_A)); + + BareSuperprojectWriter w = new BareSuperprojectWriter(bareRepo, + null, "refs/heads/master", author, mockRemoteReader, + BareWriterConfig.getDefault(), List.of()); + + RevCommit commit = w.write(Arrays.asList(repoProject)); + + String contents = readContents(bareRepo, commit, ".gitmodules"); + List<String> contentLines = Arrays + .asList(contents.split("\n")); + assertThat(contentLines.get(0), + is("[submodule \"subprojectX\"]")); + assertThat(contentLines.subList(1, contentLines.size()), + containsInAnyOrder(is("\tbranch = refs/heads/branch-x"), + is("\tpath = path/to"), + is("\turl = http://example.com/a"))); + } + } + + @Test + public void write_setExtraContents() throws Exception { + try (Repository bareRepo = createBareRepository()) { + RepoProject repoProject = new RepoProject("subprojectX", "path/to", + "refs/heads/branch-x", "remote", ""); + repoProject.setUrl("http://example.com/a"); + + RemoteReader mockRemoteReader = mock(RemoteReader.class); + when(mockRemoteReader.sha1("http://example.com/a", + "refs/heads/branch-x")) + .thenReturn(ObjectId.fromString(SHA1_A)); + + BareSuperprojectWriter w = new BareSuperprojectWriter(bareRepo, + null, "refs/heads/master", author, mockRemoteReader, + BareWriterConfig.getDefault(), + List.of(new BareSuperprojectWriter.ExtraContent("x", + "extra-content"))); + + RevCommit commit = w.write(Arrays.asList(repoProject)); + + String contents = readContents(bareRepo, commit, "x"); + assertThat(contents, is("extra-content")); + } + } + + private String readContents(Repository repo, RevCommit commit, + String path) throws Exception { + String idStr = commit.getId().name() + ":" + path; + ObjectId modId = repo.resolve(idStr); + try (ObjectReader reader = repo.newObjectReader()) { + return new String( + reader.open(modId).getCachedBytes(Integer.MAX_VALUE), + StandardCharsets.UTF_8); + + } + } +} 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 509adc2cb2..3e6d13a67e 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 @@ -15,6 +15,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; import java.io.BufferedReader; import java.io.ByteArrayInputStream; @@ -546,24 +547,29 @@ public class RepoCommandTest extends RepositoryTestCase { // The original file should exist File hello = new File(localDb.getWorkTree(), "foo/hello.txt"); assertTrue("The original file should exist", hello.exists()); - assertFalse("The original file should not be executable", - hello.canExecute()); + if (FS.DETECTED.supportsExecute()) { + assertFalse("The original file should not be executable", + FS.DETECTED.canExecute(hello)); + } assertContents(hello.toPath(), "master world"); // The dest file should also exist hello = new File(localDb.getWorkTree(), "Hello"); assertTrue("The destination file should exist", hello.exists()); - assertFalse("The destination file should not be executable", - hello.canExecute()); + if (FS.DETECTED.supportsExecute()) { + assertFalse("The destination file should not be executable", + FS.DETECTED.canExecute(hello)); + } assertContents(hello.toPath(), "master world"); } @Test public void testRepoManifestCopyFile_executable() throws Exception { + assumeTrue(FS.DETECTED.supportsExecute()); try (Git git = new Git(defaultDb)) { git.checkout().setName("master").call(); File f = JGitTestUtil.writeTrashFile(defaultDb, "hello.sh", "content of the executable file"); - f.setExecutable(true); + FS.DETECTED.setExecute(f, true); git.add().addFilepattern("hello.sh").call(); git.commit().setMessage("Add binary file").call(); } @@ -588,7 +594,8 @@ public class RepoCommandTest extends RepositoryTestCase { // The original file should exist and be an executable File hello = new File(localDb.getWorkTree(), "foo/hello.sh"); assertTrue("The original file should exist", hello.exists()); - assertTrue("The original file must be executable", hello.canExecute()); + assertTrue("The original file must be executable", + FS.DETECTED.canExecute(hello)); try (BufferedReader reader = Files.newBufferedReader(hello.toPath(), UTF_8)) { String content = reader.readLine(); @@ -600,7 +607,7 @@ public class RepoCommandTest extends RepositoryTestCase { hello = new File(localDb.getWorkTree(), "copy-hello.sh"); assertTrue("The destination file should exist", hello.exists()); assertTrue("The destination file must be executable", - hello.canExecute()); + FS.DETECTED.canExecute(hello)); try (BufferedReader reader = Files.newBufferedReader(hello.toPath(), UTF_8)) { String content = reader.readLine(); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalDiffToolTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalDiffToolTest.java index b141a86f76..f69a1794ef 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalDiffToolTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalDiffToolTest.java @@ -10,42 +10,156 @@ package org.eclipse.jgit.internal.diffmergetool; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DIFFTOOL_SECTION; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DIFF_SECTION; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CMD; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_GUITOOL; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PATH; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PROMPT; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TRUST_EXIT_CODE; 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.assertTrue; +import static org.junit.Assert.fail; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; +import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.internal.BooleanTriState; import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.util.FS.ExecutionResult; import org.junit.Test; /** * Testing external diff tools. */ -public class ExternalDiffToolTest extends ExternalToolTest { +public class ExternalDiffToolTest extends ExternalToolTestCase { + + @Test(expected = ToolException.class) + public void testUserToolWithError() throws Exception { + String toolName = "customTool"; + + int errorReturnCode = 1; + String command = "exit " + errorReturnCode; + + FileBasedConfig config = db.getConfig(); + config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD, + command); + + invokeCompare(toolName); + + fail("Expected exception to be thrown due to external tool exiting with error code: " + + errorReturnCode); + } + + @Test(expected = ToolException.class) + public void testUserToolWithCommandNotFoundError() throws Exception { + String toolName = "customTool"; + + int errorReturnCode = 127; // command not found + String command = "exit " + errorReturnCode; + + FileBasedConfig config = db.getConfig(); + config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD, + command); + + invokeCompare(toolName); + fail("Expected exception to be thrown due to external tool exiting with error code: " + + errorReturnCode); + } + + @Test + public void testUserDefinedTool() throws Exception { + String command = getEchoCommand(); + + FileBasedConfig config = db.getConfig(); + String customToolName = "customTool"; + config.setString(CONFIG_DIFFTOOL_SECTION, customToolName, + CONFIG_KEY_CMD, command); + + DiffTools manager = new DiffTools(db); + + Map<String, ExternalDiffTool> tools = manager.getUserDefinedTools(); + ExternalDiffTool externalTool = tools.get(customToolName); + boolean trustExitCode = true; + manager.compare(local, remote, externalTool, trustExitCode); + + assertEchoCommandHasCorrectOutput(); + } + + @Test + public void testUserDefinedToolWithPrompt() throws Exception { + String command = getEchoCommand(); + + FileBasedConfig config = db.getConfig(); + String customToolName = "customTool"; + config.setString(CONFIG_DIFFTOOL_SECTION, customToolName, + CONFIG_KEY_CMD, command); + + DiffTools manager = new DiffTools(db); + + PromptHandler promptHandler = PromptHandler.acceptPrompt(); + MissingToolHandler noToolHandler = new MissingToolHandler(); + + manager.compare(local, remote, Optional.of(customToolName), + BooleanTriState.TRUE, false, BooleanTriState.TRUE, + promptHandler, noToolHandler); + + assertEchoCommandHasCorrectOutput(); + + List<String> actualToolPrompts = promptHandler.toolPrompts; + List<String> expectedToolPrompts = Arrays.asList("customTool"); + assertEquals("Expected a user prompt for custom tool call", + expectedToolPrompts, actualToolPrompts); + + assertEquals("Expected to no informing about missing tools", + Collections.EMPTY_LIST, noToolHandler.missingTools); + } @Test - public void testToolNames() { + public void testUserDefinedToolWithCancelledPrompt() throws Exception { + String command = getEchoCommand(); + + FileBasedConfig config = db.getConfig(); + String customToolName = "customTool"; + config.setString(CONFIG_DIFFTOOL_SECTION, customToolName, + CONFIG_KEY_CMD, command); + DiffTools manager = new DiffTools(db); - Set<String> actualToolNames = manager.getToolNames(); - Set<String> expectedToolNames = Collections.emptySet(); - assertEquals("Incorrect set of external diff tool names", - expectedToolNames, actualToolNames); + + PromptHandler promptHandler = PromptHandler.cancelPrompt(); + MissingToolHandler noToolHandler = new MissingToolHandler(); + + Optional<ExecutionResult> result = manager.compare(local, remote, + Optional.of(customToolName), BooleanTriState.TRUE, false, + BooleanTriState.TRUE, promptHandler, noToolHandler); + assertFalse("Expected no result if user cancels the operation", + result.isPresent()); } @Test public void testAllTools() { + FileBasedConfig config = db.getConfig(); + String customToolName = "customTool"; + config.setString(CONFIG_DIFFTOOL_SECTION, customToolName, + CONFIG_KEY_CMD, "echo"); + DiffTools manager = new DiffTools(db); - Set<String> actualToolNames = manager.getAvailableTools().keySet(); + Set<String> actualToolNames = manager.getAllToolNames(); Set<String> expectedToolNames = new LinkedHashSet<>(); + expectedToolNames.add(customToolName); CommandLineDiffTool[] defaultTools = CommandLineDiffTool.values(); for (CommandLineDiffTool defaultTool : defaultTools) { String toolName = defaultTool.name(); @@ -86,11 +200,11 @@ public class ExternalDiffToolTest extends ExternalToolTest { config.setString(CONFIG_DIFFTOOL_SECTION, customToolname, CONFIG_KEY_PATH, "/usr/bin/echo"); config.setString(CONFIG_DIFFTOOL_SECTION, customToolname, - CONFIG_KEY_PROMPT, "--no-prompt"); + CONFIG_KEY_PROMPT, String.valueOf(false)); config.setString(CONFIG_DIFFTOOL_SECTION, customToolname, - CONFIG_KEY_GUITOOL, "--no-gui"); + CONFIG_KEY_GUITOOL, String.valueOf(false)); config.setString(CONFIG_DIFFTOOL_SECTION, customToolname, - CONFIG_KEY_TRUST_EXIT_CODE, "--no-trust-exit-code"); + CONFIG_KEY_TRUST_EXIT_CODE, String.valueOf(false)); DiffTools manager = new DiffTools(db); Set<String> actualToolNames = manager.getUserDefinedTools().keySet(); Set<String> expectedToolNames = new LinkedHashSet<>(); @@ -100,59 +214,240 @@ public class ExternalDiffToolTest extends ExternalToolTest { } @Test - public void testNotAvailableTools() { - DiffTools manager = new DiffTools(db); - Set<String> actualToolNames = manager.getNotAvailableTools().keySet(); - Set<String> expectedToolNames = Collections.emptySet(); - assertEquals("Incorrect set of not available external diff tools", - expectedToolNames, actualToolNames); - } + public void testCompare() throws ToolException { + String toolName = "customTool"; - @Test - public void testCompare() { - DiffTools manager = new DiffTools(db); + FileBasedConfig config = db.getConfig(); + // the default diff tool is configured without a subsection + String subsection = null; + config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_TOOL, + toolName); - String newPath = ""; - String oldPath = ""; - String newId = ""; - String oldId = ""; - String toolName = ""; - BooleanTriState prompt = BooleanTriState.UNSET; - BooleanTriState gui = BooleanTriState.UNSET; - BooleanTriState trustExitCode = BooleanTriState.UNSET; + String command = getEchoCommand(); + config.setString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_CMD, + command); + Optional<ExecutionResult> result = invokeCompare(toolName); + assertTrue("Expected external diff tool result to be available", + result.isPresent()); int expectedCompareResult = 0; - int compareResult = manager.compare(newPath, oldPath, newId, oldId, - toolName, prompt, gui, trustExitCode); assertEquals("Incorrect compare result for external diff tool", - expectedCompareResult, compareResult); + expectedCompareResult, result.get().getRc()); } @Test public void testDefaultTool() throws Exception { + String toolName = "customTool"; + String guiToolName = "customGuiTool"; + FileBasedConfig config = db.getConfig(); // the default diff tool is configured without a subsection String subsection = null; - config.setString("diff", subsection, "tool", "customTool"); + config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_TOOL, + toolName); DiffTools manager = new DiffTools(db); - BooleanTriState gui = BooleanTriState.UNSET; + boolean gui = false; String defaultToolName = manager.getDefaultToolName(gui); assertEquals( "Expected configured difftool to be the default external diff tool", - "my_default_toolname", defaultToolName); + toolName, defaultToolName); - gui = BooleanTriState.TRUE; + gui = true; String defaultGuiToolName = manager.getDefaultToolName(gui); assertEquals( - "Expected configured difftool to be the default external diff tool", - "my_gui_tool", defaultGuiToolName); + "Expected default gui difftool to be the default tool if no gui tool is set", + toolName, defaultGuiToolName); - config.setString("diff", subsection, "guitool", "customGuiTool"); + config.setString(CONFIG_DIFF_SECTION, subsection, CONFIG_KEY_GUITOOL, + guiToolName); manager = new DiffTools(db); defaultGuiToolName = manager.getDefaultToolName(gui); assertEquals( "Expected configured difftool to be the default external diff guitool", - "my_gui_tool", defaultGuiToolName); + guiToolName, defaultGuiToolName); + } + + @Test + public void testOverridePreDefinedToolPath() { + String newToolPath = "/tmp/path/"; + + CommandLineDiffTool[] defaultTools = CommandLineDiffTool.values(); + assertTrue("Expected to find pre-defined external diff tools", + defaultTools.length > 0); + + CommandLineDiffTool overridenTool = defaultTools[0]; + String overridenToolName = overridenTool.name(); + String overridenToolPath = newToolPath + overridenToolName; + FileBasedConfig config = db.getConfig(); + config.setString(CONFIG_DIFFTOOL_SECTION, overridenToolName, + CONFIG_KEY_PATH, overridenToolPath); + + DiffTools manager = new DiffTools(db); + Map<String, ExternalDiffTool> availableTools = manager + .getPredefinedTools(true); + ExternalDiffTool externalDiffTool = availableTools + .get(overridenToolName); + String actualDiffToolPath = externalDiffTool.getPath(); + assertEquals( + "Expected pre-defined external diff tool to have overriden path", + overridenToolPath, actualDiffToolPath); + String expectedDiffToolCommand = overridenToolPath + " " + + overridenTool.getParameters(); + String actualDiffToolCommand = externalDiffTool.getCommand(); + assertEquals( + "Expected pre-defined external diff tool to have overriden command", + expectedDiffToolCommand, actualDiffToolCommand); + } + + @Test(expected = ToolException.class) + public void testUndefinedTool() throws Exception { + String toolName = "undefined"; + invokeCompare(toolName); + fail("Expected exception to be thrown due to not defined external diff tool"); + } + + @Test + public void testDefaultToolExecutionWithPrompt() throws Exception { + FileBasedConfig config = db.getConfig(); + // the default diff tool is configured without a subsection + String subsection = null; + config.setString("diff", subsection, "tool", "customTool"); + + String command = getEchoCommand(); + + config.setString("difftool", "customTool", "cmd", command); + + DiffTools manager = new DiffTools(db); + + PromptHandler promptHandler = PromptHandler.acceptPrompt(); + MissingToolHandler noToolHandler = new MissingToolHandler(); + + manager.compare(local, remote, Optional.empty(), BooleanTriState.TRUE, + false, BooleanTriState.TRUE, promptHandler, noToolHandler); + + assertEchoCommandHasCorrectOutput(); + } + + @Test + public void testNoDefaultToolName() { + DiffTools manager = new DiffTools(db); + boolean gui = false; + String defaultToolName = manager.getDefaultToolName(gui); + assertNull("Expected no default tool when none is configured", + defaultToolName); + + gui = true; + defaultToolName = manager.getDefaultToolName(gui); + assertNull("Expected no default tool when none is configured", + defaultToolName); + } + + @Test + public void testExternalToolInGitAttributes() throws Exception { + String content = "attributes:\n*.txt difftool=customTool"; + File gitattributes = writeTrashFile(".gitattributes", content); + gitattributes.deleteOnExit(); + try (TestRepository<Repository> testRepository = new TestRepository<>( + db)) { + FileBasedConfig config = db.getConfig(); + config.setString("difftool", "customTool", "cmd", "echo"); + testRepository.git().add().addFilepattern(localFile.getName()) + .call(); + + testRepository.git().add().addFilepattern(".gitattributes").call(); + + testRepository.branch("master").commit().message("first commit") + .create(); + + DiffTools manager = new DiffTools(db); + Optional<String> tool = manager + .getExternalToolFromAttributes(localFile.getName()); + assertTrue("Failed to find user defined tool", tool.isPresent()); + assertEquals("Failed to find user defined tool", "customTool", + tool.get()); + } finally { + Files.delete(gitattributes.toPath()); + } + } + + @Test + public void testNotExternalToolInGitAttributes() throws Exception { + String content = ""; + File gitattributes = writeTrashFile(".gitattributes", content); + gitattributes.deleteOnExit(); + try (TestRepository<Repository> testRepository = new TestRepository<>( + db)) { + FileBasedConfig config = db.getConfig(); + config.setString("difftool", "customTool", "cmd", "echo"); + testRepository.git().add().addFilepattern(localFile.getName()) + .call(); + + testRepository.git().add().addFilepattern(".gitattributes").call(); + + testRepository.branch("master").commit().message("first commit") + .create(); + + DiffTools manager = new DiffTools(db); + Optional<String> tool = manager + .getExternalToolFromAttributes(localFile.getName()); + assertFalse( + "Expected no external tool if no default tool is specified in .gitattributes", + tool.isPresent()); + } finally { + Files.delete(gitattributes.toPath()); + } + } + + @Test(expected = ToolException.class) + public void testNullTool() throws Exception { + DiffTools manager = new DiffTools(db); + + boolean trustExitCode = true; + ExternalDiffTool tool = null; + manager.compare(local, remote, tool, trustExitCode); + } + + @Test(expected = ToolException.class) + public void testNullToolWithPrompt() throws Exception { + DiffTools manager = new DiffTools(db); + + PromptHandler promptHandler = PromptHandler.cancelPrompt(); + MissingToolHandler noToolHandler = new MissingToolHandler(); + + Optional<String> tool = null; + manager.compare(local, remote, tool, BooleanTriState.TRUE, false, + BooleanTriState.TRUE, promptHandler, noToolHandler); + } + + private Optional<ExecutionResult> invokeCompare(String toolName) + throws ToolException { + DiffTools manager = new DiffTools(db); + + BooleanTriState prompt = BooleanTriState.UNSET; + boolean gui = false; + BooleanTriState trustExitCode = BooleanTriState.TRUE; + PromptHandler promptHandler = PromptHandler.acceptPrompt(); + MissingToolHandler noToolHandler = new MissingToolHandler(); + + Optional<ExecutionResult> result = manager.compare(local, remote, + Optional.of(toolName), prompt, gui, trustExitCode, + promptHandler, noToolHandler); + return result; + } + + private String getEchoCommand() { + return "(echo \"$LOCAL\" \"$REMOTE\") > " + + commandResult.getAbsolutePath(); + } + + private void assertEchoCommandHasCorrectOutput() throws IOException { + List<String> actualLines = Files.readAllLines(commandResult.toPath()); + String actualContent = String.join(System.lineSeparator(), actualLines); + actualLines = Arrays.asList(actualContent.split(" ")); + List<String> expectedLines = Arrays.asList(localFile.getAbsolutePath(), + remoteFile.getAbsolutePath()); + assertEquals("Dummy test tool called with unexpected arguments", + expectedLines, actualLines); } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java new file mode 100644 index 0000000000..94b67b374b --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java @@ -0,0 +1,433 @@ +/* + * Copyright (C) 2020-2022, Simeon Andreev <simeon.danailov.andreev@gmail.com> and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.internal.diffmergetool; + +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CMD; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_GUITOOL; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PATH; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PROMPT; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TRUST_EXIT_CODE; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGETOOL_SECTION; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGE_SECTION; +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.assertTrue; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; + +import java.io.IOException; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import org.eclipse.jgit.lib.internal.BooleanTriState; +import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.util.FS.ExecutionResult; +import org.junit.Test; + +/** + * Testing external merge tools. + */ +public class ExternalMergeToolTest extends ExternalToolTestCase { + + @Test(expected = ToolException.class) + public void testUserToolWithError() throws Exception { + String toolName = "customTool"; + + int errorReturnCode = 1; + String command = "exit " + errorReturnCode; + + FileBasedConfig config = db.getConfig(); + config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD, + command); + config.setString(CONFIG_MERGETOOL_SECTION, toolName, + CONFIG_KEY_TRUST_EXIT_CODE, String.valueOf(Boolean.TRUE)); + + invokeMerge(toolName); + + fail("Expected exception to be thrown due to external tool exiting with error code: " + + errorReturnCode); + } + + @Test(expected = ToolException.class) + public void testUserToolWithCommandNotFoundError() throws Exception { + String toolName = "customTool"; + + int errorReturnCode = 127; // command not found + String command = "exit " + errorReturnCode; + + FileBasedConfig config = db.getConfig(); + config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD, + command); + + invokeMerge(toolName); + + fail("Expected exception to be thrown due to external tool exiting with error code: " + + errorReturnCode); + } + + @Test + public void testKdiff3() throws Exception { + assumePosixPlatform(); + + CommandLineMergeTool autoMergingTool = CommandLineMergeTool.kdiff3; + assumeMergeToolIsAvailable(autoMergingTool); + + CommandLineMergeTool tool = autoMergingTool; + PreDefinedMergeTool externalTool = new PreDefinedMergeTool(tool.name(), + tool.getPath(), tool.getParameters(true), + tool.getParameters(false), + tool.isExitCodeTrustable() ? BooleanTriState.TRUE + : BooleanTriState.FALSE); + + MergeTools manager = new MergeTools(db); + ExecutionResult result = manager.merge(local, remote, merged, null, + null, externalTool); + assertEquals("Expected merge tool to succeed", 0, result.getRc()); + + List<String> actualLines = Files.readAllLines(mergedFile.toPath()); + String actualMergeResult = String.join(System.lineSeparator(), + actualLines); + String expectedMergeResult = DEFAULT_CONTENT; + assertEquals( + "Failed to merge equal local and remote versions with pre-defined tool: " + + tool.getPath(), + expectedMergeResult, actualMergeResult); + } + + @Test + public void testUserDefinedTool() throws Exception { + String customToolName = "customTool"; + String command = getEchoCommand(); + + FileBasedConfig config = db.getConfig(); + config.setString(CONFIG_MERGETOOL_SECTION, customToolName, + CONFIG_KEY_CMD, command); + + MergeTools manager = new MergeTools(db); + Map<String, ExternalMergeTool> tools = manager.getUserDefinedTools(); + ExternalMergeTool externalTool = tools.get(customToolName); + manager.merge(local, remote, merged, base, null, externalTool); + + assertEchoCommandHasCorrectOutput(); + } + + @Test + public void testUserDefinedToolWithPrompt() throws Exception { + String customToolName = "customTool"; + String command = getEchoCommand(); + + FileBasedConfig config = db.getConfig(); + config.setString(CONFIG_MERGETOOL_SECTION, customToolName, + CONFIG_KEY_CMD, command); + + MergeTools manager = new MergeTools(db); + + PromptHandler promptHandler = PromptHandler.acceptPrompt(); + MissingToolHandler noToolHandler = new MissingToolHandler(); + + manager.merge(local, remote, merged, base, null, + Optional.of(customToolName), BooleanTriState.TRUE, false, + promptHandler, noToolHandler); + + assertEchoCommandHasCorrectOutput(); + + List<String> actualToolPrompts = promptHandler.toolPrompts; + List<String> expectedToolPrompts = Arrays.asList("customTool"); + assertEquals("Expected a user prompt for custom tool call", + expectedToolPrompts, actualToolPrompts); + + assertEquals("Expected to no informing about missing tools", + Collections.EMPTY_LIST, noToolHandler.missingTools); + } + + @Test + public void testUserDefinedToolWithCancelledPrompt() throws Exception { + MergeTools manager = new MergeTools(db); + + PromptHandler promptHandler = PromptHandler.cancelPrompt(); + MissingToolHandler noToolHandler = new MissingToolHandler(); + + Optional<ExecutionResult> result = manager.merge(local, remote, merged, + base, null, Optional.empty(), BooleanTriState.TRUE, false, + promptHandler, noToolHandler); + assertFalse("Expected no result if user cancels the operation", + result.isPresent()); + } + + @Test + public void testAllTools() { + FileBasedConfig config = db.getConfig(); + String customToolName = "customTool"; + config.setString(CONFIG_MERGETOOL_SECTION, customToolName, + CONFIG_KEY_CMD, "echo"); + + MergeTools manager = new MergeTools(db); + Set<String> actualToolNames = manager.getAllToolNames(); + Set<String> expectedToolNames = new LinkedHashSet<>(); + expectedToolNames.add(customToolName); + CommandLineMergeTool[] defaultTools = CommandLineMergeTool.values(); + for (CommandLineMergeTool defaultTool : defaultTools) { + String toolName = defaultTool.name(); + expectedToolNames.add(toolName); + } + assertEquals("Incorrect set of external merge tools", expectedToolNames, + actualToolNames); + } + + @Test + public void testOverridePredefinedToolPath() { + String toolName = CommandLineMergeTool.guiffy.name(); + String customToolPath = "/usr/bin/echo"; + + FileBasedConfig config = db.getConfig(); + config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD, + "echo"); + config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_PATH, + customToolPath); + + MergeTools manager = new MergeTools(db); + Map<String, ExternalMergeTool> tools = manager.getUserDefinedTools(); + ExternalMergeTool mergeTool = tools.get(toolName); + assertNotNull("Expected tool \"" + toolName + "\" to be user defined", + mergeTool); + + String toolPath = mergeTool.getPath(); + assertEquals("Expected external merge tool to have an overriden path", + customToolPath, toolPath); + } + + @Test + public void testUserDefinedTools() { + FileBasedConfig config = db.getConfig(); + String customToolname = "customTool"; + config.setString(CONFIG_MERGETOOL_SECTION, customToolname, + CONFIG_KEY_CMD, "echo"); + config.setString(CONFIG_MERGETOOL_SECTION, customToolname, + CONFIG_KEY_PATH, "/usr/bin/echo"); + config.setString(CONFIG_MERGETOOL_SECTION, customToolname, + CONFIG_KEY_PROMPT, String.valueOf(false)); + config.setString(CONFIG_MERGETOOL_SECTION, customToolname, + CONFIG_KEY_GUITOOL, String.valueOf(false)); + config.setString(CONFIG_MERGETOOL_SECTION, customToolname, + CONFIG_KEY_TRUST_EXIT_CODE, String.valueOf(false)); + MergeTools manager = new MergeTools(db); + Set<String> actualToolNames = manager.getUserDefinedTools().keySet(); + Set<String> expectedToolNames = new LinkedHashSet<>(); + expectedToolNames.add(customToolname); + assertEquals("Incorrect set of external merge tools", expectedToolNames, + actualToolNames); + } + + @Test + public void testCompare() throws ToolException { + String toolName = "customTool"; + + FileBasedConfig config = db.getConfig(); + // the default merge tool is configured without a subsection + String subsection = null; + config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_TOOL, + toolName); + + String command = getEchoCommand(); + + config.setString(CONFIG_MERGETOOL_SECTION, toolName, CONFIG_KEY_CMD, + command); + + Optional<ExecutionResult> result = invokeMerge(toolName); + assertTrue("Expected external merge tool result to be available", + result.isPresent()); + int expectedCompareResult = 0; + assertEquals("Incorrect compare result for external merge tool", + expectedCompareResult, result.get().getRc()); + } + + @Test + public void testDefaultTool() throws Exception { + String toolName = "customTool"; + String guiToolName = "customGuiTool"; + + FileBasedConfig config = db.getConfig(); + // the default merge tool is configured without a subsection + String subsection = null; + config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_TOOL, + toolName); + + MergeTools manager = new MergeTools(db); + boolean gui = false; + String defaultToolName = manager.getDefaultToolName(gui); + assertEquals( + "Expected configured mergetool to be the default external merge tool", + toolName, defaultToolName); + + gui = true; + String defaultGuiToolName = manager.getDefaultToolName(gui); + assertNull("Expected default mergetool to not be set", + defaultGuiToolName); + + config.setString(CONFIG_MERGE_SECTION, subsection, CONFIG_KEY_GUITOOL, + guiToolName); + manager = new MergeTools(db); + defaultGuiToolName = manager.getDefaultToolName(gui); + assertEquals( + "Expected configured mergetool to be the default external merge guitool", + guiToolName, defaultGuiToolName); + } + + @Test + public void testOverridePreDefinedToolPath() { + String newToolPath = "/tmp/path/"; + + CommandLineMergeTool[] defaultTools = CommandLineMergeTool.values(); + assertTrue("Expected to find pre-defined external merge tools", + defaultTools.length > 0); + + CommandLineMergeTool overridenTool = defaultTools[0]; + String overridenToolName = overridenTool.name(); + String overridenToolPath = newToolPath + overridenToolName; + FileBasedConfig config = db.getConfig(); + config.setString(CONFIG_MERGETOOL_SECTION, overridenToolName, + CONFIG_KEY_PATH, overridenToolPath); + + MergeTools manager = new MergeTools(db); + Map<String, ExternalMergeTool> availableTools = manager + .getPredefinedTools(true); + ExternalMergeTool externalMergeTool = availableTools + .get(overridenToolName); + String actualMergeToolPath = externalMergeTool.getPath(); + assertEquals( + "Expected pre-defined external merge tool to have overriden path", + overridenToolPath, actualMergeToolPath); + boolean withBase = true; + String expectedMergeToolCommand = overridenToolPath + " " + + overridenTool.getParameters(withBase); + String actualMergeToolCommand = externalMergeTool.getCommand(); + assertEquals( + "Expected pre-defined external merge tool to have overriden command", + expectedMergeToolCommand, actualMergeToolCommand); + } + + @Test(expected = ToolException.class) + public void testUndefinedTool() throws Exception { + String toolName = "undefined"; + invokeMerge(toolName); + fail("Expected exception to be thrown due to not defined external merge tool"); + } + + @Test + public void testDefaultToolExecutionWithPrompt() throws Exception { + FileBasedConfig config = db.getConfig(); + // the default diff tool is configured without a subsection + String subsection = null; + config.setString("merge", subsection, "tool", "customTool"); + + String command = getEchoCommand(); + + config.setString("mergetool", "customTool", "cmd", command); + + MergeTools manager = new MergeTools(db); + + PromptHandler promptHandler = PromptHandler.acceptPrompt(); + MissingToolHandler noToolHandler = new MissingToolHandler(); + + manager.merge(local, remote, merged, base, null, Optional.empty(), + BooleanTriState.TRUE, false, promptHandler, noToolHandler); + + assertEchoCommandHasCorrectOutput(); + } + + @Test + public void testNoDefaultToolName() { + MergeTools manager = new MergeTools(db); + boolean gui = false; + String defaultToolName = manager.getDefaultToolName(gui); + assertNull("Expected no default tool when none is configured", + defaultToolName); + + gui = true; + defaultToolName = manager.getDefaultToolName(gui); + assertNull("Expected no default tool when none is configured", + defaultToolName); + } + + @Test(expected = ToolException.class) + public void testNullTool() throws Exception { + MergeTools manager = new MergeTools(db); + + PromptHandler promptHandler = null; + MissingToolHandler noToolHandler = null; + + Optional<String> tool = null; + + manager.merge(local, remote, merged, base, null, tool, + BooleanTriState.TRUE, false, promptHandler, noToolHandler); + } + + @Test(expected = ToolException.class) + public void testNullToolWithPrompt() throws Exception { + MergeTools manager = new MergeTools(db); + + PromptHandler promptHandler = PromptHandler.cancelPrompt(); + MissingToolHandler noToolHandler = new MissingToolHandler(); + + Optional<String> tool = null; + + manager.merge(local, remote, merged, base, null, tool, + BooleanTriState.TRUE, false, promptHandler, noToolHandler); + } + + private Optional<ExecutionResult> invokeMerge(String toolName) + throws ToolException { + BooleanTriState prompt = BooleanTriState.UNSET; + boolean gui = false; + + MergeTools manager = new MergeTools(db); + + PromptHandler promptHandler = PromptHandler.acceptPrompt(); + MissingToolHandler noToolHandler = new MissingToolHandler(); + + Optional<ExecutionResult> result = manager.merge(local, remote, merged, + base, null, Optional.of(toolName), prompt, gui, promptHandler, + noToolHandler); + return result; + } + + private void assumeMergeToolIsAvailable( + CommandLineMergeTool autoMergingTool) { + boolean isAvailable = ExternalToolUtils.isToolAvailable(db.getFS(), + db.getDirectory(), db.getWorkTree(), autoMergingTool.getPath()); + assumeTrue("Assuming external tool is available: " + + autoMergingTool.name(), isAvailable); + } + + private String getEchoCommand() { + return "(echo $LOCAL $REMOTE $MERGED $BASE) > " + + commandResult.getAbsolutePath(); + } + + private void assertEchoCommandHasCorrectOutput() throws IOException { + List<String> actualLines = Files.readAllLines(commandResult.toPath()); + String actualContent = String.join(System.lineSeparator(), actualLines); + actualLines = Arrays.asList(actualContent.split(" ")); + List<String> expectedLines = Arrays.asList(localFile.getAbsolutePath(), + remoteFile.getAbsolutePath(), mergedFile.getAbsolutePath(), + baseFile.getAbsolutePath()); + assertEquals("Dummy test tool called with unexpected arguments", + expectedLines, actualLines); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTestCase.java index c7c8eca714..7a6ff46578 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTestCase.java @@ -11,6 +11,8 @@ package org.eclipse.jgit.internal.diffmergetool; import java.io.File; import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.util.FS; @@ -22,7 +24,7 @@ import org.junit.Before; /** * Base test case for external merge and diff tool tests. */ -public abstract class ExternalToolTest extends RepositoryTestCase { +public abstract class ExternalToolTestCase extends RepositoryTestCase { protected static final String DEFAULT_CONTENT = "line1"; @@ -36,6 +38,14 @@ public abstract class ExternalToolTest extends RepositoryTestCase { protected File commandResult; + protected FileElement local; + + protected FileElement remote; + + protected FileElement merged; + + protected FileElement base; + @Before @Override public void setUp() throws Exception { @@ -51,6 +61,15 @@ public abstract class ExternalToolTest extends RepositoryTestCase { baseFile.deleteOnExit(); commandResult = writeTrashFile("commandResult.txt", ""); commandResult.deleteOnExit(); + + local = new FileElement(localFile.getAbsolutePath(), + FileElement.Type.LOCAL); + remote = new FileElement(remoteFile.getAbsolutePath(), + FileElement.Type.REMOTE); + merged = new FileElement(mergedFile.getAbsolutePath(), + FileElement.Type.MERGED); + base = new FileElement(baseFile.getAbsolutePath(), + FileElement.Type.BASE); } @After @@ -71,4 +90,39 @@ public abstract class ExternalToolTest extends RepositoryTestCase { "This test can run only in Linux tests", FS.DETECTED instanceof FS_POSIX); } + + protected static class PromptHandler implements PromptContinueHandler { + + private final boolean promptResult; + + final List<String> toolPrompts = new ArrayList<>(); + + private PromptHandler(boolean promptResult) { + this.promptResult = promptResult; + } + + static PromptHandler acceptPrompt() { + return new PromptHandler(true); + } + + static PromptHandler cancelPrompt() { + return new PromptHandler(false); + } + + @Override + public boolean prompt(String toolName) { + toolPrompts.add(toolName); + return promptResult; + } + } + + protected static class MissingToolHandler implements InformNoToolHandler { + + final List<String> missingTools = new ArrayList<>(); + + @Override + public void inform(List<String> toolNames) { + missingTools.addAll(toolNames); + } + } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTest.java index 070d666ee5..ab588cb71e 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTest.java @@ -15,16 +15,19 @@ import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import java.time.Duration; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.LongStream; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheConfig.IndexEventConsumer; import org.eclipse.jgit.internal.storage.pack.PackExt; import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.junit.TestRng; @@ -154,6 +157,119 @@ public class DfsBlockCacheTest { @SuppressWarnings("resource") @Test + public void hasIndexEventConsumerOnlyLoaded() throws Exception { + AtomicInteger loaded = new AtomicInteger(); + IndexEventConsumer indexEventConsumer = new IndexEventConsumer() { + @Override + public void acceptRequestedEvent(int packExtPos, boolean cacheHit, + long loadMicros, long bytes, + Duration lastEvictionDuration) { + assertEquals(PackExt.INDEX.getPosition(), packExtPos); + assertTrue(cacheHit); + assertTrue(lastEvictionDuration.isZero()); + loaded.incrementAndGet(); + } + }; + + DfsBlockCache.reconfigure(new DfsBlockCacheConfig().setBlockSize(512) + .setBlockLimit(512 * 4) + .setIndexEventConsumer(indexEventConsumer)); + cache = DfsBlockCache.getInstance(); + + DfsRepositoryDescription repo = new DfsRepositoryDescription("test"); + InMemoryRepository r1 = new InMemoryRepository(repo); + byte[] content = rng.nextBytes(424242); + ObjectId id; + try (ObjectInserter ins = r1.newObjectInserter()) { + id = ins.insert(OBJ_BLOB, content); + ins.flush(); + } + + try (ObjectReader rdr = r1.newObjectReader()) { + byte[] actual = rdr.open(id, OBJ_BLOB).getBytes(); + assertTrue(Arrays.equals(content, actual)); + } + // All cache entries are hot and cache is at capacity. + assertTrue(LongStream.of(cache.getHitCount()).sum() > 0); + assertEquals(99, cache.getFillPercentage()); + + InMemoryRepository r2 = new InMemoryRepository(repo); + content = rng.nextBytes(424242); + try (ObjectInserter ins = r2.newObjectInserter()) { + ins.insert(OBJ_BLOB, content); + ins.flush(); + } + assertTrue(cache.getEvictions()[PackExt.PACK.getPosition()] > 0); + assertEquals(1, cache.getEvictions()[PackExt.INDEX.getPosition()]); + assertEquals(1, loaded.get()); + } + + @SuppressWarnings("resource") + @Test + public void hasIndexEventConsumerLoadedAndEvicted() throws Exception { + AtomicInteger loaded = new AtomicInteger(); + AtomicInteger evicted = new AtomicInteger(); + IndexEventConsumer indexEventConsumer = new IndexEventConsumer() { + @Override + public void acceptRequestedEvent(int packExtPos, boolean cacheHit, + long loadMicros, long bytes, + Duration lastEvictionDuration) { + assertEquals(PackExt.INDEX.getPosition(), packExtPos); + assertTrue(cacheHit); + assertTrue(lastEvictionDuration.isZero()); + loaded.incrementAndGet(); + } + + @Override + public void acceptEvictedEvent(int packExtPos, long bytes, + int totalCacheHitCount, Duration lastEvictionDuration) { + assertEquals(PackExt.INDEX.getPosition(), packExtPos); + assertTrue(totalCacheHitCount > 0); + assertTrue(lastEvictionDuration.isZero()); + evicted.incrementAndGet(); + } + + @Override + public boolean shouldReportEvictedEvent() { + return true; + } + }; + + DfsBlockCache.reconfigure(new DfsBlockCacheConfig().setBlockSize(512) + .setBlockLimit(512 * 4) + .setIndexEventConsumer(indexEventConsumer)); + cache = DfsBlockCache.getInstance(); + + DfsRepositoryDescription repo = new DfsRepositoryDescription("test"); + InMemoryRepository r1 = new InMemoryRepository(repo); + byte[] content = rng.nextBytes(424242); + ObjectId id; + try (ObjectInserter ins = r1.newObjectInserter()) { + id = ins.insert(OBJ_BLOB, content); + ins.flush(); + } + + try (ObjectReader rdr = r1.newObjectReader()) { + byte[] actual = rdr.open(id, OBJ_BLOB).getBytes(); + assertTrue(Arrays.equals(content, actual)); + } + // All cache entries are hot and cache is at capacity. + assertTrue(LongStream.of(cache.getHitCount()).sum() > 0); + assertEquals(99, cache.getFillPercentage()); + + InMemoryRepository r2 = new InMemoryRepository(repo); + content = rng.nextBytes(424242); + try (ObjectInserter ins = r2.newObjectInserter()) { + ins.insert(OBJ_BLOB, content); + ins.flush(); + } + assertTrue(cache.getEvictions()[PackExt.PACK.getPosition()] > 0); + assertEquals(1, cache.getEvictions()[PackExt.INDEX.getPosition()]); + assertEquals(1, loaded.get()); + assertEquals(1, evicted.get()); + } + + @Test public void noConcurrencySerializedReads_oneRepo() throws Exception { InMemoryRepository r1 = createRepoWithBitmap("test"); // Reset cache with concurrency Level at 1 i.e. no concurrency. @@ -267,7 +383,6 @@ public class DfsBlockCacheTest { assertEquals(2, cache.getMissCount()[0]); } - @SuppressWarnings("resource") @Test public void highConcurrencyParallelReads_oneRepo() throws Exception { InMemoryRepository r1 = createRepoWithBitmap("test"); @@ -290,7 +405,6 @@ public class DfsBlockCacheTest { assertEquals(1, cache.getMissCount()[0]); } - @SuppressWarnings("resource") @Test public void highConcurrencyParallelReads_oneRepoParallelReverseIndex() throws Exception { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BatchRefUpdateTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BatchRefUpdateTest.java index 6357a0b9a8..daf4382719 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BatchRefUpdateTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BatchRefUpdateTest.java @@ -42,6 +42,8 @@ import java.util.List; import java.util.Map; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Predicate; +import java.util.function.Function; +import java.util.stream.Collectors; import org.eclipse.jgit.events.ListenerHandle; import org.eclipse.jgit.events.RefsChangedListener; @@ -127,6 +129,7 @@ public class BatchRefUpdateTest extends LocalDiskRepositoryTestCase { } diskRepo = fileRepo; + addRepoToClose(diskRepo); setLogAllRefUpdates(true); if (!useReftable) { @@ -1190,7 +1193,8 @@ public class BatchRefUpdateTest extends LocalDiskRepositoryTestCase { } Map<String, Ref> refs = diskRepo.getRefDatabase() - .getRefs(RefDatabase.ALL); + .getRefsByPrefix(RefDatabase.ALL).stream() + .collect(Collectors.toMap(Ref::getName, Function.identity())); Ref actualHead = refs.remove(Constants.HEAD); if (actualHead != null) { String actualLeafName = actualHead.getLeaf().getName(); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileReftableStackTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileReftableStackTest.java index 6c74f0079a..6c7992716c 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileReftableStackTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileReftableStackTest.java @@ -14,6 +14,7 @@ import static org.eclipse.jgit.lib.Ref.Storage.PACKED; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeFalse; import java.io.File; import java.io.FileNotFoundException; @@ -32,10 +33,12 @@ import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectIdRef; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.util.FileUtils; +import org.eclipse.jgit.util.SystemReader; import org.junit.After; import org.junit.Before; import org.junit.Test; + public class FileReftableStackTest { private static Ref newRef(String name, ObjectId id) { @@ -115,9 +118,12 @@ public class FileReftableStackTest { testCompaction(1024); } - @SuppressWarnings({ "resource", "unused" }) + @SuppressWarnings("resource") @Test public void missingReftable() throws Exception { + // Can't delete in-use files on Windows. + assumeFalse(SystemReader.getInstance().isWindows()); + try (FileReftableStack stack = new FileReftableStack( new File(reftableDir, "refs"), reftableDir, null, () -> new Config())) { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcTestCase.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcTestCase.java index bfb233f77f..48f6e06385 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcTestCase.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcTestCase.java @@ -42,6 +42,7 @@ public abstract class GcTestCase extends LocalDiskRepositoryTestCase { @Override @After public void tearDown() throws Exception { + tr.close(); super.tearDown(); } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/LockFileTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/LockFileTest.java index 509935dfb9..7eab1dcb09 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/LockFileTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/LockFileTest.java @@ -200,4 +200,16 @@ public class LockFileTest extends RepositoryTestCase { assertFalse(lock.isLocked()); checkFile(f, "contentother"); } + + @Test + public void testUnlockNoop() throws Exception { + File f = writeTrashFile("somefile", "content"); + try { + LockFile lock = new LockFile(f); + lock.unlock(); + lock.unlock(); + } catch (Throwable e) { + fail("unlock should be noop if not locked at all."); + } + } } 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 1ff2264f67..3fe8f52fba 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 @@ -722,6 +722,7 @@ public class PackWriterTest extends SampleDataRepositoryTestCase { */ private FileRepository setUpRepoWithMultiplePackfiles() throws Exception { FileRepository fileRepository = createWorkRepository(); + addRepoToClose(fileRepository); try (Git git = new Git(fileRepository)) { // Creates 2 objects (C1 = commit, T1 = tree) git.commit().setMessage("First commit").call(); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/io/CancellableDigestOutputStreamTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/io/CancellableDigestOutputStreamTest.java new file mode 100644 index 0000000000..09a7c0b28a --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/io/CancellableDigestOutputStreamTest.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2022, Tencent. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.storage.io; + +import static org.eclipse.jgit.internal.storage.io.CancellableDigestOutputStream.BYTES_TO_WRITE_BEFORE_CANCEL_CHECK; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.io.InterruptedIOException; + +import org.eclipse.jgit.lib.ProgressMonitor; +import org.eclipse.jgit.util.io.NullOutputStream; +import org.junit.Test; + +public class CancellableDigestOutputStreamTest { + private static class CancelledTestMonitor implements ProgressMonitor { + + private boolean cancelled = false; + + public void setCancelled(boolean cancelled) { + this.cancelled = cancelled; + } + + @Override + public void start(int totalTasks) { + // not implemented + } + + @Override + public void beginTask(String title, int totalWork) { + // not implemented + } + + @Override + public void update(int completed) { + // not implemented + } + + @Override + public void endTask() { + // not implemented + } + + @Override + public boolean isCancelled() { + return cancelled; + } + } + + @Test + public void testCancelInProcess() throws Exception { + CancelledTestMonitor m = new CancelledTestMonitor(); + try (CancellableDigestOutputStream out = new CancellableDigestOutputStream( + m, NullOutputStream.INSTANCE)) { + byte[] KB = new byte[1024]; + int triggerCancelWriteCnt = BYTES_TO_WRITE_BEFORE_CANCEL_CHECK + / KB.length; + for (int i = 0; i < triggerCancelWriteCnt + 1; i++) { + out.write(KB); + } + assertTrue(out.length() > BYTES_TO_WRITE_BEFORE_CANCEL_CHECK); + m.setCancelled(true); + + for (int i = 0; i < triggerCancelWriteCnt - 1; i++) { + out.write(KB); + } + + long lastLength = out.length(); + assertThrows(InterruptedIOException.class, () -> { + out.write(1); + }); + assertEquals(lastLength, out.length()); + + assertThrows(InterruptedIOException.class, () -> { + out.write(new byte[1]); + }); + assertEquals(lastLength, out.length()); + } + } + + @Test + public void testTriggerCheckAfterSingleBytes() throws Exception { + CancelledTestMonitor m = new CancelledTestMonitor(); + try (CancellableDigestOutputStream out = new CancellableDigestOutputStream( + m, NullOutputStream.INSTANCE)) { + + byte[] bytes = new byte[BYTES_TO_WRITE_BEFORE_CANCEL_CHECK + 1]; + m.setCancelled(true); + + assertThrows(InterruptedIOException.class, () -> { + out.write(bytes); + }); + assertEquals(BYTES_TO_WRITE_BEFORE_CANCEL_CHECK, out.length()); + } + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFileTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFileTest.java index 11741b41aa..d065280778 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFileTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFileTest.java @@ -369,20 +369,21 @@ public class OpenSshConfigFileTest extends RepositoryTestCase { @Test public void testListValueSingle() throws Exception { - config("Host orcz\nUserKnownHostsFile /foo/bar\n"); + config("Host orcz\nUserKnownHostsFile ~/foo/bar\n"); final HostConfig c = lookup("orcz"); assertNotNull(c); - assertEquals("/foo/bar", c.getValue("UserKnownHostsFile")); + assertEquals(new File(home, "foo/bar").getPath(), + c.getValue("UserKnownHostsFile")); } @Test public void testListValueMultiple() throws Exception { // Tilde expansion occurs within the parser - config("Host orcz\nUserKnownHostsFile \"~/foo/ba z\" /foo/bar \n"); + config("Host orcz\nUserKnownHostsFile \"~/foo/ba z\" ~/foo/bar \n"); final HostConfig c = lookup("orcz"); assertNotNull(c); assertArrayEquals(new Object[] { new File(home, "foo/ba z").getPath(), - "/foo/bar" }, + new File(home, "foo/bar").getPath() }, c.getValues("UserKnownHostsFile").toArray()); } @@ -403,22 +404,23 @@ public class OpenSshConfigFileTest extends RepositoryTestCase { @Test public void testIdentityFile() throws Exception { - config("Host orcz\nIdentityFile \"~/foo/ba z\"\nIdentityFile /foo/bar"); + config("Host orcz\nIdentityFile \"~/foo/ba z\"\nIdentityFile ~/foo/bar"); final HostConfig h = lookup("orcz"); assertNotNull(h); // Does tilde replacement assertArrayEquals(new Object[] { new File(home, "foo/ba z").getPath(), - "/foo/bar" }, + new File(home, "foo/bar").getPath() }, h.getValues(SshConstants.IDENTITY_FILE).toArray()); } @Test public void testMultiIdentityFile() throws Exception { - config("IdentityFile \"~/foo/ba z\"\nHost orcz\nIdentityFile /foo/bar\nHOST *\nIdentityFile /foo/baz"); + config("IdentityFile \"~/foo/ba z\"\nHost orcz\nIdentityFile ~/foo/bar\nHOST *\nIdentityFile ~/foo/baz"); final HostConfig h = lookup("orcz"); assertNotNull(h); assertArrayEquals(new Object[] { new File(home, "foo/ba z").getPath(), - "/foo/bar", "/foo/baz" }, + new File(home, "foo/bar").getPath(), + new File(home, "foo/baz").getPath() }, h.getValues(SshConstants.IDENTITY_FILE).toArray()); } @@ -434,23 +436,23 @@ public class OpenSshConfigFileTest extends RepositoryTestCase { @Test public void testPattern() throws Exception { - config("Host repo.or.cz\nIdentityFile ~/foo/bar\nHOST *.or.cz\nIdentityFile /foo/baz"); + config("Host repo.or.cz\nIdentityFile ~/foo/bar\nHOST *.or.cz\nIdentityFile ~/foo/baz"); final HostConfig h = lookup("repo.or.cz"); assertNotNull(h); assertIdentity(new File(home, "foo/bar"), h); assertArrayEquals(new Object[] { new File(home, "foo/bar").getPath(), - "/foo/baz" }, + new File(home, "foo/baz").getPath() }, h.getValues(SshConstants.IDENTITY_FILE).toArray()); } @Test public void testMultiHost() throws Exception { - config("Host orcz *.or.cz\nIdentityFile ~/foo/bar\nHOST *.or.cz\nIdentityFile /foo/baz"); + config("Host orcz *.or.cz\nIdentityFile ~/foo/bar\nHOST *.or.cz\nIdentityFile ~/foo/baz"); final HostConfig h1 = lookup("repo.or.cz"); assertNotNull(h1); assertIdentity(new File(home, "foo/bar"), h1); assertArrayEquals(new Object[] { new File(home, "foo/bar").getPath(), - "/foo/baz" }, + new File(home, "foo/baz").getPath() }, h1.getValues(SshConstants.IDENTITY_FILE).toArray()); final HostConfig h2 = lookup("orcz"); assertNotNull(h2); @@ -547,18 +549,36 @@ public class OpenSshConfigFileTest extends RepositoryTestCase { @Test public void testEnVarSubstitution() throws Exception { - config("Host orcz\nIdentityFile /tmp/${TST_VAR}\n" - + "CertificateFile /tmp/${}/foo\nUser ${TST_VAR}\nIdentityAgent /tmp/${TST_VAR/bar"); + config("Host orcz\nIdentityFile ~/tmp/${TST_VAR}\n" + + "CertificateFile ~/tmp/${}/foo\nUser ${TST_VAR}\nIdentityAgent ~/tmp/${TST_VAR/bar"); HostConfig h = lookup("orcz"); assertNotNull(h); - assertEquals("/tmp/TEST", + File tmp = new File(home, "tmp"); + assertEquals(new File(tmp, "TEST").getPath(), h.getValue(SshConstants.IDENTITY_FILE)); // No variable name - assertEquals("/tmp/${}/foo", h.getValue(SshConstants.CERTIFICATE_FILE)); + assertEquals(new File(new File(tmp, "${}"), "foo").getPath(), + h.getValue(SshConstants.CERTIFICATE_FILE)); // User doesn't get env var substitution: assertUser("${TST_VAR}", h); // Unterminated: - assertEquals("/tmp/${TST_VAR/bar", + assertEquals(new File(new File(tmp, "${TST_VAR"), "bar").getPath(), + h.getValue(SshConstants.IDENTITY_AGENT)); + } + + @Test + public void testIdentityAgentNone() throws Exception { + config("Host orcz\nIdentityAgent none\n"); + HostConfig h = lookup("orcz"); + assertEquals(SshConstants.NONE, + h.getValue(SshConstants.IDENTITY_AGENT)); + } + + @Test + public void testIdentityAgentSshAuthSock() throws Exception { + config("Host orcz\nIdentityAgent SSH_AUTH_SOCK\n"); + HostConfig h = lookup("orcz"); + assertEquals(SshConstants.ENV_SSH_AUTH_SOCKET, h.getValue(SshConstants.IDENTITY_AGENT)); } @@ -607,13 +627,16 @@ public class OpenSshConfigFileTest extends RepositoryTestCase { @Test public void testMultipleMatch() throws Exception { - config("Host foo.bar\nPort 29418\nIdentityFile /foo\n\n" - + "Host *.bar\nPort 22\nIdentityFile /bar\n" - + "Host foo.bar\nPort 47\nIdentityFile /baz\n"); + config("Host foo.bar\nPort 29418\nIdentityFile ~/foo\n\n" + + "Host *.bar\nPort 22\nIdentityFile ~/bar\n" + + "Host foo.bar\nPort 47\nIdentityFile ~/baz\n"); HostConfig h = lookup("foo.bar"); assertNotNull(h); assertPort(29418, h); - assertArrayEquals(new Object[] { "/foo", "/bar", "/baz" }, + assertArrayEquals( + new Object[] { new File(home, "foo").getPath(), + new File(home, "bar").getPath(), + new File(home, "baz").getPath() }, h.getValues(SshConstants.IDENTITY_FILE).toArray()); } @@ -633,4 +656,61 @@ public class OpenSshConfigFileTest extends RepositoryTestCase { assertNotNull(h); assertPort(22, h); } + + @Test + public void testTimeSpec() throws Exception { + assertEquals(-1, OpenSshConfigFile.timeSpec(null)); + assertEquals(-1, OpenSshConfigFile.timeSpec("")); + assertEquals(-1, OpenSshConfigFile.timeSpec(" ")); + assertEquals(-1, OpenSshConfigFile.timeSpec("s")); + assertEquals(-1, OpenSshConfigFile.timeSpec(" s")); + assertEquals(-1, OpenSshConfigFile.timeSpec(" +s")); + assertEquals(-1, OpenSshConfigFile.timeSpec(" -s")); + assertEquals(-1, OpenSshConfigFile.timeSpec("1ms")); + assertEquals(600, OpenSshConfigFile.timeSpec("600")); + assertEquals(600, OpenSshConfigFile.timeSpec("600s")); + assertEquals(600, OpenSshConfigFile.timeSpec(" 600s")); + assertEquals(600, OpenSshConfigFile.timeSpec(" 600s ")); + assertEquals(600, OpenSshConfigFile.timeSpec("\t600s")); + assertEquals(600, OpenSshConfigFile.timeSpec(" \t600 ")); + assertEquals(-1, OpenSshConfigFile.timeSpec(" 600 s ")); + assertEquals(-1, OpenSshConfigFile.timeSpec("600 s")); + assertEquals(600, OpenSshConfigFile.timeSpec("10m")); + assertEquals(5400, OpenSshConfigFile.timeSpec("1h30m")); + assertEquals(5400, OpenSshConfigFile.timeSpec("1h 30m")); + assertEquals(5400, OpenSshConfigFile.timeSpec("1h \t30m")); + assertEquals(5400, OpenSshConfigFile.timeSpec("1h+30m")); + assertEquals(5400, OpenSshConfigFile.timeSpec("1h +30m")); + assertEquals(-1, OpenSshConfigFile.timeSpec("1h + 30m")); + assertEquals(-1, OpenSshConfigFile.timeSpec("1h -30m")); + assertEquals(3630, OpenSshConfigFile.timeSpec("1h30s")); + assertEquals(5400, OpenSshConfigFile.timeSpec("30m 1h")); + assertEquals(3600, OpenSshConfigFile.timeSpec("30m 30m")); + assertEquals(60, OpenSshConfigFile.timeSpec("30 30")); + assertEquals(0, OpenSshConfigFile.timeSpec("0")); + assertEquals(1, OpenSshConfigFile.timeSpec("1")); + assertEquals(1, OpenSshConfigFile.timeSpec("1S")); + assertEquals(1, OpenSshConfigFile.timeSpec("1s")); + assertEquals(60, OpenSshConfigFile.timeSpec("1M")); + assertEquals(60, OpenSshConfigFile.timeSpec("1m")); + assertEquals(3600, OpenSshConfigFile.timeSpec("1H")); + assertEquals(3600, OpenSshConfigFile.timeSpec("1h")); + assertEquals(86400, OpenSshConfigFile.timeSpec("1D")); + assertEquals(86400, OpenSshConfigFile.timeSpec("1d")); + assertEquals(604800, OpenSshConfigFile.timeSpec("1W")); + assertEquals(604800, OpenSshConfigFile.timeSpec("1w")); + assertEquals(172800, OpenSshConfigFile.timeSpec("2d")); + assertEquals(604800, OpenSshConfigFile.timeSpec("1w")); + assertEquals(604800 + 172800 + 3 * 3600 + 30 * 60 + 10, + OpenSshConfigFile.timeSpec("1w2d3h30m10s")); + assertEquals(-1, OpenSshConfigFile.timeSpec("-7")); + assertEquals(-1, OpenSshConfigFile.timeSpec("-9d")); + assertEquals(Integer.MAX_VALUE, OpenSshConfigFile + .timeSpec(Integer.toString(Integer.MAX_VALUE))); + assertEquals(-1, OpenSshConfigFile + .timeSpec(Long.toString(Integer.MAX_VALUE + 1L))); + assertEquals(-1, OpenSshConfigFile + .timeSpec(Integer.toString(Integer.MAX_VALUE / 60 + 1) + 'M')); + assertEquals(-1, OpenSshConfigFile.timeSpec("1000000000000000000000w")); + } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/AbbrevConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/AbbrevConfigTest.java new file mode 100644 index 0000000000..96ace08dd1 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/AbbrevConfigTest.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2022, Matthias Sohn <matthias.sohn@sap.com> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.lib; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import java.io.IOException; + +import org.eclipse.jgit.api.errors.InvalidConfigurationException; +import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.junit.Test; + +public class AbbrevConfigTest extends RepositoryTestCase { + + @Test + public void testDefault() throws Exception { + assertEquals(7, testCoreAbbrev(null)); + } + + @Test + public void testAuto() throws Exception { + assertEquals(7, testCoreAbbrev("auto")); + } + + @Test + public void testNo() throws Exception { + assertEquals(40, testCoreAbbrev("no")); + } + + @Test + public void testValidMin() throws Exception { + assertEquals(4, testCoreAbbrev("4")); + } + + @Test + public void testValid() throws Exception { + assertEquals(22, testCoreAbbrev("22")); + } + + @Test + public void testValidMax() throws Exception { + assertEquals(40, testCoreAbbrev("40")); + } + + @Test + public void testInvalid() { + assertThrows(InvalidConfigurationException.class, + () -> testCoreAbbrev("foo")); + } + + @Test + public void testInvalid2() { + assertThrows(InvalidConfigurationException.class, + () -> testCoreAbbrev("2k")); + } + + @Test + public void testInvalidNegative() { + assertThrows(InvalidConfigurationException.class, + () -> testCoreAbbrev("-1000")); + } + + @Test + public void testInvalidBelowRange() { + assertThrows(InvalidConfigurationException.class, + () -> testCoreAbbrev("3")); + } + + @Test + public void testInvalidBelowRange2() { + assertThrows(InvalidConfigurationException.class, + () -> testCoreAbbrev("-1")); + } + + @Test + public void testInvalidAboveRange() { + assertThrows(InvalidConfigurationException.class, + () -> testCoreAbbrev("41")); + } + + @Test + public void testInvalidAboveRange2() { + assertThrows(InvalidConfigurationException.class, + () -> testCoreAbbrev("100000")); + } + + @Test + public void testToStringNo() + throws InvalidConfigurationException, IOException { + assertEquals("40", setCoreAbbrev("no").toString()); + } + + @Test + public void testToString() + throws InvalidConfigurationException, IOException { + assertEquals("7", setCoreAbbrev("auto").toString()); + } + + @Test + public void testToString12() + throws InvalidConfigurationException, IOException { + assertEquals("12", setCoreAbbrev("12").toString()); + } + + private int testCoreAbbrev(String value) + throws InvalidConfigurationException, IOException { + return setCoreAbbrev(value).get(); + } + + private AbbrevConfig setCoreAbbrev(String value) + throws IOException, InvalidConfigurationException { + FileBasedConfig config = db.getConfig(); + config.setString(ConfigConstants.CONFIG_CORE_SECTION, null, + ConfigConstants.CONFIG_KEY_ABBREV, value); + config.save(); + return AbbrevConfig.parseFromConfig(db); + } + +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitConfigTest.java new file mode 100644 index 0000000000..7066f9d422 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitConfigTest.java @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2022, Thomas Wolf <thomas.wolf@paranor.ch> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.lib; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lib.CommitConfig.CleanupMode; +import org.junit.Test; + +public class CommitConfigTest { + + @Test + public void testDefaults() throws Exception { + CommitConfig cfg = parse(""); + assertEquals("Unexpected clean-up mode", CleanupMode.DEFAULT, + cfg.getCleanupMode()); + } + + @Test + public void testCommitCleanup() throws Exception { + String[] values = { "strip", "whitespace", "verbatim", "scissors", + "default" }; + CleanupMode[] expected = { CleanupMode.STRIP, CleanupMode.WHITESPACE, + CleanupMode.VERBATIM, CleanupMode.SCISSORS, + CleanupMode.DEFAULT }; + for (int i = 0; i < values.length; i++) { + CommitConfig cfg = parse("[commit]\n\tcleanup = " + values[i]); + assertEquals("Unexpected clean-up mode", expected[i], + cfg.getCleanupMode()); + } + } + + @Test + public void testResolve() throws Exception { + String[] values = { "strip", "whitespace", "verbatim", "scissors", + "default" }; + CleanupMode[] expected = { CleanupMode.STRIP, CleanupMode.WHITESPACE, + CleanupMode.VERBATIM, CleanupMode.SCISSORS, + CleanupMode.DEFAULT }; + for (int i = 0; i < values.length; i++) { + CommitConfig cfg = parse("[commit]\n\tcleanup = " + values[i]); + for (CleanupMode mode : CleanupMode.values()) { + for (int j = 0; j < 2; j++) { + CleanupMode resolved = cfg.resolve(mode, j == 0); + if (mode != CleanupMode.DEFAULT) { + assertEquals("Clean-up mode should be unchanged", mode, + resolved); + } else if (i + 1 < values.length) { + assertEquals("Unexpected clean-up mode", expected[i], + resolved); + } else { + assertEquals("Unexpected clean-up mode", + j == 0 ? CleanupMode.STRIP + : CleanupMode.WHITESPACE, + resolved); + } + } + } + } + } + + @Test + public void testCleanDefaultThrows() throws Exception { + assertThrows(IllegalArgumentException.class, () -> CommitConfig + .cleanText("Whatever", CleanupMode.DEFAULT, '#')); + } + + @Test + public void testCleanVerbatim() throws Exception { + String message = "\n \nWhatever \n\n\n# A comment\n\nMore\t \n\n\n"; + assertEquals("Unexpected message change", message, + CommitConfig.cleanText(message, CleanupMode.VERBATIM, '#')); + } + + @Test + public void testCleanWhitespace() throws Exception { + String message = "\n \nWhatever \n\n\n# A comment\n\nMore\t \n\n\n"; + assertEquals("Unexpected message change", + "Whatever\n\n# A comment\n\nMore\n", + CommitConfig.cleanText(message, CleanupMode.WHITESPACE, '#')); + } + + @Test + public void testCleanStrip() throws Exception { + String message = "\n \nWhatever \n\n\n# A comment\n\nMore\t \n\n\n"; + assertEquals("Unexpected message change", "Whatever\n\nMore\n", + CommitConfig.cleanText(message, CleanupMode.STRIP, '#')); + } + + @Test + public void testCleanStripCustomChar() throws Exception { + String message = "\n \nWhatever \n\n\n# Not a comment\n\n <A comment\nMore\t \n\n\n"; + assertEquals("Unexpected message change", + "Whatever\n\n# Not a comment\n\nMore\n", + CommitConfig.cleanText(message, CleanupMode.STRIP, '<')); + } + + @Test + public void testCleanScissors() throws Exception { + String message = "\n \nWhatever \n\n\n# Not a comment\n\n <A comment\nMore\t \n\n\n" + + "# ------------------------ >8 ------------------------\n" + + "More\nMore\n"; + assertEquals("Unexpected message change", + "Whatever\n\n# Not a comment\n\n <A comment\nMore\n", + CommitConfig.cleanText(message, CleanupMode.SCISSORS, '#')); + } + + @Test + public void testCleanScissorsCustomChar() throws Exception { + String message = "\n \nWhatever \n\n\n# Not a comment\n\n <A comment\nMore\t \n\n\n" + + "< ------------------------ >8 ------------------------\n" + + "More\nMore\n"; + assertEquals("Unexpected message change", + "Whatever\n\n# Not a comment\n\n <A comment\nMore\n", + CommitConfig.cleanText(message, CleanupMode.SCISSORS, '<')); + } + + @Test + public void testCleanScissorsAtTop() throws Exception { + String message = "# ------------------------ >8 ------------------------\n" + + "\n \nWhatever \n\n\n# Not a comment\n\n <A comment\nMore\t \n\n\n" + + "More\nMore\n"; + assertEquals("Unexpected message change", "", + CommitConfig.cleanText(message, CleanupMode.SCISSORS, '#')); + } + + @Test + public void testCleanScissorsNoScissor() throws Exception { + String message = "\n \nWhatever \n\n\n# A comment\n\nMore\t \n\n\n"; + assertEquals("Unexpected message change", + "Whatever\n\n# A comment\n\nMore\n", + CommitConfig.cleanText(message, CleanupMode.SCISSORS, '#')); + } + + @Test + public void testCleanScissorsNoScissor2() throws Exception { + String message = "Text\n" + + "## ------------------------ >8 ------------------------\n" + + "More\nMore\n"; + assertEquals("Unexpected message change", message, + CommitConfig.cleanText(message, CleanupMode.SCISSORS, '#')); + } + + @Test + public void testCleanScissorsNoScissor3() throws Exception { + String message = "Text\n" + // Wrong number of dashes + + "# ----------------------- >8 ------------------------\n" + + "More\nMore\n"; + assertEquals("Unexpected message change", message, + CommitConfig.cleanText(message, CleanupMode.SCISSORS, '#')); + } + + @Test + public void testCleanScissorsAtEnd() throws Exception { + String message = "Text\n" + + "# ------------------------ >8 ------------------------\n"; + assertEquals("Unexpected message change", "Text\n", + CommitConfig.cleanText(message, CleanupMode.SCISSORS, '#')); + } + + @Test + public void testCommentCharDefault() throws Exception { + CommitConfig cfg = parse(""); + assertEquals('#', cfg.getCommentChar()); + assertFalse(cfg.isAutoCommentChar()); + } + + @Test + public void testCommentCharAuto() throws Exception { + CommitConfig cfg = parse("[core]\n\tcommentChar = auto\n"); + assertEquals('#', cfg.getCommentChar()); + assertTrue(cfg.isAutoCommentChar()); + } + + @Test + public void testCommentCharEmpty() throws Exception { + CommitConfig cfg = parse("[core]\n\tcommentChar =\n"); + assertEquals('#', cfg.getCommentChar()); + } + + @Test + public void testCommentCharInvalid() throws Exception { + CommitConfig cfg = parse("[core]\n\tcommentChar = \" \"\n"); + assertEquals('#', cfg.getCommentChar()); + } + + @Test + public void testCommentCharNonAscii() throws Exception { + CommitConfig cfg = parse("[core]\n\tcommentChar = ö\n"); + assertEquals('#', cfg.getCommentChar()); + } + + @Test + public void testCommentChar() throws Exception { + CommitConfig cfg = parse("[core]\n\tcommentChar = _\n"); + assertEquals('_', cfg.getCommentChar()); + } + + @Test + public void testDetermineCommentChar() throws Exception { + String text = "A commit message\n\nBody\n"; + assertEquals('#', CommitConfig.determineCommentChar(text)); + } + + @Test + public void testDetermineCommentChar2() throws Exception { + String text = "A commit message\n\nBody\n\n# Conflicts:\n#\tfoo.txt\n"; + char ch = CommitConfig.determineCommentChar(text); + assertNotEquals('#', ch); + assertTrue(ch > ' ' && ch < 127); + } + + @Test + public void testDetermineCommentChar3() throws Exception { + String text = "A commit message\n\n;Body\n\n# Conflicts:\n#\tfoo.txt\n"; + char ch = CommitConfig.determineCommentChar(text); + assertNotEquals('#', ch); + assertNotEquals(';', ch); + assertTrue(ch > ' ' && ch < 127); + } + + @Test + public void testDetermineCommentChar4() throws Exception { + String text = "A commit message\n\nBody\n\n # Conflicts:\n\t #\tfoo.txt\n"; + char ch = CommitConfig.determineCommentChar(text); + assertNotEquals('#', ch); + assertTrue(ch > ' ' && ch < 127); + } + + @Test + public void testDetermineCommentChar5() throws Exception { + String text = "A commit message\n\nBody\n\n#a\n;b\n@c\n!d\n$\n%\n^\n&\n|\n:"; + char ch = CommitConfig.determineCommentChar(text); + assertEquals(0, ch); + } + + private static CommitConfig parse(String content) + throws ConfigInvalidException { + Config c = new Config(); + c.fromText(content); + return c.get(CommitConfig.KEY); + } + +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitTemplateConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitTemplateConfigTest.java new file mode 100644 index 0000000000..42bafb60ca --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitTemplateConfigTest.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2021, 2022 SAP SE and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.lib; + +import static org.junit.Assert.assertEquals; + +import java.io.File; +import java.io.IOException; + +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.junit.JGitTestUtil; +import org.eclipse.jgit.storage.file.FileRepositoryBuilder; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +/* + * This test was moved from ConfigTest to allow skipping it when running the + * test using bazel which doesn't allow tests to create files in the home + * directory + */ +public class CommitTemplateConfigTest { + + @Rule + public TemporaryFolder tmp = new TemporaryFolder(); + + @Test + public void testCommitTemplatePathInHomeDirecory() + throws ConfigInvalidException, IOException { + Config config = new Config(null); + File tempFile = tmp.newFile("testCommitTemplate-"); + File workTree = tmp.newFolder("dummy-worktree"); + Repository repo = FileRepositoryBuilder.create(workTree); + String templateContent = "content of the template"; + JGitTestUtil.write(tempFile, templateContent); + // proper evaluation of the ~/ directory + String homeDir = System.getProperty("user.home"); + File tempFileInHomeDirectory = File.createTempFile("fileInHomeFolder", + ".tmp", new File(homeDir)); + tempFileInHomeDirectory.deleteOnExit(); + JGitTestUtil.write(tempFileInHomeDirectory, templateContent); + String expectedTemplatePath = "~/" + tempFileInHomeDirectory.getName(); + config = ConfigTest + .parse("[commit]\n\ttemplate = " + expectedTemplatePath + "\n"); + String templatePath = config.get(CommitConfig.KEY) + .getCommitTemplatePath(); + assertEquals(expectedTemplatePath, templatePath); + assertEquals(templateContent, + config.get(CommitConfig.KEY).getCommitTemplateContent(repo)); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java index 9ee54d5b60..a85a4f49b6 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java @@ -1177,7 +1177,7 @@ public class ConfigTest { assertEquals(exp, c.getLong("s", null, "a", 0L)); } - private static Config parse(String content) + static Config parse(String content) throws ConfigInvalidException { return parse(content, null); } @@ -1488,7 +1488,8 @@ public class ConfigTest { String expectedTemplatePath = tempFile.getPath(); Config config = parse( - "[commit]\n\ttemplate = " + expectedTemplatePath + "\n"); + "[commit]\n\ttemplate = " + + Config.escapeValue(expectedTemplatePath) + "\n"); String templatePath = config.get(CommitConfig.KEY) .getCommitTemplatePath(); @@ -1537,7 +1538,8 @@ public class ConfigTest { JGitTestUtil.write(tempFile, templateContent); String expectedTemplatePath = tempFile.getPath(); config = parse("[i18n]\n\tcommitEncoding = utf-8\n" - + "[commit]\n\ttemplate = " + expectedTemplatePath + "\n"); + + "[commit]\n\ttemplate = " + + Config.escapeValue(expectedTemplatePath) + "\n"); assertEquals(templateContent, config.get(CommitConfig.KEY).getCommitTemplateContent(repo)); String commitEncoding = config.get(CommitConfig.KEY) @@ -1546,31 +1548,6 @@ public class ConfigTest { "utf-8", commitEncoding); } - @Test - public void testCommitTemplatePathInHomeDirecory() - throws ConfigInvalidException, IOException { - Config config = new Config(null); - File tempFile = tmp.newFile("testCommitTemplate-"); - File workTree = tmp.newFolder("dummy-worktree"); - Repository repo = FileRepositoryBuilder.create(workTree); - String templateContent = "content of the template"; - JGitTestUtil.write(tempFile, templateContent); - // proper evaluation of the ~/ directory - String homeDir = System.getProperty("user.home"); - File tempFileInHomeDirectory = File.createTempFile("fileInHomeFolder", - ".tmp", new File(homeDir)); - tempFileInHomeDirectory.deleteOnExit(); - JGitTestUtil.write(tempFileInHomeDirectory, templateContent); - String expectedTemplatePath = tempFileInHomeDirectory.getPath() - .replace(homeDir, "~"); - config = parse("[commit]\n\ttemplate = " + expectedTemplatePath + "\n"); - String templatePath = config.get(CommitConfig.KEY) - .getCommitTemplatePath(); - assertEquals(expectedTemplatePath, templatePath); - assertEquals(templateContent, - config.get(CommitConfig.KEY).getCommitTemplateContent(repo)); - } - @Test(expected = ConfigInvalidException.class) public void testCommitTemplateWithInvalidEncoding() throws ConfigInvalidException, IOException { @@ -1581,7 +1558,8 @@ public class ConfigTest { String templateContent = "content of the template"; JGitTestUtil.write(tempFile, templateContent); config = parse("[i18n]\n\tcommitEncoding = invalidEcoding\n" - + "[commit]\n\ttemplate = " + tempFile.getPath() + "\n"); + + "[commit]\n\ttemplate = " + + Config.escapeValue(tempFile.getPath()) + "\n"); config.get(CommitConfig.KEY).getCommitTemplateContent(repo); } @@ -1595,7 +1573,7 @@ public class ConfigTest { String templateContent = "content of the template"; JGitTestUtil.write(tempFile, templateContent); // commit message encoding - String expectedTemplatePath = "/nonExistingTemplate"; + String expectedTemplatePath = "~/nonExistingTemplate"; config = parse("[commit]\n\ttemplate = " + expectedTemplatePath + "\n"); String templatePath = config.get(CommitConfig.KEY) .getCommitTemplatePath(); 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 af8a58f6f0..0fafcd6a36 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 @@ -2,7 +2,7 @@ * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com> * Copyright (C) 2008-2011, Shawn O. Pearce <spearce@spearce.org> * Copyright (C) 2008-2011, Robin Rosenberg <robin.rosenberg@dewire.com> - * Copyright (C) 2010, 2020 Christian Halstrick <christian.halstrick@sap.com> and others + * Copyright (C) 2010, 2022 Christian Halstrick <christian.halstrick@sap.com> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -242,6 +242,7 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { ListenerHandle handle = null; try (Git git = new Git(db); TestRepository<Repository> db_t = new TestRepository<>(db)) { + db.incrementOpen(); handle = db.getListenerList() .addWorkingTreeModifiedListener(recorder); BranchBuilder master = db_t.branch("master"); @@ -261,6 +262,7 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { String attributes) throws Exception { try (Git git = new Git(db); TestRepository<Repository> db_t = new TestRepository<>(db)) { + db.incrementOpen(); BranchBuilder master = db_t.branch("master"); master.commit().add("f", inIndex).message("m0").create(); if (!StringUtils.isEmptyOrNull(attributes)) { @@ -313,8 +315,9 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { @Test public void testCheckoutWithLFAuto() throws Exception { - checkoutLineEndings("first line\nsecond line\n", - "first line\nsecond line\n", "f text=auto"); + String expected = String.format("first line%nsecond line%n"); + checkoutLineEndings("first line\nsecond line\n", expected, + "f text=auto"); } @Test @@ -325,9 +328,9 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { @Test public void testCheckoutWithLFAutoEolNative() throws Exception { + String expected = String.format("first line%nsecond line%n"); checkoutLineEndings( - "first line\nsecond line\n", "first line\nsecond line\n" - .replaceAll("\n", System.lineSeparator()), + "first line\nsecond line\n", expected, "f text=auto eol=native"); } @@ -2064,6 +2067,7 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { public void testCheckoutWithEmptyIndexDoesntOverwrite() throws Exception { try (Git git = new Git(db); TestRepository<Repository> db_t = new TestRepository<>(db)) { + db.incrementOpen(); // prepare the commits BranchBuilder master = db_t.branch("master"); RevCommit mergeCommit = master.commit() diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/T0001_PersonIdentTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/PersonIdentTest.java index e9bab7c4ad..97da1757e0 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/T0001_PersonIdentTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/PersonIdentTest.java @@ -13,12 +13,14 @@ package org.eclipse.jgit.lib; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import java.time.Instant; +import java.time.ZoneId; import java.util.Date; import java.util.TimeZone; import org.junit.Test; -public class T0001_PersonIdentTest { +public class PersonIdentTest { @Test public void test001_NewIdent() { @@ -42,6 +44,34 @@ public class T0001_PersonIdentTest { p.toExternalString()); } + @Test + public void testNewIdentInstant() { + PersonIdent p = new PersonIdent("A U Thor", "author@example.com", + Instant.ofEpochMilli(1142878501000L), + ZoneId.of("America/New_York")); + assertEquals("A U Thor", p.getName()); + assertEquals("author@example.com", p.getEmailAddress()); + assertEquals(Instant.ofEpochMilli(1142878501000L), + p.getWhenAsInstant()); + assertEquals("A U Thor <author@example.com> 1142878501 -0500", + p.toExternalString()); + assertEquals(ZoneId.of("GMT-05:00"), p.getZoneId()); + } + + @Test + public void testNewIdentInstant2() { + final PersonIdent p = new PersonIdent("A U Thor", "author@example.com", + Instant.ofEpochMilli(1142878501000L), + ZoneId.of("Asia/Kolkata")); + assertEquals("A U Thor", p.getName()); + assertEquals("author@example.com", p.getEmailAddress()); + assertEquals(Instant.ofEpochMilli(1142878501000L), + p.getWhenAsInstant()); + assertEquals("A U Thor <author@example.com> 1142878501 +0530", + p.toExternalString()); + assertEquals(ZoneId.of("GMT+05:30"), p.getZoneId()); + } + @SuppressWarnings("unused") @Test(expected = IllegalArgumentException.class) public void nullForNameShouldThrowIllegalArgumentException() { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergeMessageFormatterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergeMessageFormatterTest.java index dedb56c7b0..a2576cc677 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergeMessageFormatterTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergeMessageFormatterTest.java @@ -13,6 +13,7 @@ import static org.junit.Assert.assertEquals; import java.io.IOException; import java.util.Arrays; +import java.util.List; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectIdRef; @@ -157,8 +158,8 @@ public class MergeMessageFormatterTest extends SampleDataRepositoryTestCase { public void testFormatWithConflictsNoFooter() { String originalMessage = "Header Line\n\nCommit body\n"; String message = formatter.formatWithConflicts(originalMessage, - Arrays.asList(new String[] { "path1" })); - assertEquals("Header Line\n\nCommit body\n\nConflicts:\n\tpath1\n", + List.of("path1"), '#'); + assertEquals("Header Line\n\nCommit body\n\n# Conflicts:\n#\tpath1\n", message); } @@ -166,8 +167,17 @@ public class MergeMessageFormatterTest extends SampleDataRepositoryTestCase { public void testFormatWithConflictsNoFooterNoLineBreak() { String originalMessage = "Header Line\n\nCommit body"; String message = formatter.formatWithConflicts(originalMessage, - Arrays.asList(new String[] { "path1" })); - assertEquals("Header Line\n\nCommit body\n\nConflicts:\n\tpath1\n", + List.of("path1"), '#'); + assertEquals("Header Line\n\nCommit body\n\n# Conflicts:\n#\tpath1\n", + message); + } + + @Test + public void testFormatWithConflictsCustomCharacter() { + String originalMessage = "Header Line\n\nCommit body"; + String message = formatter.formatWithConflicts(originalMessage, + List.of("path1"), ';'); + assertEquals("Header Line\n\nCommit body\n\n; Conflicts:\n;\tpath1\n", message); } @@ -176,9 +186,9 @@ public class MergeMessageFormatterTest extends SampleDataRepositoryTestCase { String originalMessage = "Header Line\n\nCommit body\n\nChangeId:" + " I123456789123456789123456789123456789\nBug:1234567\n"; String message = formatter.formatWithConflicts(originalMessage, - Arrays.asList(new String[] { "path1" })); + List.of("path1"), '#'); assertEquals( - "Header Line\n\nCommit body\n\nConflicts:\n\tpath1\n\n" + "Header Line\n\nCommit body\n\n# Conflicts:\n#\tpath1\n\n" + "ChangeId: I123456789123456789123456789123456789\nBug:1234567\n", message); } @@ -188,9 +198,9 @@ public class MergeMessageFormatterTest extends SampleDataRepositoryTestCase { String originalMessage = "Header Line\n\nCommit body\nBug:1234567\nMore Body\n\nChangeId:" + " I123456789123456789123456789123456789\nBug:1234567\n"; String message = formatter.formatWithConflicts(originalMessage, - Arrays.asList(new String[] { "path1" })); + List.of("path1"), '#'); assertEquals( - "Header Line\n\nCommit body\nBug:1234567\nMore Body\n\nConflicts:\n\tpath1\n\n" + "Header Line\n\nCommit body\nBug:1234567\nMore Body\n\n# Conflicts:\n#\tpath1\n\n" + "ChangeId: I123456789123456789123456789123456789\nBug:1234567\n", message); } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java index dd8573d2bc..cbacaed728 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java @@ -1810,6 +1810,7 @@ public class MergerTest extends RepositoryTestCase { private String readBlob(ObjectId treeish, String path) throws Exception { try (TestRepository<?> tr = new TestRepository<>(db); RevWalk rw = tr.getRevWalk()) { + db.incrementOpen(); RevTree tree = rw.parseTree(treeish); RevObject obj = tr.get(tree, path); if (obj == null) { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/storage/file/FileBasedConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/storage/file/FileBasedConfigTest.java index 7f0bfefbe7..b964e97752 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/storage/file/FileBasedConfigTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/storage/file/FileBasedConfigTest.java @@ -19,6 +19,7 @@ import static org.junit.Assert.assertFalse; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; @@ -276,7 +277,8 @@ public class FileBasedConfigTest { throws IOException { AtomicBoolean userConfigTimeRead = new AtomicBoolean(false); - Path userConfigFile = createFile(CONTENT1.getBytes(), "home"); + Path userConfigFile = createFile( + CONTENT1.getBytes(StandardCharsets.UTF_8), "home"); mockSystemReader.setUserGitConfig( new FileBasedConfig(userConfigFile.toFile(), FS.DETECTED) { @@ -289,7 +291,8 @@ public class FileBasedConfigTest { } }); - Path file = createFile(CONTENT2.getBytes(), "repo"); + Path file = createFile(CONTENT2.getBytes(StandardCharsets.UTF_8), + "repo"); FileBasedConfig fileBasedConfig = new FileBasedConfig(file.toFile(), FS.DETECTED); fileBasedConfig.save(); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleWalkTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleWalkTest.java index ea994f06aa..f446d07513 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleWalkTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleWalkTest.java @@ -217,7 +217,6 @@ public class SubmoduleWalkTest extends RepositoryTestCase { assertEqualsFile(modulesGitDir, subRepo.getDirectory()); assertEqualsFile(new File(db.getWorkTree(), path), subRepo.getWorkTree()); - subRepo.close(); assertFalse(gen.next()); } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BasePackConnectionTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BasePackConnectionTest.java index 7d438c1dd8..505f334905 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BasePackConnectionTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BasePackConnectionTest.java @@ -15,11 +15,15 @@ import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import java.io.ByteArrayInputStream; +import java.io.EOFException; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; +import org.eclipse.jgit.errors.NoRemoteRepositoryException; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectIdRef; @@ -30,6 +34,16 @@ import org.junit.Test; public class BasePackConnectionTest { @Test + public void testReadAdvertisedRefsShouldThrowExceptionWithOriginalCause() { + try (FailingBasePackConnection basePackConnection = + new FailingBasePackConnection()) { + Exception result = assertThrows(NoRemoteRepositoryException.class, + basePackConnection::readAdvertisedRefs); + assertEquals(EOFException.class, result.getCause().getClass()); + } + } + + @Test public void testUpdateWithSymRefsAdds() { final Ref mainRef = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, "refs/heads/main", ObjectId.fromString( @@ -244,4 +258,12 @@ public class BasePackConnectionTest { assertEquals(oidName, headRef.getObjectId().name()); assertEquals(oidName, mainRef.getObjectId().name()); } -}
\ No newline at end of file + + private static class FailingBasePackConnection extends BasePackConnection { + FailingBasePackConnection() { + super(new TransportLocal(new URIish(), + new java.io.File(""))); + pckIn = new PacketLineIn(new ByteArrayInputStream(new byte[0])); + } + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BasePackPushConnectionTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BasePackPushConnectionTest.java new file mode 100644 index 0000000000..cf8c5ffe79 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BasePackPushConnectionTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2022 Darius Jokilehto <darius.jokilehto+os@gmail.com> + * Copyright (c) 2022 Antonio Barone <syntonyze@gmail.com> + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.transport; + +import org.eclipse.jgit.errors.NoRemoteRepositoryException; +import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.internal.JGitText; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.EOFException; +import java.io.IOException; +import java.util.Arrays; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.endsWith; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +public class BasePackPushConnectionTest { + @Test + public void testNoRemoteRepository() { + NoRemoteRepositoryException openFetchException = + new NoRemoteRepositoryException( new URIish(), "not found"); + IOException ioException = new IOException("not read"); + + try (FailingBasePackPushConnection fbppc = + new FailingBasePackPushConnection(openFetchException)) { + TransportException result = fbppc.noRepository(ioException); + + assertEquals(openFetchException, result); + assertThat(Arrays.asList(result.getSuppressed()), + hasItem(ioException)); + } + } + + @Test + public void testPushNotPermitted() { + URIish uri = new URIish(); + TransportException openFetchException = new TransportException(uri, + "a transport exception"); + IOException ioException = new IOException("not read"); + + try (FailingBasePackPushConnection fbppc = + new FailingBasePackPushConnection(openFetchException)) { + TransportException result = fbppc.noRepository(ioException); + + assertEquals(TransportException.class, result.getClass()); + assertThat(result.getMessage(), + endsWith(JGitText.get().pushNotPermitted)); + assertEquals(openFetchException, result.getCause()); + assertThat(Arrays.asList(result.getSuppressed()), + hasItem(ioException)); + } + } + + @Test + public void testReadAdvertisedRefPropagatesCauseAndSuppressedExceptions() { + IOException ioException = new IOException("not read"); + try (FailingBasePackPushConnection basePackConnection = + new FailingBasePackPushConnection( + new NoRemoteRepositoryException( + new URIish(), "not found", ioException))) { + Exception result = assertThrows(NoRemoteRepositoryException.class, + basePackConnection::readAdvertisedRefs); + assertEquals(ioException, result.getCause()); + assertThat(Arrays.asList(result.getSuppressed()), + hasItem(instanceOf(EOFException.class))); + } + } + + private static class FailingBasePackPushConnection + extends BasePackPushConnection { + FailingBasePackPushConnection(TransportException openFetchException) { + super(new TransportLocal(new URIish(), + new java.io.File("")) { + @Override public FetchConnection openFetch() + throws TransportException { + throw openFetchException; + } + }); + pckIn = new PacketLineIn(new ByteArrayInputStream(new byte[0])); + } + } +} 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 054eb9c5ad..bb62a0d892 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 @@ -80,6 +80,7 @@ public class BundleWriterTest extends SampleDataRepositoryTestCase { // Then we clone a new repo from that bundle and do a simple test. This // makes sure we could read the bundle we created. Repository newRepo = createBareRepository(); + addRepoToClose(newRepo); FetchResult fetchResult = fetchFromBundle(newRepo, bundle); Ref advertisedRef = fetchResult .getAdvertisedRef("refs/heads/firstcommit"); @@ -116,6 +117,7 @@ public class BundleWriterTest extends SampleDataRepositoryTestCase { // makes sure // we could read the bundle we created. Repository newRepo = createBareRepository(); + addRepoToClose(newRepo); FetchResult fetchResult = fetchFromBundle(newRepo, bundle); Ref advertisedRef = fetchResult.getAdvertisedRef("refs/heads/aa"); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PackParserTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PackParserTest.java index 60b8098b31..93bedb3c97 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PackParserTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PackParserTest.java @@ -110,6 +110,7 @@ public class PackParserTest extends RepositoryTestCase { public void testTinyThinPack() throws Exception { RevBlob a; try (TestRepository d = new TestRepository<Repository>(db)) { + db.incrementOpen(); a = d.blob("a"); } @@ -132,6 +133,7 @@ public class PackParserTest extends RepositoryTestCase { public void testPackWithDuplicateBlob() throws Exception { final byte[] data = Constants.encode("0123456789abcdefg"); try (TestRepository<Repository> d = new TestRepository<>(db)) { + db.incrementOpen(); assertTrue(db.getObjectDatabase().has(d.blob(data))); } @@ -151,6 +153,7 @@ public class PackParserTest extends RepositoryTestCase { public void testPackWithTrailingGarbage() throws Exception { RevBlob a; try (TestRepository d = new TestRepository<Repository>(db)) { + db.incrementOpen(); a = d.blob("a"); } @@ -180,6 +183,7 @@ public class PackParserTest extends RepositoryTestCase { public void testMaxObjectSizeFullBlob() throws Exception { final byte[] data = Constants.encode("0123456789"); try (TestRepository d = new TestRepository<Repository>(db)) { + db.incrementOpen(); d.blob(data); } @@ -213,6 +217,7 @@ public class PackParserTest extends RepositoryTestCase { public void testMaxObjectSizeDeltaBlock() throws Exception { RevBlob a; try (TestRepository d = new TestRepository<Repository>(db)) { + db.incrementOpen(); a = d.blob("a"); } @@ -246,6 +251,7 @@ public class PackParserTest extends RepositoryTestCase { public void testMaxObjectSizeDeltaResultSize() throws Exception { RevBlob a; try (TestRepository d = new TestRepository<Repository>(db)) { + db.incrementOpen(); a = d.blob("0123456789"); } @@ -278,6 +284,7 @@ public class PackParserTest extends RepositoryTestCase { public void testNonMarkingInputStream() throws Exception { RevBlob a; try (TestRepository d = new TestRepository<Repository>(db)) { + db.incrementOpen(); a = d.blob("a"); } @@ -318,6 +325,7 @@ public class PackParserTest extends RepositoryTestCase { public void testDataAfterPackFooterSingleRead() throws Exception { RevBlob a; try (TestRepository d = new TestRepository<Repository>(db)) { + db.incrementOpen(); a = d.blob("a"); } @@ -379,6 +387,7 @@ public class PackParserTest extends RepositoryTestCase { final byte[] data = Constants.encode("a"); RevBlob b; try (TestRepository d = new TestRepository<Repository>(db)) { + db.incrementOpen(); b = d.blob(data); } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushConfigTest.java index 6109d6cb4d..cbc1d546ac 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushConfigTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushConfigTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017, David Pursehouse <david.pursehouse@gmail.com> and others + * Copyright (C) 2017, 2022 David Pursehouse <david.pursehouse@gmail.com> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -14,10 +14,13 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.transport.PushConfig.PushDefault; import org.eclipse.jgit.transport.PushConfig.PushRecurseSubmodulesMode; import org.junit.Test; public class PushConfigTest { + @Test public void pushRecurseSubmoduleMatch() throws Exception { assertTrue(PushRecurseSubmodulesMode.CHECK.matchConfigValue("check")); @@ -52,4 +55,59 @@ public class PushConfigTest { assertEquals("check", PushRecurseSubmodulesMode.CHECK.toConfigValue()); assertEquals("false", PushRecurseSubmodulesMode.NO.toConfigValue()); } + + @Test + public void pushDefaultMatch() throws Exception { + assertTrue(PushDefault.NOTHING.matchConfigValue("nothing")); + assertTrue(PushDefault.NOTHING.matchConfigValue("NOTHING")); + assertTrue(PushDefault.CURRENT.matchConfigValue("current")); + assertTrue(PushDefault.CURRENT.matchConfigValue("CURRENT")); + assertTrue(PushDefault.UPSTREAM.matchConfigValue("upstream")); + assertTrue(PushDefault.UPSTREAM.matchConfigValue("UPSTREAM")); + assertTrue(PushDefault.UPSTREAM.matchConfigValue("tracking")); + assertTrue(PushDefault.UPSTREAM.matchConfigValue("TRACKING")); + assertTrue(PushDefault.SIMPLE.matchConfigValue("simple")); + assertTrue(PushDefault.SIMPLE.matchConfigValue("SIMPLE")); + assertTrue(PushDefault.MATCHING.matchConfigValue("matching")); + assertTrue(PushDefault.MATCHING.matchConfigValue("MATCHING")); + } + + @Test + public void pushDefaultNoMatch() throws Exception { + assertFalse(PushDefault.NOTHING.matchConfigValue("n")); + assertFalse(PushDefault.CURRENT.matchConfigValue("")); + assertFalse(PushDefault.UPSTREAM.matchConfigValue("track")); + } + + @Test + public void pushDefaultToConfigValue() throws Exception { + assertEquals("nothing", PushDefault.NOTHING.toConfigValue()); + assertEquals("current", PushDefault.CURRENT.toConfigValue()); + assertEquals("upstream", PushDefault.UPSTREAM.toConfigValue()); + assertEquals("simple", PushDefault.SIMPLE.toConfigValue()); + assertEquals("matching", PushDefault.MATCHING.toConfigValue()); + } + + @Test + public void testEmptyConfig() throws Exception { + PushConfig cfg = parse(""); + assertEquals(PushRecurseSubmodulesMode.NO, cfg.getRecurseSubmodules()); + assertEquals(PushDefault.SIMPLE, cfg.getPushDefault()); + } + + @Test + public void testConfig() throws Exception { + PushConfig cfg = parse( + "[push]\n\tdefault = tracking\n\trecurseSubmodules = on-demand\n"); + assertEquals(PushRecurseSubmodulesMode.ON_DEMAND, + cfg.getRecurseSubmodules()); + assertEquals(PushDefault.UPSTREAM, cfg.getPushDefault()); + } + + private static PushConfig parse(String content) throws Exception { + Config c = new Config(); + c.fromText(content); + return c.get(PushConfig::new); + } + } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushProcessTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushProcessTest.java index 6928859622..2e8b30f151 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushProcessTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushProcessTest.java @@ -14,14 +14,19 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.HashSet; import java.util.Map; +import org.eclipse.jgit.api.errors.AbortedByHookException; import org.eclipse.jgit.errors.NotSupportedException; import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.hooks.PrePushHook; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectIdRef; import org.eclipse.jgit.lib.ProgressMonitor; @@ -31,6 +36,7 @@ import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.TextProgressMonitor; import org.eclipse.jgit.test.resources.SampleDataRepositoryTestCase; import org.eclipse.jgit.transport.RemoteRefUpdate.Status; +import org.eclipse.jgit.util.io.NullOutputStream; import org.junit.Before; import org.junit.Test; @@ -220,7 +226,17 @@ public class PushProcessTest extends SampleDataRepositoryTestCase { .fromString("0000000000000000000000000000000000000001")); final Ref ref = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, "refs/heads/master", ObjectId.fromString("ac7e7e44c1885efb472ad54a78327d66bfc4ecef")); - testOneUpdateStatus(rru, ref, Status.REJECTED_REMOTE_CHANGED, null); + try (ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + PrintStream out = new PrintStream(bytes, true, + StandardCharsets.UTF_8); + PrintStream err = new PrintStream(NullOutputStream.INSTANCE)) { + MockPrePushHook hook = new MockPrePushHook(db, out, err); + testOneUpdateStatus(rru, ref, Status.REJECTED_REMOTE_CHANGED, null, + hook); + out.flush(); + String result = new String(bytes.toString(StandardCharsets.UTF_8)); + assertEquals("", result); + } } /** @@ -256,10 +272,22 @@ public class PushProcessTest extends SampleDataRepositoryTestCase { refUpdates.add(rruOk); refUpdates.add(rruReject); advertisedRefs.add(refToChange); - executePush(); - assertEquals(Status.OK, rruOk.getStatus()); - assertTrue(rruOk.isFastForward()); - assertEquals(Status.NON_EXISTING, rruReject.getStatus()); + try (ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + PrintStream out = new PrintStream(bytes, true, + StandardCharsets.UTF_8); + PrintStream err = new PrintStream(NullOutputStream.INSTANCE)) { + MockPrePushHook hook = new MockPrePushHook(db, out, err); + executePush(hook); + assertEquals(Status.OK, rruOk.getStatus()); + assertTrue(rruOk.isFastForward()); + assertEquals(Status.NON_EXISTING, rruReject.getStatus()); + out.flush(); + String result = new String(bytes.toString(StandardCharsets.UTF_8)); + assertEquals( + "null 0000000000000000000000000000000000000000 " + + "refs/heads/master 2c349335b7f797072cf729c4f3bb0914ecb6dec9\n", + result); + } } /** @@ -346,10 +374,18 @@ public class PushProcessTest extends SampleDataRepositoryTestCase { final Ref advertisedRef, final Status expectedStatus, Boolean fastForward) throws NotSupportedException, TransportException { + return testOneUpdateStatus(rru, advertisedRef, expectedStatus, + fastForward, null); + } + + private PushResult testOneUpdateStatus(final RemoteRefUpdate rru, + final Ref advertisedRef, final Status expectedStatus, + Boolean fastForward, PrePushHook hook) + throws NotSupportedException, TransportException { refUpdates.add(rru); if (advertisedRef != null) advertisedRefs.add(advertisedRef); - final PushResult result = executePush(); + final PushResult result = executePush(hook); assertEquals(expectedStatus, rru.getStatus()); if (fastForward != null) assertEquals(fastForward, Boolean.valueOf(rru.isFastForward())); @@ -358,7 +394,12 @@ public class PushProcessTest extends SampleDataRepositoryTestCase { private PushResult executePush() throws NotSupportedException, TransportException { - process = new PushProcess(transport, refUpdates); + return executePush(null); + } + + private PushResult executePush(PrePushHook hook) + throws NotSupportedException, TransportException { + process = new PushProcess(transport, refUpdates, hook); return process.execute(new TextProgressMonitor()); } @@ -416,4 +457,20 @@ public class PushProcessTest extends SampleDataRepositoryTestCase { } } } + + private static class MockPrePushHook extends PrePushHook { + + private final PrintStream output; + + public MockPrePushHook(Repository repo, PrintStream out, + PrintStream err) { + super(repo, out, err); + output = out; + } + + @Override + protected void doRun() throws AbortedByHookException, IOException { + output.print(getStdinArgs()); + } + } } 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 d1e5446827..a91bc95c8d 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 @@ -73,11 +73,14 @@ public class ReceivePackAdvertiseRefsHookTest extends LocalDiskRepositoryTestCas super.setUp(); src = createBareRepository(); + addRepoToClose(src); dst = createBareRepository(); + addRepoToClose(dst); // Fill dst with a some common history. // try (TestRepository<Repository> d = new TestRepository<>(dst)) { + dst.incrementOpen(); a = d.blob("a"); A = d.commit(d.tree(d.file("a", a))); B = d.commit().parent(A).create(); @@ -106,9 +109,6 @@ public class ReceivePackAdvertiseRefsHookTest extends LocalDiskRepositoryTestCas dst.getDirectory()) { @Override ReceivePack createReceivePack(Repository db) { - db.close(); - dst.incrementOpen(); - final ReceivePack rp = super.createReceivePack(dst); rp.setAdvertiseRefsHook(new HidePrivateHook()); return rp; @@ -136,8 +136,6 @@ public class ReceivePackAdvertiseRefsHookTest extends LocalDiskRepositoryTestCas dst.getDirectory()) { @Override ReceivePack createReceivePack(Repository db) { - dst.incrementOpen(); - ReceivePack rp = super.createReceivePack(dst); rp.setAdvertiseRefsHook(new AdvertiseRefsHook() { @Override @@ -173,9 +171,6 @@ public class ReceivePackAdvertiseRefsHookTest extends LocalDiskRepositoryTestCas return new TransportLocal(src, uriOf(dst), dst.getDirectory()) { @Override ReceivePack createReceivePack(Repository db) { - db.close(); - dst.incrementOpen(); - final ReceivePack rp = super.createReceivePack(dst); rp.setCheckReceivedObjects(true); rp.setCheckReferencedObjectsAreReachable(true); @@ -211,6 +206,7 @@ public class ReceivePackAdvertiseRefsHookTest extends LocalDiskRepositoryTestCas // Now use b but in a different commit than what is hidden. // try (TestRepository<Repository> s = new TestRepository<>(src)) { + src.incrementOpen(); RevCommit N = s.commit().parent(B).add("q", b).create(); s.update(R_MASTER, N); @@ -228,7 +224,6 @@ public class ReceivePackAdvertiseRefsHookTest extends LocalDiskRepositoryTestCas try (TransportLocal t = newTransportLocalWithStrictValidation()) { t.setPushThin(true); r = t.push(PM, Collections.singleton(u)); - dst.close(); } assertNotNull("have result", r); @@ -290,6 +285,7 @@ public class ReceivePackAdvertiseRefsHookTest extends LocalDiskRepositoryTestCas public void testUsingHiddenDeltaBaseFails() throws Exception { byte[] delta = { 0x1, 0x1, 0x1, 'c' }; try (TestRepository<Repository> s = new TestRepository<>(src)) { + src.incrementOpen(); RevCommit N = s.commit().parent(B) .add("q", s.blob(BinaryDelta.apply( @@ -348,6 +344,7 @@ public class ReceivePackAdvertiseRefsHookTest extends LocalDiskRepositoryTestCas // Try to use the 'b' blob that is hidden. // try (TestRepository<Repository> s = new TestRepository<>(src)) { + src.incrementOpen(); RevCommit N = s.commit().parent(B).add("q", s.blob("b")).create(); // But don't include it in the pack. @@ -401,6 +398,7 @@ public class ReceivePackAdvertiseRefsHookTest extends LocalDiskRepositoryTestCas // Try to use the 'n' blob that is not on the server. // try (TestRepository<Repository> s = new TestRepository<>(src)) { + src.incrementOpen(); RevBlob n = s.blob("n"); RevCommit N = s.commit().parent(B).add("q", n).create(); @@ -491,6 +489,7 @@ public class ReceivePackAdvertiseRefsHookTest extends LocalDiskRepositoryTestCas .toString(); try (TestRepository<Repository> s = new TestRepository<>(src)) { + src.incrementOpen(); RevBlob blob = s.blob(fakeGitmodules); RevCommit N = s.commit().parent(B).add(".gitmodules", blob) .create(); @@ -517,6 +516,7 @@ public class ReceivePackAdvertiseRefsHookTest extends LocalDiskRepositoryTestCas @Test public void testUsingUnknownTreeFails() throws Exception { try (TestRepository<Repository> s = new TestRepository<>(src)) { + src.incrementOpen(); RevCommit N = s.commit().parent(B).add("q", s.blob("a")).create(); RevTree t = s.parseBody(N).getTree(); 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 5569bca23c..ef0817adb8 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 @@ -443,6 +443,26 @@ public class RefSpecTest { a.setDestination("refs/remotes/origin/*/*"); } + @Test(expected = IllegalArgumentException.class) + public void invalidNegativeAndForce() { + assertNotNull(new RefSpec("^+refs/heads/master")); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidForceAndNegative() { + assertNotNull(new RefSpec("+^refs/heads/master")); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidNegativeNoSrcDest() { + assertNotNull(new RefSpec("^")); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidNegativeBothSrcDest() { + assertNotNull(new RefSpec("^refs/heads/*:refs/heads/*")); + } + @Test public void sourceOnlywithWildcard() { RefSpec a = new RefSpec("refs/heads/*", @@ -466,4 +486,46 @@ public class RefSpecTest { assertTrue(a.matchSource("refs/heads/master")); assertNull(a.getDestination()); } + + @Test + public void matching() { + RefSpec a = new RefSpec(":"); + assertTrue(a.isMatching()); + assertFalse(a.isForceUpdate()); + } + + @Test + public void matchingForced() { + RefSpec a = new RefSpec("+:"); + assertTrue(a.isMatching()); + assertTrue(a.isForceUpdate()); + } + + @Test + public void negativeRefSpecWithDest() { + RefSpec a = new RefSpec("^:refs/readonly/*"); + assertTrue(a.isNegative()); + assertNull(a.getSource()); + assertEquals(a.getDestination(), "refs/readonly/*"); + } + + // Because of some of the API's existing behavior, without a colon at the + // end of the refspec, dest will be null. + @Test + public void negativeRefSpecWithSrcAndNullDest() { + RefSpec a = new RefSpec("^refs/testdata/*"); + assertTrue(a.isNegative()); + assertNull(a.getDestination()); + assertEquals(a.getSource(), "refs/testdata/*"); + } + + // Because of some of the API's existing behavior, with a colon at the end + // of the refspec, dest will be empty. + @Test + public void negativeRefSpecWithSrcAndEmptyDest() { + RefSpec a = new RefSpec("^refs/testdata/*:"); + assertTrue(a.isNegative()); + assertTrue(a.getDestination().isEmpty()); + assertEquals(a.getSource(), "refs/testdata/*"); + } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/SideBandInputStreamTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/SideBandInputStreamTest.java new file mode 100644 index 0000000000..7ac83195fb --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/SideBandInputStreamTest.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.transport; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; + +import org.junit.Before; +import org.junit.Test; + +public class SideBandInputStreamTest { + + private StringWriter messages; + + private SideBandInputStream sideband; + + @Before + public void setup() { + messages = new StringWriter(); + } + + @Test + public void progressSingleCR() throws IOException { + init(packet("message\r")); + assertTrue(sideband.read() < 0); + assertEquals("message\r", messages.toString()); + } + + @Test + public void progressSingleLF() throws IOException { + init(packet("message\n")); + assertTrue(sideband.read() < 0); + assertEquals("message\n", messages.toString()); + } + + @Test + public void progressSingleCRLF() throws IOException { + init(packet("message\r\n")); + assertTrue(sideband.read() < 0); + assertEquals("message\r\n", messages.toString()); + } + + @Test + public void progressMultiCR() throws IOException { + init(packet("message 0%\rmessage 100%\r")); + assertTrue(sideband.read() < 0); + assertEquals("message 0%\rmessage 100%\r", messages.toString()); + } + + @Test + public void progressMultiLF() throws IOException { + init(packet("message 0%\nmessage 100%\n")); + assertTrue(sideband.read() < 0); + assertEquals("message 0%\nmessage 100%\n", messages.toString()); + } + + @Test + public void progressMultiCRLF() throws IOException { + init(packet("message 0%\r\nmessage 100%\r\n")); + assertTrue(sideband.read() < 0); + assertEquals("message 0%\r\nmessage 100%\r\n", messages.toString()); + } + + @Test + public void progressPartial() throws IOException { + init(packet("message")); + assertTrue(sideband.read() < 0); + assertEquals("", messages.toString()); + sideband.drainMessages(); + assertEquals("message\n", messages.toString()); + } + + @Test + public void progressPartialTwoCR() throws IOException { + init(packet("message") + packet("message\r")); + assertTrue(sideband.read() < 0); + assertEquals("messagemessage\r", messages.toString()); + } + + @Test + public void progressPartialTwoLF() throws IOException { + init(packet("message") + packet("message\n")); + assertTrue(sideband.read() < 0); + assertEquals("messagemessage\n", messages.toString()); + } + + @Test + public void progressPartialTwoCRLF() throws IOException { + init(packet("message") + packet("message\r\n")); + assertTrue(sideband.read() < 0); + assertEquals("messagemessage\r\n", messages.toString()); + } + + @Test + public void progressPartialThreeCR() throws IOException { + init(packet("message") + packet("message") + packet("message\r")); + assertTrue(sideband.read() < 0); + assertEquals("messagemessagemessage\r", messages.toString()); + } + + @Test + public void progressPartialThreeLF() throws IOException { + init(packet("message") + packet("message") + packet("message\n")); + assertTrue(sideband.read() < 0); + assertEquals("messagemessagemessage\n", messages.toString()); + } + + @Test + public void progressPartialThreeCRLF() throws IOException { + init(packet("message") + packet("message") + packet("message\r\n")); + assertTrue(sideband.read() < 0); + assertEquals("messagemessagemessage\r\n", messages.toString()); + } + + @Test + public void progressPartialCR() throws IOException { + init(packet("message 0%\rmessage 100%")); + assertTrue(sideband.read() < 0); + assertEquals("message 0%\r", messages.toString()); + sideband.drainMessages(); + assertEquals("message 0%\rmessage 100%\n", messages.toString()); + } + + @Test + public void progressPartialLF() throws IOException { + init(packet("message 0%\nmessage 100%")); + assertTrue(sideband.read() < 0); + assertEquals("message 0%\n", messages.toString()); + sideband.drainMessages(); + assertEquals("message 0%\nmessage 100%\n", messages.toString()); + } + + @Test + public void progressPartialCRLF() throws IOException { + init(packet("message 0%\r\nmessage 100%")); + assertTrue(sideband.read() < 0); + assertEquals("message 0%\r\n", messages.toString()); + sideband.drainMessages(); + assertEquals("message 0%\r\nmessage 100%\n", messages.toString()); + } + + @Test + public void progressPartialSplitCR() throws IOException { + init(packet("message") + "0006\001a" + packet(" 0%\rmessa") + + packet("ge 100%")); + assertEquals('a', sideband.read()); + assertEquals("", messages.toString()); + assertTrue(sideband.read() < 0); + assertEquals("message 0%\r", messages.toString()); + sideband.drainMessages(); + assertEquals("message 0%\rmessage 100%\n", messages.toString()); + } + + @Test + public void progressPartialSplitLF() throws IOException { + init(packet("message") + "0006\001a" + packet(" 0%\nmessa") + + packet("ge 100%")); + assertEquals('a', sideband.read()); + assertEquals("", messages.toString()); + assertTrue(sideband.read() < 0); + assertEquals("message 0%\n", messages.toString()); + sideband.drainMessages(); + assertEquals("message 0%\nmessage 100%\n", messages.toString()); + } + + @Test + public void progressPartialSplitCRLF() throws IOException { + init(packet("message") + "0006\001a" + packet(" 0%\r\nmessa") + + packet("ge 100%")); + assertEquals('a', sideband.read()); + assertEquals("", messages.toString()); + assertTrue(sideband.read() < 0); + assertEquals("message 0%\r\n", messages.toString()); + sideband.drainMessages(); + assertEquals("message 0%\r\nmessage 100%\n", messages.toString()); + } + + @Test + public void progressInterleaved() throws IOException { + init(packet("message 0%\r") + "0006\001a" + packet("message 10%") + + "0006\001b" + packet("\rmessage 100%\n")); + assertEquals('a', sideband.read()); + assertEquals("message 0%\r", messages.toString()); + assertEquals('b', sideband.read()); + assertEquals("message 0%\r", messages.toString()); + assertTrue(sideband.read() < 0); + assertEquals("message 0%\rmessage 10%\rmessage 100%\n", + messages.toString()); + } + + @Test + public void progressInterleavedPartial() throws IOException { + init(packet("message 0%\r") + "0006\001a" + packet("message 10%") + + "0006\001b" + packet("\rmessage 100%")); + assertEquals('a', sideband.read()); + assertEquals("message 0%\r", messages.toString()); + assertEquals('b', sideband.read()); + assertEquals("message 0%\r", messages.toString()); + assertTrue(sideband.read() < 0); + assertEquals("message 0%\rmessage 10%\r", messages.toString()); + sideband.drainMessages(); + assertEquals("message 0%\rmessage 10%\rmessage 100%\n", + messages.toString()); + } + + private String packet(String data) { + return String.format("%04x\002%s", Integer.valueOf(data.length() + 5), + data); + } + + private void init(String packets) { + InputStream rawIn = new ByteArrayInputStream( + (packets + "0000").getBytes(StandardCharsets.UTF_8)); + sideband = new SideBandInputStream(rawIn, null, messages, null); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackTest.java index 1c5a521801..7131905850 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackTest.java @@ -1107,6 +1107,61 @@ public class UploadPackTest { } @Test + public void testV2FetchServerStopsNegotiationForRefWithoutParents() + throws Exception { + RevCommit fooCommit = remote.commit().message("x").create(); + RevCommit barCommit = remote.commit().message("y").create(); + remote.update("refs/changes/01/1/1", fooCommit); + remote.update("refs/changes/02/2/1", barCommit); + + ByteArrayInputStream recvStream = uploadPackV2("command=fetch\n", + PacketLineIn.delimiter(), + "want " + fooCommit.toObjectId().getName() + "\n", + "have " + barCommit.toObjectId().getName() + "\n", + PacketLineIn.end()); + PacketLineIn pckIn = new PacketLineIn(recvStream); + + assertThat(pckIn.readString(), is("acknowledgments")); + assertThat(pckIn.readString(), + is("ACK " + barCommit.toObjectId().getName())); + assertThat(pckIn.readString(), is("ready")); + assertTrue(PacketLineIn.isDelimiter(pckIn.readString())); + assertThat(pckIn.readString(), is("packfile")); + parsePack(recvStream); + assertTrue(client.getObjectDatabase().has(fooCommit.toObjectId())); + } + + @Test + public void testV2FetchServerDoesNotStopNegotiationWhenOneRefWithoutParentAndOtherWithParents() + throws Exception { + RevCommit fooCommit = remote.commit().message("x").create(); + RevCommit barParent = remote.commit().message("y").create(); + RevCommit barChild = remote.commit().message("y").parent(barParent) + .create(); + RevCommit fooBarParent = remote.commit().message("z").create(); + RevCommit fooBarChild = remote.commit().message("y") + .parent(fooBarParent) + .create(); + remote.update("refs/changes/01/1/1", fooCommit); + remote.update("refs/changes/02/2/1", barChild); + remote.update("refs/changes/03/3/1", fooBarChild); + + ByteArrayInputStream recvStream = uploadPackV2("command=fetch\n", + PacketLineIn.delimiter(), + "want " + fooCommit.toObjectId().getName() + "\n", + "want " + barChild.toObjectId().getName() + "\n", + "want " + fooBarChild.toObjectId().getName() + "\n", + "have " + fooBarParent.toObjectId().getName() + "\n", + PacketLineIn.end()); + PacketLineIn pckIn = new PacketLineIn(recvStream); + + assertThat(pckIn.readString(), is("acknowledgments")); + assertThat(pckIn.readString(), + is("ACK " + fooBarParent.toObjectId().getName())); + assertTrue(PacketLineIn.isEnd(pckIn.readString())); + } + + @Test public void testV2FetchThinPack() throws Exception { String commonInBlob = "abcdefghijklmnopqrstuvwxyz"; diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/WalkEncryptionTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/WalkEncryptionTest.java index f289a922bb..5adf7faf20 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/WalkEncryptionTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/WalkEncryptionTest.java @@ -99,7 +99,7 @@ import org.slf4j.LoggerFactory; public class WalkEncryptionTest { /** - * Logger setup: ${project_loc}/tst-rsrc/log4j.properties + * Logger setup: ${project_loc}/tst-rsrc/simplelogger.properties */ static final Logger logger = LoggerFactory.getLogger(WalkEncryptionTest.class); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FilterCommandsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FilterCommandsTest.java index 36f94fbd20..89d31c3e8f 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FilterCommandsTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FilterCommandsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016, Christian Halstrick <christian.halstrick@sap.com> and others + * Copyright (C) 2016, 2022 Christian Halstrick <christian.halstrick@sap.com> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -10,12 +10,17 @@ package org.eclipse.jgit.util; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.HashSet; +import java.util.Set; import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.MergeResult; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.attributes.FilterCommand; import org.eclipse.jgit.attributes.FilterCommandFactory; @@ -86,6 +91,14 @@ public class FilterCommandsTest extends RepositoryTestCase { secondCommit = git.commit().setMessage("Second commit").call(); } + @Override + public void tearDown() throws Exception { + Set<String> existingFilters = new HashSet<>( + FilterCommandRegistry.getRegisteredFilterCommands()); + existingFilters.forEach(FilterCommandRegistry::unregister); + super.tearDown(); + } + @Test public void testBuiltinCleanFilter() throws IOException, GitAPIException { @@ -217,4 +230,133 @@ public class FilterCommandsTest extends RepositoryTestCase { config.save(); } + @Test + public void testBranchSwitch() throws Exception { + String builtinCommandPrefix = "jgit://builtin/test/"; + FilterCommandRegistry.register(builtinCommandPrefix + "smudge", + new TestCommandFactory('s')); + FilterCommandRegistry.register(builtinCommandPrefix + "clean", + new TestCommandFactory('c')); + StoredConfig config = git.getRepository().getConfig(); + config.setString("filter", "test", "smudge", + builtinCommandPrefix + "smudge"); + config.setString("filter", "test", "clean", + builtinCommandPrefix + "clean"); + config.save(); + // We're on the test branch + File aFile = writeTrashFile("a.txt", "a"); + writeTrashFile(".gitattributes", "a.txt filter=test"); + File cFile = writeTrashFile("cc/c.txt", "C"); + writeTrashFile("cc/.gitattributes", "c.txt filter=test"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("On test").call(); + git.checkout().setName("master").call(); + git.branchCreate().setName("other").call(); + git.checkout().setName("other").call(); + writeTrashFile("b.txt", "b"); + writeTrashFile(".gitattributes", "b.txt filter=test"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("On other").call(); + git.checkout().setName("test").call(); + checkFile(aFile, "scsa"); + checkFile(cFile, "scsC"); + } + + @Test + public void testCheckoutSingleFile() throws Exception { + String builtinCommandPrefix = "jgit://builtin/test/"; + FilterCommandRegistry.register(builtinCommandPrefix + "smudge", + new TestCommandFactory('s')); + FilterCommandRegistry.register(builtinCommandPrefix + "clean", + new TestCommandFactory('c')); + StoredConfig config = git.getRepository().getConfig(); + config.setString("filter", "test", "smudge", + builtinCommandPrefix + "smudge"); + config.setString("filter", "test", "clean", + builtinCommandPrefix + "clean"); + config.save(); + // We're on the test branch + File aFile = writeTrashFile("a.txt", "a"); + File attributes = writeTrashFile(".gitattributes", "a.txt filter=test"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("On test").call(); + git.checkout().setName("master").call(); + git.branchCreate().setName("other").call(); + git.checkout().setName("other").call(); + writeTrashFile("b.txt", "b"); + writeTrashFile(".gitattributes", "b.txt filter=test"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("On other").call(); + git.checkout().setName("master").call(); + assertFalse(aFile.exists()); + assertFalse(attributes.exists()); + git.checkout().setStartPoint("test").addPath("a.txt").call(); + checkFile(aFile, "scsa"); + } + + @Test + public void testCheckoutSingleFile2() throws Exception { + String builtinCommandPrefix = "jgit://builtin/test/"; + FilterCommandRegistry.register(builtinCommandPrefix + "smudge", + new TestCommandFactory('s')); + FilterCommandRegistry.register(builtinCommandPrefix + "clean", + new TestCommandFactory('c')); + StoredConfig config = git.getRepository().getConfig(); + config.setString("filter", "test", "smudge", + builtinCommandPrefix + "smudge"); + config.setString("filter", "test", "clean", + builtinCommandPrefix + "clean"); + config.save(); + // We're on the test branch + File aFile = writeTrashFile("a.txt", "a"); + File attributes = writeTrashFile(".gitattributes", "a.txt filter=test"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("On test").call(); + git.checkout().setName("master").call(); + git.branchCreate().setName("other").call(); + git.checkout().setName("other").call(); + writeTrashFile("b.txt", "b"); + writeTrashFile(".gitattributes", "b.txt filter=test"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("On other").call(); + git.checkout().setName("master").call(); + assertFalse(aFile.exists()); + assertFalse(attributes.exists()); + writeTrashFile(".gitattributes", ""); + git.checkout().setStartPoint("test").addPath("a.txt").call(); + checkFile(aFile, "scsa"); + } + + @Test + public void testMerge() throws Exception { + String builtinCommandPrefix = "jgit://builtin/test/"; + FilterCommandRegistry.register(builtinCommandPrefix + "smudge", + new TestCommandFactory('s')); + FilterCommandRegistry.register(builtinCommandPrefix + "clean", + new TestCommandFactory('c')); + StoredConfig config = git.getRepository().getConfig(); + config.setString("filter", "test", "smudge", + builtinCommandPrefix + "smudge"); + config.setString("filter", "test", "clean", + builtinCommandPrefix + "clean"); + config.save(); + // We're on the test branch. Set up two branches that are expected to + // merge cleanly. + File aFile = writeTrashFile("a.txt", "a"); + writeTrashFile(".gitattributes", "a.txt filter=test"); + git.add().addFilepattern(".").call(); + RevCommit aCommit = git.commit().setMessage("On test").call(); + git.checkout().setName("master").call(); + assertFalse(aFile.exists()); + git.branchCreate().setName("other").call(); + git.checkout().setName("other").call(); + writeTrashFile("b/b.txt", "b"); + writeTrashFile("b/.gitattributes", "b.txt filter=test"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("On other").call(); + MergeResult result = git.merge().include(aCommit).call(); + assertEquals(MergeResult.MergeStatus.MERGED, result.getMergeStatus()); + checkFile(aFile, "scsa"); + } + } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/HookTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/HookTest.java index 33ed360efd..1231aefee0 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/HookTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/HookTest.java @@ -9,6 +9,7 @@ */ package org.eclipse.jgit.util; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.fail; @@ -77,7 +78,7 @@ public class HookTest extends RepositoryTestCase { "Rejected by \"commit-msg\" hook.\nstderr\n", e.getMessage()); assertEquals("unexpected output from commit-msg hook", "test\n", - out.toString()); + out.toString(UTF_8)); } } @@ -95,7 +96,7 @@ public class HookTest extends RepositoryTestCase { git.commit().setMessage("commit") .setHookOutputStream(new PrintStream(out)).call(); assertEquals(".git/COMMIT_EDITMSG\n", - out.toString("UTF-8")); + out.toString(UTF_8)); } @Test @@ -129,9 +130,9 @@ public class HookTest extends RepositoryTestCase { new PrintStream(out), new PrintStream(err), "stdin"); assertEquals("unexpected hook output", "test arg1 arg2\nstdin\n", - out.toString("UTF-8")); + out.toString(UTF_8)); assertEquals("unexpected output on stderr stream", "stderr\n", - err.toString("UTF-8")); + err.toString(UTF_8)); assertEquals("unexpected exit code", 0, res.getExitCode()); assertEquals("unexpected process status", ProcessResult.Status.OK, res.getStatus()); @@ -160,7 +161,7 @@ public class HookTest extends RepositoryTestCase { } assertEquals("unexpected hook output", "test pre-commit\ntest commit-msg .git/COMMIT_EDITMSG\ntest post-commit\n", - out.toString("UTF-8")); + out.toString(UTF_8)); } @Test @@ -181,9 +182,9 @@ public class HookTest extends RepositoryTestCase { assertEquals("unexpected hook output", "test arg1 arg2\nstdin\n" + db.getDirectory().getAbsolutePath() + '\n' + db.getWorkTree().getAbsolutePath() + '\n', - out.toString("UTF-8")); + out.toString(UTF_8)); assertEquals("unexpected output on stderr stream", "stderr\n", - err.toString("UTF-8")); + err.toString(UTF_8)); assertEquals("unexpected exit code", 0, res.getExitCode()); assertEquals("unexpected process status", ProcessResult.Status.OK, res.getStatus()); @@ -214,9 +215,9 @@ public class HookTest extends RepositoryTestCase { "test arg1 arg2\nstdin\n" + db.getDirectory().getAbsolutePath() + '\n' + db.getWorkTree().getAbsolutePath() + '\n', - out.toString("UTF-8")); + out.toString(UTF_8)); assertEquals("unexpected output on stderr stream", "stderr\n", - err.toString("UTF-8")); + err.toString(UTF_8)); assertEquals("unexpected exit code", 0, res.getExitCode()); assertEquals("unexpected process status", ProcessResult.Status.OK, res.getStatus()); @@ -249,9 +250,9 @@ public class HookTest extends RepositoryTestCase { "test arg1 arg2\nstdin\n" + db.getDirectory().getAbsolutePath() + '\n' + db.getWorkTree().getAbsolutePath() + '\n', - out.toString("UTF-8")); + out.toString(UTF_8)); assertEquals("unexpected output on stderr stream", "stderr\n", - err.toString("UTF-8")); + err.toString(UTF_8)); assertEquals("unexpected exit code", 0, res.getExitCode()); assertEquals("unexpected process status", ProcessResult.Status.OK, res.getStatus()); @@ -281,9 +282,9 @@ public class HookTest extends RepositoryTestCase { "test arg1 arg2\nstdin\n" + db.getDirectory().getAbsolutePath() + '\n' + db.getWorkTree().getAbsolutePath() + '\n', - out.toString("UTF-8")); + out.toString(UTF_8)); assertEquals("unexpected output on stderr stream", "stderr\n", - err.toString("UTF-8")); + err.toString(UTF_8)); assertEquals("unexpected exit code", 0, res.getExitCode()); assertEquals("unexpected process status", ProcessResult.Status.OK, res.getStatus()); @@ -310,7 +311,7 @@ public class HookTest extends RepositoryTestCase { "Rejected by \"pre-commit\" hook.\nstderr\n", e.getMessage()); assertEquals("unexpected output from pre-commit hook", "test\n", - out.toString()); + out.toString(UTF_8)); } } diff --git a/org.eclipse.jgit/.settings/.api_filters b/org.eclipse.jgit/.settings/.api_filters new file mode 100644 index 0000000000..6eb8bd3732 --- /dev/null +++ b/org.eclipse.jgit/.settings/.api_filters @@ -0,0 +1,88 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<component id="org.eclipse.jgit" version="2"> + <resource path="src/org/eclipse/jgit/errors/NoRemoteRepositoryException.java" type="org.eclipse.jgit.errors.NoRemoteRepositoryException"> + <filter id="1141899266"> + <message_arguments> + <message_argument value="5.13"/> + <message_argument value="6.1"/> + <message_argument value="NoRemoteRepositoryException(URIish, String, Throwable)"/> + </message_arguments> + </filter> + </resource> + <resource path="src/org/eclipse/jgit/lib/ObjectDatabase.java" type="org.eclipse.jgit.lib.ObjectDatabase"> + <filter id="336695337"> + <message_arguments> + <message_argument value="org.eclipse.jgit.lib.ObjectDatabase"/> + <message_argument value="getApproximateObjectCount()"/> + </message_arguments> + </filter> + </resource> + <resource path="src/org/eclipse/jgit/lib/TypedConfigGetter.java" type="org.eclipse.jgit.lib.TypedConfigGetter"> + <filter id="403767336"> + <message_arguments> + <message_argument value="org.eclipse.jgit.lib.TypedConfigGetter"/> + <message_argument value="UNSET_INT"/> + </message_arguments> + </filter> + <filter id="403804204"> + <message_arguments> + <message_argument value="org.eclipse.jgit.lib.TypedConfigGetter"/> + <message_argument value="getIntInRange(Config, String, String, String, int, int, int)"/> + </message_arguments> + </filter> + </resource> + <resource path="src/org/eclipse/jgit/merge/ResolveMerger.java" type="org.eclipse.jgit.merge.ResolveMerger"> + <filter id="338792546"> + <message_arguments> + <message_argument value="org.eclipse.jgit.merge.ResolveMerger"/> + <message_argument value="addCheckoutMetadata(String, Attributes)"/> + </message_arguments> + </filter> + <filter id="338792546"> + <message_arguments> + <message_argument value="org.eclipse.jgit.merge.ResolveMerger"/> + <message_argument value="addToCheckout(String, DirCacheEntry, Attributes)"/> + </message_arguments> + </filter> + <filter id="338792546"> + <message_arguments> + <message_argument value="org.eclipse.jgit.merge.ResolveMerger"/> + <message_argument value="processEntry(CanonicalTreeParser, CanonicalTreeParser, CanonicalTreeParser, DirCacheBuildIterator, WorkingTreeIterator, boolean, Attributes)"/> + </message_arguments> + </filter> + </resource> + <resource path="src/org/eclipse/jgit/transport/AwsRequestSignerV4.java" type="org.eclipse.jgit.transport.AwsRequestSignerV4"> + <filter id="1108344834"> + <message_arguments> + <message_argument value="5.13"/> + <message_argument value="6.3"/> + <message_argument value="org.eclipse.jgit.transport.AwsRequestSignerV4"/> + </message_arguments> + </filter> + </resource> + <resource path="src/org/eclipse/jgit/transport/BasePackPushConnection.java" type="org.eclipse.jgit.transport.BasePackPushConnection"> + <filter id="338792546"> + <message_arguments> + <message_argument value="org.eclipse.jgit.transport.BasePackPushConnection"/> + <message_argument value="noRepository()"/> + </message_arguments> + </filter> + </resource> + <resource path="src/org/eclipse/jgit/transport/PushConfig.java" type="org.eclipse.jgit.transport.PushConfig"> + <filter id="338722907"> + <message_arguments> + <message_argument value="org.eclipse.jgit.transport.PushConfig"/> + <message_argument value="PushConfig()"/> + </message_arguments> + </filter> + </resource> + <resource path="src/org/eclipse/jgit/util/HttpSupport.java" type="org.eclipse.jgit.util.HttpSupport"> + <filter id="1141899266"> + <message_arguments> + <message_argument value="5.13"/> + <message_argument value="6.3"/> + <message_argument value="urlEncode(String, boolean)"/> + </message_arguments> + </filter> + </resource> +</component> diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF index 1a3978f3ae..6efbffdbe8 100644 --- a/org.eclipse.jgit/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit/META-INF/MANIFEST.MF @@ -70,6 +70,8 @@ Export-Package: org.eclipse.jgit.annotations;version="7.0.0", org.eclipse.jgit.internal;version="7.0.0"; x-friends:="org.eclipse.jgit.test, org.eclipse.jgit.http.test", + org.eclipse.jgit.internal.diff;version="7.0.0"; + x-friends:="org.eclipse.jgit.test", org.eclipse.jgit.internal.diffmergetool;version="7.0.0"; x-friends:="org.eclipse.jgit.test, org.eclipse.jgit.pgm.test, 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 ee97c265e9..66adad5151 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -209,12 +209,14 @@ couldNotGetAdvertisedRef=Remote {0} did not advertise Ref for branch {1}. This R couldNotGetRepoStatistics=Could not get repository statistics couldNotFindTabInLine=Could not find tab in line {0}. Tab is the mandatory separator for the Netscape Cookie File Format. couldNotFindSixTabsInLine=Could not find 6 tabs but only {0} in line '{1}'. 7 tab separated columns per line are mandatory for the Netscape Cookie File Format. +couldNotHashByteArrayWithSha256=Could not hash byte array with SHA-256 algorithm. couldNotLockHEAD=Could not lock HEAD couldNotPersistCookies=Could not persist received cookies in file ''{0}'' couldNotReadCookieFile=Could not read cookie file ''{0}'' couldNotReadIndexInOneGo=Could not read index in one go, only {0} out of {1} read couldNotReadObjectWhileParsingCommit=Could not read an object while parsing commit {0} couldNotRewindToUpstreamCommit=Could not rewind to upstream commit +couldNotSignStringWithKey=Could not sign string with key. couldNotURLEncodeToUTF8=Could not URL encode to UTF-8 countingObjects=Counting objects corruptPack=Pack file {0} is corrupt, removing it from pack list @@ -237,6 +239,9 @@ deleteTagUnexpectedResult=Delete tag returned unexpected result {0} deletingNotSupported=Deleting {0} not supported. destinationIsNotAWildcard=Destination is not a wildcard. detachedHeadDetected=HEAD is detached +diffToolNotGivenError=No diff tool provided and no defaults configured. +diffToolNotSpecifiedInGitAttributesError=Diff tool specified in git attributes cannot be found. +diffToolNullError=Parameter for diff tool cannot be null. dirCacheDoesNotHaveABackingFile=DirCache does not have a backing file dirCacheFileIsNotLocked=DirCache {0} not locked dirCacheIsNotLocked=DirCache is not locked @@ -354,6 +359,8 @@ initFailedNonBareRepoSameDirs=When initializing a non-bare repo with directory { inMemoryBufferLimitExceeded=In-memory buffer limit exceeded inputDidntMatchLength=Input did not match supplied length. {0} bytes are missing. inputStreamMustSupportMark=InputStream must support mark() +integerValueNotInRange=Integer value {0}.{1} = {2} not in range {3}..{4} +integerValueNotInRangeSubSection=Integer value {0}.{1}.{2} = {3} not in range {4}..{5} integerValueOutOfRange=Integer value {0}.{1} out of range internalRevisionError=internal revision error internalServerError=internal server error @@ -361,9 +368,11 @@ interruptedWriting=Interrupted writing {0} inTheFuture=in the future invalidAdvertisementOf=invalid advertisement of {0} invalidAncestryLength=Invalid ancestry length +invalidAwsApiSignatureVersion=Invalid aws.api.signature.version: {0} invalidBooleanValue=Invalid boolean value: {0}.{1}={2} invalidChannel=Invalid channel {0} invalidCommitParentNumber=Invalid commit parent number +invalidCoreAbbrev=Invalid value {0} of option core.abbrev invalidDepth=Invalid depth: {0} invalidEncoding=Invalid encoding from git config i18n.commitEncoding: {0} invalidEncryption=Invalid encryption @@ -390,6 +399,7 @@ invalidLineInConfigFileWithParam=Invalid line in config file: {0} invalidModeFor=Invalid mode {0} for {1} {2} in {3}. invalidModeForPath=Invalid mode {0} for path {1} invalidNameContainsDotDot=Invalid name (contains ".."): {0} +invalidNegativeAndForce= RefSpec can't be negative and forceful. invalidObject=Invalid {0} {1}: {2} invalidOldIdSent=invalid old id sent invalidPacketLineHeader=Invalid packet line header: {0} @@ -453,11 +463,14 @@ mergeStrategyDoesNotSupportHeads=merge strategy {0} does not support {1} heads t mergeUsingStrategyResultedInDescription=Merge of revisions {0} with base {1} using strategy {2} resulted in: {3}. {4} mergeRecursiveConflictsWhenMergingCommonAncestors=Multiple common ancestors were found and merging them resulted in a conflict: {0}, {1} mergeRecursiveTooManyMergeBasesFor = "More than {0} merge bases for:\n a {1}\n b {2} found:\n count {3}" +mergeToolNotGivenError=No merge tool provided and no defaults configured. +mergeToolNullError=Parameter for merge tool cannot be null. messageAndTaggerNotAllowedInUnannotatedTags = Unannotated tags cannot have a message or tagger minutesAgo={0} minutes ago mismatchOffset=mismatch offset for object {0} mismatchCRC=mismatch CRC for object {0} missingAccesskey=Missing accesskey. +missingAwsRegion=Missing region (e.g. us-west-2). missingConfigurationForKey=No value for key {0} found in configuration missingCookieFile=Configured http.cookieFile ''{0}'' is missing missingCRC=missing CRC for object {0} @@ -564,6 +577,11 @@ pushCertificateInvalidField=Push certificate has missing or invalid value for {0 pushCertificateInvalidFieldValue=Push certificate has missing or invalid value for {0}: {1} pushCertificateInvalidHeader=Push certificate has invalid header format pushCertificateInvalidSignature=Push certificate has invalid signature format +pushDefaultNothing=No refspec given and push.default=nothing; no upstream branch can be determined +pushDefaultNoUpstream=No upstream branch found for local branch ''{0}'' +pushDefaultSimple=push.default=simple requires local branch name ''{0}'' to be equal to upstream tracked branch name ''{1}'' +pushDefaultTriangularUpstream=push.default=upstream cannot be used when the push remote ''{0}'' is different from the fetch remote ''{1}'' +pushDefaultUnknown=Unknown push.default={0}; cannot push pushIsNotSupportedForBundleTransport=Push is not supported for bundle transport pushNotPermitted=push not permitted pushOptionsNotSupported=Push options not supported; received {0} @@ -739,6 +757,7 @@ unableToWrite=Unable to write {0} unableToSignCommitNoSecretKey=Unable to sign commit. Signing key not available. unauthorized=Unauthorized unencodeableFile=Unencodable file: {0} +unexpectedAwsApiSignatureVersion=Unexpected AWS API Signature Version: {0} unexpectedCompareResult=Unexpected metadata comparison result: {0} unexpectedEndOfConfigFile=Unexpected end of config file unexpectedEndOfInput=Unexpected end of input diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java index 7922f9e729..ceba89d166 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java @@ -9,6 +9,8 @@ */ package org.eclipse.jgit.api; +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_ABBREV_STRING_LENGTH; + import java.io.IOException; import java.text.MessageFormat; import java.util.LinkedList; @@ -28,6 +30,7 @@ import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.events.WorkingTreeModifiedEvent; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.CommitConfig; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectId; @@ -124,7 +127,7 @@ public class CherryPickCommand extends GitCommand<CherryPickResult> { final RevCommit srcParent = getParentCommit(srcCommit, revWalk); String ourName = calculateOurName(headRef); - String cherryPickName = srcCommit.getId().abbreviate(7).name() + String cherryPickName = srcCommit.getId().abbreviate(OBJECT_ID_ABBREV_STRING_LENGTH).name() + " " + srcCommit.getShortMessage(); //$NON-NLS-1$ Merger merger = strategy.newMerger(repo); @@ -181,9 +184,13 @@ public class CherryPickCommand extends GitCommand<CherryPickResult> { String message; if (unmergedPaths != null) { + CommitConfig cfg = repo.getConfig() + .get(CommitConfig.KEY); + message = srcCommit.getFullMessage(); + char commentChar = cfg.getCommentChar(message); message = new MergeMessageFormatter() - .formatWithConflicts(srcCommit.getFullMessage(), - unmergedPaths); + .formatWithConflicts(message, unmergedPaths, + commentChar); } else { message = srcCommit.getFullMessage(); } 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 37f1d482aa..3b3baf5a12 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java @@ -19,6 +19,7 @@ import java.util.HashMap; import java.util.LinkedList; import java.util.List; +import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.api.errors.AbortedByHookException; import org.eclipse.jgit.api.errors.CanceledException; import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; @@ -46,6 +47,8 @@ import org.eclipse.jgit.hooks.PostCommitHook; import org.eclipse.jgit.hooks.PreCommitHook; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.CommitConfig; +import org.eclipse.jgit.lib.CommitConfig.CleanupMode; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.GpgConfig; @@ -133,6 +136,12 @@ public class CommitCommand extends GitCommand<RevCommit> { private CredentialsProvider credentialsProvider; + private @NonNull CleanupMode cleanupMode = CleanupMode.VERBATIM; + + private boolean cleanDefaultIsStrip = true; + + private Character commentChar; + /** * Constructor for CommitCommand * @@ -200,7 +209,7 @@ public class CommitCommand extends GitCommand<RevCommit> { throw new WrongRepositoryStateException( JGitText.get().commitAmendOnInitialNotPossible); - if (headId != null) + if (headId != null) { if (amend) { RevCommit previousCommit = rw.parseCommit(headId); for (RevCommit p : previousCommit.getParents()) @@ -210,7 +219,7 @@ public class CommitCommand extends GitCommand<RevCommit> { } else { parents.add(0, headId); } - + } if (!noVerify) { message = Hooks .commitMsg(repo, @@ -219,6 +228,33 @@ public class CommitCommand extends GitCommand<RevCommit> { .setCommitMessage(message).call(); } + CommitConfig config = null; + if (CleanupMode.DEFAULT.equals(cleanupMode)) { + config = repo.getConfig().get(CommitConfig.KEY); + cleanupMode = config.resolve(cleanupMode, cleanDefaultIsStrip); + } + char comments = (char) 0; + if (CleanupMode.STRIP.equals(cleanupMode) + || CleanupMode.SCISSORS.equals(cleanupMode)) { + if (commentChar == null) { + if (config == null) { + config = repo.getConfig().get(CommitConfig.KEY); + } + if (config.isAutoCommentChar()) { + // We're supposed to pick a character that isn't used, + // but then cleaning up won't remove any lines. So don't + // bother. + comments = (char) 0; + cleanupMode = CleanupMode.WHITESPACE; + } else { + comments = config.getCommentChar(); + } + } else { + comments = commentChar.charValue(); + } + } + message = CommitConfig.cleanText(message, cleanupMode, comments); + RevCommit revCommit; DirCache index = repo.lockDirCache(); try (ObjectInserter odi = repo.newObjectInserter()) { @@ -287,8 +323,14 @@ public class CommitCommand extends GitCommand<RevCommit> { private void sign(CommitBuilder commit) throws ServiceUnavailableException, CanceledException, UnsupportedSigningFormatException { if (gpgSigner == null) { - throw new ServiceUnavailableException( - JGitText.get().signingServiceUnavailable); + gpgSigner = GpgSigner.getDefault(); + if (gpgSigner == null) { + throw new ServiceUnavailableException( + JGitText.get().signingServiceUnavailable); + } + } + if (signingKey == null) { + signingKey = gpgConfig.getSigningKey(); } if (gpgSigner instanceof GpgObjectSigner) { ((GpgObjectSigner) gpgSigner).signObject(commit, @@ -623,12 +665,6 @@ public class CommitCommand extends GitCommand<RevCommit> { signCommit = gpgConfig.isSignCommits() ? Boolean.TRUE : Boolean.FALSE; } - if (signingKey == null) { - signingKey = gpgConfig.getSigningKey(); - } - if (gpgSigner == null) { - gpgSigner = GpgSigner.getDefault(); - } } private boolean isMergeDuringRebase(RepositoryState state) { @@ -658,6 +694,57 @@ public class CommitCommand extends GitCommand<RevCommit> { } /** + * Sets the {@link CleanupMode} to apply to the commit message. If not + * called, {@link CommitCommand} applies {@link CleanupMode#VERBATIM}. + * + * @param mode + * {@link CleanupMode} to set + * @return {@code this} + * @since 6.1 + */ + public CommitCommand setCleanupMode(@NonNull CleanupMode mode) { + checkCallable(); + this.cleanupMode = mode; + return this; + } + + /** + * Sets the default clean mode if {@link #setCleanupMode(CleanupMode) + * setCleanupMode(CleanupMode.DEFAULT)} is set and git config + * {@code commit.cleanup = default} or is not set. + * + * @param strip + * if {@code true}, default to {@link CleanupMode#STRIP}; + * otherwise default to {@link CleanupMode#WHITESPACE} + * @return {@code this} + * @since 6.1 + */ + public CommitCommand setDefaultClean(boolean strip) { + checkCallable(); + this.cleanDefaultIsStrip = strip; + return this; + } + + /** + * Sets the comment character to apply when cleaning a commit message. If + * {@code null} (the default) and the {@link #setCleanupMode(CleanupMode) + * clean-up mode} is {@link CleanupMode#STRIP} or + * {@link CleanupMode#SCISSORS}, the value of git config + * {@code core.commentChar} will be used. + * + * @param commentChar + * the comment character, or {@code null} to use the value from + * the git config + * @return {@code this} + * @since 6.1 + */ + public CommitCommand setCommentCharacter(Character commentChar) { + checkCallable(); + this.commentChar = commentChar; + return this; + } + + /** * Set whether to allow to create an empty commit * * @param allowEmpty @@ -806,7 +893,7 @@ public class CommitCommand extends GitCommand<RevCommit> { * command line. * * @param amend - * whether to ammend the tip of the current branch + * whether to amend the tip of the current branch * @return {@code this} */ public CommitCommand setAmend(boolean amend) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java index 1e524fadab..805a886392 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java @@ -11,6 +11,7 @@ package org.eclipse.jgit.api; import static org.eclipse.jgit.lib.Constants.R_REFS; import static org.eclipse.jgit.lib.Constants.R_TAGS; +import static org.eclipse.jgit.lib.TypedConfigGetter.UNSET_INT; import java.io.IOException; import java.text.MessageFormat; @@ -33,6 +34,7 @@ import org.eclipse.jgit.errors.InvalidPatternException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.fnmatch.FileNameMatcher; import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.AbbrevConfig; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; @@ -89,6 +91,11 @@ public class DescribeCommand extends GitCommand<String> { private boolean always; /** + * The prefix length to use when abbreviating a commit hash. + */ + private int abbrev = UNSET_INT; + + /** * Constructor for DescribeCommand. * * @param repo @@ -205,12 +212,33 @@ public class DescribeCommand extends GitCommand<String> { return this; } + /** + * Sets the prefix length to use when abbreviating an object SHA-1. + * + * @param abbrev + * minimum length of the abbreviated string. Must be in the range + * [{@value AbbrevConfig#MIN_ABBREV}, + * {@value Constants#OBJECT_ID_STRING_LENGTH}]. + * @return {@code this} + * @since 6.1 + */ + public DescribeCommand setAbbrev(int abbrev) { + if (abbrev == 0) { + this.abbrev = 0; + } else { + this.abbrev = AbbrevConfig.capAbbrev(abbrev); + } + return this; + } + private String longDescription(Ref tag, int depth, ObjectId tip) throws IOException { - return String.format( - "%s-%d-g%s", formatRefName(tag.getName()), //$NON-NLS-1$ - Integer.valueOf(depth), w.getObjectReader().abbreviate(tip) - .name()); + if (abbrev == 0) { + return formatRefName(tag.getName()); + } + return String.format("%s-%d-g%s", formatRefName(tag.getName()), //$NON-NLS-1$ + Integer.valueOf(depth), + w.getObjectReader().abbreviate(tip, abbrev).name()); } /** @@ -302,6 +330,9 @@ public class DescribeCommand extends GitCommand<String> { if (target == null) { setTarget(Constants.HEAD); } + if (abbrev == UNSET_INT) { + abbrev = AbbrevConfig.parseFromConfig(repo).get(); + } Collection<Ref> tagList = repo.getRefDatabase() .getRefsByPrefix(useAll ? R_REFS : R_TAGS); @@ -413,7 +444,12 @@ public class DescribeCommand extends GitCommand<String> { // if all the nodes are dominated by all the tags, the walk stops if (candidates.isEmpty()) { - return always ? w.getObjectReader().abbreviate(target).name() : null; + return always + ? w.getObjectReader() + .abbreviate(target, + AbbrevConfig.capAbbrev(abbrev)) + .name() + : null; } Candidate best = Collections.min(candidates, 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 0c691062f9..c3415581ef 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/LsRemoteCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/LsRemoteCommand.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011, 2020 Christoph Brill <egore911@egore911.de> and others + * Copyright (C) 2011, 2022 Christoph Brill <egore911@egore911.de> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -9,6 +9,7 @@ */ package org.eclipse.jgit.api; +import java.io.IOException; import java.net.URISyntaxException; import java.text.MessageFormat; import java.util.ArrayList; @@ -20,8 +21,8 @@ import java.util.Map; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.InvalidRemoteException; import org.eclipse.jgit.api.errors.JGitInternalException; +import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.errors.NotSupportedException; -import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Ref; @@ -30,6 +31,8 @@ import org.eclipse.jgit.transport.FetchConnection; import org.eclipse.jgit.transport.RefSpec; import org.eclipse.jgit.transport.Transport; import org.eclipse.jgit.transport.URIish; +import org.eclipse.jgit.transport.UrlConfig; +import org.eclipse.jgit.util.SystemReader; /** * The ls-remote command @@ -153,7 +156,7 @@ public class LsRemoteCommand extends try (Transport transport = repo != null ? Transport.open(repo, remote) - : Transport.open(new URIish(remote))) { + : Transport.open(new URIish(translate(remote)))) { transport.setOptionUploadPack(uploadPack); configure(transport); Collection<RefSpec> refSpecs = new ArrayList<>(1); @@ -185,11 +188,16 @@ public class LsRemoteCommand extends throw new JGitInternalException( JGitText.get().exceptionCaughtDuringExecutionOfLsRemoteCommand, e); - } catch (TransportException e) { + } catch (IOException | ConfigInvalidException e) { throw new org.eclipse.jgit.api.errors.TransportException( - e.getMessage(), - e); + e.getMessage(), e); } } + private String translate(String uri) + throws IOException, ConfigInvalidException { + UrlConfig urls = new UrlConfig( + SystemReader.getInstance().getUserConfig()); + return urls.replace(uri); + } } 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 ef56d802c8..ed4a5342b3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java @@ -34,6 +34,7 @@ import org.eclipse.jgit.dircache.DirCacheCheckout; import org.eclipse.jgit.events.WorkingTreeModifiedEvent; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.CommitConfig; import org.eclipse.jgit.lib.Config.ConfigEnum; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.NullProgressMonitor; @@ -404,8 +405,11 @@ public class MergeCommand extends GitCommand<MergeResult> { MergeStatus.FAILED, mergeStrategy, lowLevelResults, failingPaths, null); } + CommitConfig cfg = repo.getConfig().get(CommitConfig.KEY); + char commentChar = cfg.getCommentChar(message); String mergeMessageWithConflicts = new MergeMessageFormatter() - .formatWithConflicts(mergeMessage, unmergedPaths); + .formatWithConflicts(mergeMessage, unmergedPaths, + commentChar); repo.writeMergeCommitMsg(mergeMessageWithConflicts); return new MergeResult(null, merger.getBaseCommitId(), new ObjectId[] { headCommit.getId(), diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/PushCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/PushCommand.java index aa5a63499c..08353dfdfa 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/PushCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/PushCommand.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010, Chris Aniszczyk <caniszczyk@gmail.com> and others + * Copyright (C) 2010, 2022 Chris Aniszczyk <caniszczyk@gmail.com> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -21,7 +21,9 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.eclipse.jgit.api.errors.DetachedHeadException; import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.InvalidRefNameException; import org.eclipse.jgit.api.errors.InvalidRemoteException; import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.errors.NotSupportedException; @@ -29,11 +31,16 @@ import org.eclipse.jgit.errors.TooLargeObjectInPackException; import org.eclipse.jgit.errors.TooLargePackException; import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.BranchConfig; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.PushConfig; +import org.eclipse.jgit.transport.PushConfig.PushDefault; import org.eclipse.jgit.transport.PushResult; import org.eclipse.jgit.transport.RefLeaseSpec; import org.eclipse.jgit.transport.RefSpec; @@ -52,7 +59,7 @@ import org.eclipse.jgit.transport.Transport; public class PushCommand extends TransportCommand<PushCommand, Iterable<PushResult>> { - private String remote = Constants.DEFAULT_REMOTE_NAME; + private String remote; private final List<RefSpec> refSpecs; @@ -71,6 +78,10 @@ public class PushCommand extends private List<String> pushOptions; + // Legacy behavior as default. Use setPushDefault(null) to determine the + // value from the git config. + private PushDefault pushDefault = PushDefault.CURRENT; + /** * <p> * Constructor for PushCommand. @@ -98,19 +109,20 @@ public class PushCommand extends InvalidRemoteException, org.eclipse.jgit.api.errors.TransportException { checkCallable(); + setCallable(false); ArrayList<PushResult> pushResults = new ArrayList<>(3); try { + Config config = repo.getConfig(); + remote = determineRemote(config, remote); if (refSpecs.isEmpty()) { - RemoteConfig config = new RemoteConfig(repo.getConfig(), + RemoteConfig rc = new RemoteConfig(config, getRemote()); - refSpecs.addAll(config.getPushRefSpecs()); - } - if (refSpecs.isEmpty()) { - Ref head = repo.exactRef(Constants.HEAD); - if (head != null && head.isSymbolic()) - refSpecs.add(new RefSpec(head.getLeaf().getName())); + refSpecs.addAll(rc.getPushRefSpecs()); + if (refSpecs.isEmpty()) { + determineDefaultRefSpecs(config); + } } if (force) { @@ -118,8 +130,8 @@ public class PushCommand extends refSpecs.set(i, refSpecs.get(i).setForceUpdate(true)); } - final List<Transport> transports; - transports = Transport.openAll(repo, remote, Transport.Operation.PUSH); + List<Transport> transports = Transport.openAll(repo, remote, + Transport.Operation.PUSH); for (@SuppressWarnings("resource") // Explicitly closed in finally final Transport transport : transports) { transport.setPushThin(thin); @@ -171,6 +183,102 @@ public class PushCommand extends return pushResults; } + private String determineRemote(Config config, String remoteName) + throws IOException { + if (remoteName != null) { + return remoteName; + } + Ref head = repo.exactRef(Constants.HEAD); + String effectiveRemote = null; + BranchConfig branchCfg = null; + if (head != null && head.isSymbolic()) { + String currentBranch = head.getLeaf().getName(); + branchCfg = new BranchConfig(config, + Repository.shortenRefName(currentBranch)); + effectiveRemote = branchCfg.getPushRemote(); + } + if (effectiveRemote == null) { + effectiveRemote = config.getString( + ConfigConstants.CONFIG_REMOTE_SECTION, null, + ConfigConstants.CONFIG_KEY_PUSH_DEFAULT); + if (effectiveRemote == null && branchCfg != null) { + effectiveRemote = branchCfg.getRemote(); + } + } + if (effectiveRemote == null) { + effectiveRemote = Constants.DEFAULT_REMOTE_NAME; + } + return effectiveRemote; + } + + private String getCurrentBranch() + throws IOException, DetachedHeadException { + Ref head = repo.exactRef(Constants.HEAD); + if (head != null && head.isSymbolic()) { + return head.getLeaf().getName(); + } + throw new DetachedHeadException(); + } + + private void determineDefaultRefSpecs(Config config) + throws IOException, GitAPIException { + if (pushDefault == null) { + pushDefault = config.get(PushConfig::new).getPushDefault(); + } + switch (pushDefault) { + case CURRENT: + refSpecs.add(new RefSpec(getCurrentBranch())); + break; + case MATCHING: + refSpecs.add(new RefSpec(":")); //$NON-NLS-1$ + break; + case NOTHING: + throw new InvalidRefNameException( + JGitText.get().pushDefaultNothing); + case SIMPLE: + case UPSTREAM: + String currentBranch = getCurrentBranch(); + BranchConfig branchCfg = new BranchConfig(config, + Repository.shortenRefName(currentBranch)); + String fetchRemote = branchCfg.getRemote(); + if (fetchRemote == null) { + fetchRemote = Constants.DEFAULT_REMOTE_NAME; + } + boolean isTriangular = !fetchRemote.equals(remote); + if (isTriangular) { + if (PushDefault.UPSTREAM.equals(pushDefault)) { + throw new InvalidRefNameException(MessageFormat.format( + JGitText.get().pushDefaultTriangularUpstream, + remote, fetchRemote)); + } + // Strange, but consistent with C git: "simple" doesn't even + // check whether there is a configured upstream, and if so, that + // it is equal to the local branch name. It just becomes + // "current". + refSpecs.add(new RefSpec(currentBranch)); + } else { + String trackedBranch = branchCfg.getMerge(); + if (branchCfg.isRemoteLocal() || trackedBranch == null + || !trackedBranch.startsWith(Constants.R_HEADS)) { + throw new InvalidRefNameException(MessageFormat.format( + JGitText.get().pushDefaultNoUpstream, + currentBranch)); + } + if (PushDefault.SIMPLE.equals(pushDefault) + && !trackedBranch.equals(currentBranch)) { + throw new InvalidRefNameException(MessageFormat.format( + JGitText.get().pushDefaultSimple, currentBranch, + trackedBranch)); + } + refSpecs.add(new RefSpec(currentBranch + ':' + trackedBranch)); + } + break; + default: + throw new InvalidRefNameException(MessageFormat + .format(JGitText.get().pushDefaultUnknown, pushDefault)); + } + } + /** * The remote (uri or name) used for the push operation. If no remote is * set, the default value of <code>Constants.DEFAULT_REMOTE_NAME</code> will @@ -336,9 +444,37 @@ public class PushCommand extends } /** + * Retrieves the {@link PushDefault} currently set. + * + * @return the {@link PushDefault}, or {@code null} if not set + * @since 6.1 + */ + public PushDefault getPushDefault() { + return pushDefault; + } + + /** + * Sets an explicit {@link PushDefault}. The default used if this is not + * called is {@link PushDefault#CURRENT} for compatibility reasons with + * earlier JGit versions. + * + * @param pushDefault + * {@link PushDefault} to set; if {@code null} the value defined + * in the git config will be used. + * + * @return {@code this} + * @since 6.1 + */ + public PushCommand setPushDefault(PushDefault pushDefault) { + checkCallable(); + this.pushDefault = pushDefault; + return this; + } + + /** * Push all branches under refs/heads/*. * - * @return {code this} + * @return {@code this} */ public PushCommand setPushAll() { refSpecs.add(Transport.REFSPEC_PUSH_ALL); @@ -348,7 +484,7 @@ public class PushCommand extends /** * Push all tags under refs/tags/*. * - * @return {code this} + * @return {@code this} */ public PushCommand setPushTags() { refSpecs.add(Transport.REFSPEC_TAGS); 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 a26ffc2e66..4e0d9d78c3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java @@ -29,6 +29,7 @@ import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.api.RebaseResult.Status; import org.eclipse.jgit.api.ResetCommand.ResetType; import org.eclipse.jgit.api.errors.CheckoutConflictException; @@ -52,6 +53,8 @@ import org.eclipse.jgit.errors.RevisionSyntaxException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.AbbreviatedObjectId; import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.CommitConfig; +import org.eclipse.jgit.lib.CommitConfig.CleanupMode; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.NullProgressMonitor; @@ -205,6 +208,8 @@ public class RebaseCommand extends GitCommand<RebaseResult> { private InteractiveHandler interactiveHandler; + private CommitConfig commitConfig; + private boolean stopAfterInitialization = false; private RevCommit newHead; @@ -246,6 +251,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> { lastStepWasForward = false; checkCallable(); checkParameters(); + commitConfig = repo.getConfig().get(CommitConfig.KEY); try { switch (operation) { case ABORT: @@ -441,11 +447,17 @@ public class RebaseCommand extends GitCommand<RebaseResult> { return null; // continue rebase process on pick command case REWORD: String oldMessage = commitToPick.getFullMessage(); - String newMessage = interactiveHandler - .modifyCommitMessage(oldMessage); + CleanupMode mode = commitConfig.resolve(CleanupMode.DEFAULT, true); + boolean[] doChangeId = { false }; + String newMessage = editCommitMessage(doChangeId, oldMessage, mode, + commitConfig.getCommentChar(oldMessage)); try (Git git = new Git(repo)) { - newHead = git.commit().setMessage(newMessage).setAmend(true) - .setNoVerify(true).call(); + newHead = git.commit() + .setMessage(newMessage) + .setAmend(true) + .setNoVerify(true) + .setInsertChangeId(doChangeId[0]) + .call(); } return null; case EDIT: @@ -460,17 +472,49 @@ public class RebaseCommand extends GitCommand<RebaseResult> { resetSoftToParent(); List<RebaseTodoLine> steps = repo.readRebaseTodo( rebaseState.getPath(GIT_REBASE_TODO), false); - RebaseTodoLine nextStep = steps.isEmpty() ? null : steps.get(0); + boolean isLast = steps.isEmpty(); + if (!isLast) { + switch (steps.get(0).getAction()) { + case FIXUP: + case SQUASH: + break; + default: + isLast = true; + break; + } + } File messageFixupFile = rebaseState.getFile(MESSAGE_FIXUP); File messageSquashFile = rebaseState.getFile(MESSAGE_SQUASH); - if (isSquash && messageFixupFile.exists()) + if (isSquash && messageFixupFile.exists()) { messageFixupFile.delete(); - newHead = doSquashFixup(isSquash, commitToPick, nextStep, + } + newHead = doSquashFixup(isSquash, commitToPick, isLast, messageFixupFile, messageSquashFile); } return null; } + private String editCommitMessage(boolean[] doChangeId, String message, + @NonNull CleanupMode mode, char commentChar) { + String newMessage; + CommitConfig.CleanupMode cleanup; + if (interactiveHandler instanceof InteractiveHandler2) { + InteractiveHandler2.ModifyResult modification = ((InteractiveHandler2) interactiveHandler) + .editCommitMessage(message, mode, commentChar); + newMessage = modification.getMessage(); + cleanup = modification.getCleanupMode(); + if (CleanupMode.DEFAULT.equals(cleanup)) { + cleanup = mode; + } + doChangeId[0] = modification.shouldAddChangeId(); + } else { + newMessage = interactiveHandler.modifyCommitMessage(message); + cleanup = CommitConfig.CleanupMode.STRIP; + doChangeId[0] = false; + } + return CommitConfig.cleanText(newMessage, cleanup, commentChar); + } + private RebaseResult cherryPickCommit(RevCommit commitToPick) throws IOException, GitAPIException, NoMessageException, UnmergedPathsException, ConcurrentRefUpdateException, @@ -707,7 +751,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> { } private RevCommit doSquashFixup(boolean isSquash, RevCommit commitToPick, - RebaseTodoLine nextStep, File messageFixup, File messageSquash) + boolean isLast, File messageFixup, File messageSquash) throws IOException, GitAPIException { if (!messageSquash.exists()) { @@ -717,24 +761,20 @@ public class RebaseCommand extends GitCommand<RebaseResult> { initializeSquashFixupFile(MESSAGE_SQUASH, previousCommit.getFullMessage()); - if (!isSquash) - initializeSquashFixupFile(MESSAGE_FIXUP, - previousCommit.getFullMessage()); + if (!isSquash) { + rebaseState.createFile(MESSAGE_FIXUP, + previousCommit.getFullMessage()); + } } - String currSquashMessage = rebaseState - .readFile(MESSAGE_SQUASH); + String currSquashMessage = rebaseState.readFile(MESSAGE_SQUASH); int count = parseSquashFixupSequenceCount(currSquashMessage) + 1; String content = composeSquashMessage(isSquash, commitToPick, currSquashMessage, count); rebaseState.createFile(MESSAGE_SQUASH, content); - if (messageFixup.exists()) - rebaseState.createFile(MESSAGE_FIXUP, content); - return squashIntoPrevious( - !messageFixup.exists(), - nextStep); + return squashIntoPrevious(!messageFixup.exists(), isLast); } private void resetSoftToParent() throws IOException, @@ -756,26 +796,31 @@ public class RebaseCommand extends GitCommand<RebaseResult> { } private RevCommit squashIntoPrevious(boolean sequenceContainsSquash, - RebaseTodoLine nextStep) + boolean isLast) throws IOException, GitAPIException { RevCommit retNewHead; - String commitMessage = rebaseState - .readFile(MESSAGE_SQUASH); - + String commitMessage; + if (!isLast || sequenceContainsSquash) { + commitMessage = rebaseState.readFile(MESSAGE_SQUASH); + } else { + commitMessage = rebaseState.readFile(MESSAGE_FIXUP); + } try (Git git = new Git(repo)) { - if (nextStep == null || ((nextStep.getAction() != Action.FIXUP) - && (nextStep.getAction() != Action.SQUASH))) { - // this is the last step in this sequence + if (isLast) { + boolean[] doChangeId = { false }; if (sequenceContainsSquash) { - commitMessage = interactiveHandler - .modifyCommitMessage(commitMessage); + char commentChar = commitMessage.charAt(0); + commitMessage = editCommitMessage(doChangeId, commitMessage, + CleanupMode.STRIP, commentChar); } retNewHead = git.commit() - .setMessage(stripCommentLines(commitMessage)) - .setAmend(true).setNoVerify(true).call(); + .setMessage(commitMessage) + .setAmend(true) + .setNoVerify(true) + .setInsertChangeId(doChangeId[0]) + .call(); rebaseState.getFile(MESSAGE_SQUASH).delete(); rebaseState.getFile(MESSAGE_FIXUP).delete(); - } else { // Next step is either Squash or Fixup retNewHead = git.commit().setMessage(commitMessage) @@ -785,46 +830,61 @@ public class RebaseCommand extends GitCommand<RebaseResult> { return retNewHead; } - private static String stripCommentLines(String commitMessage) { - StringBuilder result = new StringBuilder(); - for (String line : commitMessage.split("\n")) { //$NON-NLS-1$ - if (!line.trim().startsWith("#")) //$NON-NLS-1$ - result.append(line).append("\n"); //$NON-NLS-1$ - } - if (!commitMessage.endsWith("\n")) { //$NON-NLS-1$ - int bufferSize = result.length(); - if (bufferSize > 0 && result.charAt(bufferSize - 1) == '\n') { - result.deleteCharAt(bufferSize - 1); - } - } - return result.toString(); - } - @SuppressWarnings("nls") - private static String composeSquashMessage(boolean isSquash, + private String composeSquashMessage(boolean isSquash, RevCommit commitToPick, String currSquashMessage, int count) { StringBuilder sb = new StringBuilder(); String ordinal = getOrdinal(count); - sb.setLength(0); - sb.append("# This is a combination of ").append(count) - .append(" commits.\n"); - // Add the previous message without header (i.e first line) - sb.append(currSquashMessage - .substring(currSquashMessage.indexOf('\n') + 1)); - sb.append("\n"); - if (isSquash) { - sb.append("# This is the ").append(count).append(ordinal) - .append(" commit message:\n"); - sb.append(commitToPick.getFullMessage()); + // currSquashMessage is always non-empty here, and the first character + // is the comment character used so far. + char commentChar = currSquashMessage.charAt(0); + String newMessage = commitToPick.getFullMessage(); + if (!isSquash) { + sb.append(commentChar).append(" This is a combination of ") + .append(count).append(" commits.\n"); + // Add the previous message without header (i.e first line) + sb.append(currSquashMessage + .substring(currSquashMessage.indexOf('\n') + 1)); + sb.append('\n'); + sb.append(commentChar).append(" The ").append(count).append(ordinal) + .append(" commit message will be skipped:\n") + .append(commentChar).append(' '); + sb.append(newMessage.replaceAll("([\n\r])", + "$1" + commentChar + ' ')); } else { - sb.append("# The ").append(count).append(ordinal) - .append(" commit message will be skipped:\n# "); - sb.append(commitToPick.getFullMessage().replaceAll("([\n\r])", - "$1# ")); + String currentMessage = currSquashMessage; + if (commitConfig.isAutoCommentChar()) { + // Figure out a new comment character taking into account the + // new message + String cleaned = CommitConfig.cleanText(currentMessage, + CommitConfig.CleanupMode.STRIP, commentChar) + '\n' + + newMessage; + char newCommentChar = commitConfig.getCommentChar(cleaned); + if (newCommentChar != commentChar) { + currentMessage = replaceCommentChar(currentMessage, + commentChar, newCommentChar); + commentChar = newCommentChar; + } + } + sb.append(commentChar).append(" This is a combination of ") + .append(count).append(" commits.\n"); + // Add the previous message without header (i.e first line) + sb.append( + currentMessage.substring(currentMessage.indexOf('\n') + 1)); + sb.append('\n'); + sb.append(commentChar).append(" This is the ").append(count) + .append(ordinal).append(" commit message:\n"); + sb.append(newMessage); } return sb.toString(); } + private String replaceCommentChar(String message, char oldChar, + char newChar) { + // (?m) - Switch on multi-line matching; \h - horizontal whitespace + return message.replaceAll("(?m)^(\\h*)" + oldChar, "$1" + newChar); //$NON-NLS-1$ //$NON-NLS-2$ + } + private static String getOrdinal(int count) { switch (count % 10) { case 1: @@ -858,10 +918,11 @@ public class RebaseCommand extends GitCommand<RebaseResult> { private void initializeSquashFixupFile(String messageFile, String fullMessage) throws IOException { - rebaseState - .createFile( - messageFile, - "# This is a combination of 1 commits.\n# The first commit's message is:\n" + fullMessage); //$NON-NLS-1$); + char commentChar = commitConfig.getCommentChar(fullMessage); + rebaseState.createFile(messageFile, + commentChar + " This is a combination of 1 commits.\n" //$NON-NLS-1$ + + commentChar + " The first commit's message is:\n" //$NON-NLS-1$ + + fullMessage); } private String getOurCommitName() { @@ -1625,26 +1686,106 @@ public class RebaseCommand extends GitCommand<RebaseResult> { } /** - * Allows configure rebase interactive process and modify commit message + * Allows to configure the interactive rebase process steps and to modify + * commit messages. */ public interface InteractiveHandler { + /** - * Given list of {@code steps} should be modified according to user - * rebase configuration + * Callback API to modify the initial list of interactive rebase steps. + * * @param steps - * initial configuration of rebase interactive + * initial configuration of interactive rebase */ void prepareSteps(List<RebaseTodoLine> steps); /** - * Used for editing commit message on REWORD + * Used for editing commit message on REWORD or SQUASH. * - * @param commit + * @param message + * existing commit message * @return new commit message */ - String modifyCommitMessage(String commit); + String modifyCommitMessage(String message); } + /** + * Extends {@link InteractiveHandler} with an enhanced callback for editing + * commit messages. + * + * @since 6.1 + */ + public interface InteractiveHandler2 extends InteractiveHandler { + + /** + * Callback API for editing a commit message on REWORD or SQUASH. + * <p> + * The callback gets the comment character currently set, and the + * clean-up mode. It can use this information when presenting the + * message to the user, and it also has the possibility to clean the + * message itself (in which case the returned {@link ModifyResult} + * should have {@link CleanupMode#VERBATIM} set lest JGit cleans the + * message again). It can also override the initial clean-up mode by + * returning clean-up mode other than {@link CleanupMode#DEFAULT}. If it + * does return {@code DEFAULT}, the passed-in {@code mode} will be + * applied. + * </p> + * + * @param message + * existing commit message + * @param mode + * {@link CleanupMode} currently set + * @param commentChar + * comment character used + * @return a {@link ModifyResult} + */ + @NonNull + ModifyResult editCommitMessage(@NonNull String message, + @NonNull CleanupMode mode, char commentChar); + + @Override + default String modifyCommitMessage(String message) { + // Should actually not be called; but do something reasonable anyway + ModifyResult result = editCommitMessage( + message == null ? "" : message, CleanupMode.STRIP, //$NON-NLS-1$ + '#'); + return result.getMessage(); + } + + /** + * Describes the result of editing a commit message: the new message, + * and how it should be cleaned. + */ + interface ModifyResult { + + /** + * Retrieves the new commit message. + * + * @return the message + */ + @NonNull + String getMessage(); + + /** + * Tells how the message returned by {@link #getMessage()} should be + * cleaned. + * + * @return the {@link CleanupMode} + */ + @NonNull + CleanupMode getCleanupMode(); + + /** + * Tells whether a Gerrit Change-Id should be computed and added to + * the commit message, as with + * {@link CommitCommand#setInsertChangeId(boolean)}. + * + * @return {@code true} if a Change-Id should be handled, + * {@code false} otherwise + */ + boolean shouldAddChangeId(); + } + } PersonIdent parseAuthor(byte[] raw) { if (raw.length == 0) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RevertCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RevertCommand.java index 22ef4d0a32..513f579b67 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RevertCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RevertCommand.java @@ -9,6 +9,8 @@ */ package org.eclipse.jgit.api; +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_ABBREV_STRING_LENGTH; + import java.io.IOException; import java.text.MessageFormat; import java.util.LinkedList; @@ -28,6 +30,7 @@ import org.eclipse.jgit.dircache.DirCacheCheckout; import org.eclipse.jgit.events.WorkingTreeModifiedEvent; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.CommitConfig; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectId; @@ -128,8 +131,9 @@ public class RevertCommand extends GitCommand<RevCommit> { revWalk.parseHeaders(srcParent); String ourName = calculateOurName(headRef); - String revertName = srcCommit.getId().abbreviate(7).name() - + " " + srcCommit.getShortMessage(); //$NON-NLS-1$ + String revertName = srcCommit.getId() + .abbreviate(OBJECT_ID_ABBREV_STRING_LENGTH).name() + " " //$NON-NLS-1$ + + srcCommit.getShortMessage(); ResolveMerger merger = (ResolveMerger) strategy.newMerger(repo); merger.setWorkingTreeIterator(new FileTreeIterator(repo)); @@ -182,9 +186,12 @@ public class RevertCommand extends GitCommand<RevCommit> { MergeStatus.CONFLICTING, strategy, merger.getMergeResults(), failingPaths, null); if (!merger.failed() && !unmergedPaths.isEmpty()) { + CommitConfig config = repo.getConfig() + .get(CommitConfig.KEY); + char commentChar = config.getCommentChar(newMessage); String message = new MergeMessageFormatter() - .formatWithConflicts(newMessage, - merger.getUnmergedPaths()); + .formatWithConflicts(newMessage, + merger.getUnmergedPaths(), commentChar); repo.writeRevertHead(srcCommit.getId()); repo.writeMergeCommitMsg(message); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashCreateCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashCreateCommand.java index 35fd8992b6..f7a1f4eff8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashCreateCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashCreateCommand.java @@ -9,6 +9,8 @@ */ package org.eclipse.jgit.api; +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_ABBREV_STRING_LENGTH; + import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -302,7 +304,8 @@ public class StashCreateCommand extends GitCommand<RevCommit> { builder.setParentId(headCommit); builder.setTreeId(cache.writeTree(inserter)); builder.setMessage(MessageFormat.format(indexMessage, branch, - headCommit.abbreviate(7).name(), + headCommit.abbreviate(OBJECT_ID_ABBREV_STRING_LENGTH) + .name(), headCommit.getShortMessage())); ObjectId indexCommit = inserter.insert(builder); @@ -319,7 +322,10 @@ public class StashCreateCommand extends GitCommand<RevCommit> { builder.setParentIds(new ObjectId[0]); builder.setTreeId(untrackedDirCache.writeTree(inserter)); builder.setMessage(MessageFormat.format(MSG_UNTRACKED, - branch, headCommit.abbreviate(7).name(), + branch, + headCommit + .abbreviate(OBJECT_ID_ABBREV_STRING_LENGTH) + .name(), headCommit.getShortMessage())); untrackedCommit = inserter.insert(builder); } @@ -339,7 +345,8 @@ public class StashCreateCommand extends GitCommand<RevCommit> { builder.addParentId(untrackedCommit); builder.setMessage(MessageFormat.format( workingDirectoryMessage, branch, - headCommit.abbreviate(7).name(), + headCommit.abbreviate(OBJECT_ID_ABBREV_STRING_LENGTH) + .name(), headCommit.getShortMessage())); builder.setTreeId(cache.writeTree(inserter)); commitId = inserter.insert(builder); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesHandler.java b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesHandler.java index 638dd827ed..7ec78597fa 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesHandler.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesHandler.java @@ -1,43 +1,11 @@ /* - * Copyright (C) 2015, Ivan Motsch <ivan.motsch@bsiag.com> + * Copyright (C) 2015, 2022 Ivan Motsch <ivan.motsch@bsiag.com> and others * - * 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 + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://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. + * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.attributes; @@ -46,6 +14,7 @@ import java.util.HashMap; import java.util.List; import java.util.ListIterator; import java.util.Map; +import java.util.function.Supplier; import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.attributes.Attribute.State; @@ -84,6 +53,8 @@ public class AttributesHandler { private final TreeWalk treeWalk; + private final Supplier<CanonicalTreeParser> attributesTree; + private final AttributesNode globalNode; private final AttributesNode infoNode; @@ -98,22 +69,41 @@ public class AttributesHandler { * @param treeWalk * a {@link org.eclipse.jgit.treewalk.TreeWalk} * @throws java.io.IOException + * @deprecated since 6.1, use {@link #AttributesHandler(TreeWalk, Supplier)} + * instead */ + @Deprecated public AttributesHandler(TreeWalk treeWalk) throws IOException { + this(treeWalk, () -> treeWalk.getTree(CanonicalTreeParser.class)); + } + + /** + * Create an {@link org.eclipse.jgit.attributes.AttributesHandler} with + * default rules as well as merged rules from global, info and worktree root + * attributes + * + * @param treeWalk + * a {@link org.eclipse.jgit.treewalk.TreeWalk} + * @param attributesTree + * the tree to read .gitattributes from + * @throws java.io.IOException + * @since 6.1 + */ + public AttributesHandler(TreeWalk treeWalk, + Supplier<CanonicalTreeParser> attributesTree) throws IOException { this.treeWalk = treeWalk; - AttributesNodeProvider attributesNodeProvider =treeWalk.getAttributesNodeProvider(); + this.attributesTree = attributesTree; + AttributesNodeProvider attributesNodeProvider = treeWalk + .getAttributesNodeProvider(); this.globalNode = attributesNodeProvider != null ? attributesNodeProvider.getGlobalAttributesNode() : null; this.infoNode = attributesNodeProvider != null ? attributesNodeProvider.getInfoAttributesNode() : null; AttributesNode rootNode = attributesNode(treeWalk, - rootOf( - treeWalk.getTree(WorkingTreeIterator.class)), - rootOf( - treeWalk.getTree(DirCacheIterator.class)), - rootOf(treeWalk - .getTree(CanonicalTreeParser.class))); + rootOf(treeWalk.getTree(WorkingTreeIterator.class)), + rootOf(treeWalk.getTree(DirCacheIterator.class)), + rootOf(attributesTree.get())); expansions.put(BINARY_RULE_KEY, BINARY_RULE_ATTRIBUTES); for (AttributesNode node : new AttributesNode[] { globalNode, rootNode, @@ -152,7 +142,7 @@ public class AttributesHandler { isDirectory, treeWalk.getTree(WorkingTreeIterator.class), treeWalk.getTree(DirCacheIterator.class), - treeWalk.getTree(CanonicalTreeParser.class), + attributesTree.get(), attributes); // Gets the attributes located in the global attribute file diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameGenerator.java index 10d77528f6..77967df2e5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameGenerator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameGenerator.java @@ -41,6 +41,7 @@ import org.eclipse.jgit.dircache.DirCacheEntry; import org.eclipse.jgit.dircache.DirCacheIterator; import org.eclipse.jgit.errors.NoWorkTreeException; import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.internal.diff.FilteredRenameDetector; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.MutableObjectId; @@ -1109,9 +1110,10 @@ public class BlameGenerator implements AutoCloseable { treeWalk.setFilter(TreeFilter.ANY_DIFF); treeWalk.reset(parent.getTree(), commit.getTree()); - renameDetector.reset(); - renameDetector.addAll(DiffEntry.scan(treeWalk)); - for (DiffEntry ent : renameDetector.compute()) { + List<DiffEntry> diffs = DiffEntry.scan(treeWalk); + FilteredRenameDetector filteredRenameDetector = new FilteredRenameDetector( + renameDetector); + for (DiffEntry ent : filteredRenameDetector.compute(diffs, path)) { if (isRename(ent) && ent.getNewPath().equals(path.getPath())) return ent; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/ContentSource.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/ContentSource.java index 1a41df3d0a..64ff19c9c3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/ContentSource.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/ContentSource.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010, 2020 Google Inc. and others + * Copyright (C) 2010, 2021 Google Inc. and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -91,6 +91,29 @@ public abstract class ContentSource { public abstract ObjectLoader open(String path, ObjectId id) throws IOException; + /** + * Closes the used resources like ObjectReader, TreeWalk etc. Default + * implementation does nothing. + * + * @since 6.2 + */ + public void close() { + // Do nothing + } + + /** + * Checks if the source is from "working tree", so it can be accessed as a + * file directly. + * + * @since 6.2 + * + * @return true if working tree source and false otherwise (loader must be + * used) + */ + public boolean isWorkingTreeSource() { + return false; + } + private static class ObjectReaderSource extends ContentSource { private final ObjectReader reader; @@ -111,6 +134,16 @@ public abstract class ContentSource { public ObjectLoader open(String path, ObjectId id) throws IOException { return reader.open(id, Constants.OBJ_BLOB); } + + @Override + public void close() { + reader.close(); + } + + @Override + public boolean isWorkingTreeSource() { + return false; + } } private static class WorkingTreeSource extends ContentSource { @@ -194,6 +227,16 @@ public abstract class ContentSource { throw new FileNotFoundException(path); } } + + @Override + public void close() { + tw.close(); + } + + @Override + public boolean isWorkingTreeSource() { + return true; + } } /** A pair of sources to access the old and new sides of a DiffEntry. */ @@ -261,5 +304,37 @@ public abstract class ContentSource { throw new IllegalArgumentException(); } } + + /** + * Closes used resources. + * + * @since 6.2 + */ + public void close() { + oldSource.close(); + newSource.close(); + } + + /** + * Checks if source (side) is a "working tree". + * + * @since 6.2 + * + * @param side + * which side of the entry to read (OLD or NEW). + * @return is the source a "working tree" + * + */ + public boolean isWorkingTreeSource(DiffEntry.Side side) { + switch (side) { + case OLD: + return oldSource.isWorkingTreeSource(); + case NEW: + return newSource.isWorkingTreeSource(); + default: + throw new IllegalArgumentException(); + } + } + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java index 49da95c9ab..1a5f74f98a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java @@ -18,6 +18,7 @@ import static org.eclipse.jgit.diff.DiffEntry.ChangeType.MODIFY; import static org.eclipse.jgit.diff.DiffEntry.ChangeType.RENAME; import static org.eclipse.jgit.diff.DiffEntry.Side.NEW; import static org.eclipse.jgit.diff.DiffEntry.Side.OLD; +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_ABBREV_STRING_LENGTH; import static org.eclipse.jgit.lib.Constants.encode; import static org.eclipse.jgit.lib.Constants.encodeASCII; import static org.eclipse.jgit.lib.FileMode.GITLINK; @@ -90,7 +91,7 @@ public class DiffFormatter implements AutoCloseable { private int context = 3; - private int abbreviationLength = 7; + private int abbreviationLength = OBJECT_ID_ABBREV_STRING_LENGTH; private DiffAlgorithm diffAlgorithm; 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 c904a782db..f6fc393c45 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java @@ -4,7 +4,8 @@ * Copyright (C) 2008, Roger C. Soares <rogersoares@intelinet.com.br> * Copyright (C) 2006, Shawn O. Pearce <spearce@spearce.org> * Copyright (C) 2010, Chrisian Halstrick <christian.halstrick@sap.com> - * Copyright (C) 2019-2020, Andre Bossert <andre.bossert@siemens.com> + * Copyright (C) 2019, 2020, Andre Bossert <andre.bossert@siemens.com> + * Copyright (C) 2017, 2022, Thomas Wolf <thomas.wolf@paranor.ch> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -25,9 +26,9 @@ import java.nio.file.StandardCopyOption; import java.text.MessageFormat; import java.time.Instant; import java.util.ArrayList; -import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -112,7 +113,7 @@ public class DirCacheCheckout { private Repository repo; - private HashMap<String, CheckoutMetadata> updated = new HashMap<>(); + private Map<String, CheckoutMetadata> updated = new LinkedHashMap<>(); private ArrayList<String> conflicts = new ArrayList<>(); @@ -299,7 +300,7 @@ public class DirCacheCheckout { walk = new NameConflictTreeWalk(repo); builder = dc.builder(); - addTree(walk, headCommitTree); + walk.setHead(addTree(walk, headCommitTree)); addTree(walk, mergeCommitTree); int dciPos = walk.addTree(new DirCacheBuildIterator(builder)); walk.addTree(workingTree); @@ -315,13 +316,6 @@ public class DirCacheCheckout { } } - private void addTree(TreeWalk tw, ObjectId id) throws MissingObjectException, IncorrectObjectTypeException, IOException { - if (id == null) - tw.addTree(new EmptyTreeIterator()); - else - tw.addTree(id); - } - /** * Scan index and merge tree (no HEAD). Used e.g. for initial checkout when * there is no head yet. @@ -341,7 +335,7 @@ public class DirCacheCheckout { builder = dc.builder(); walk = new NameConflictTreeWalk(repo); - addTree(walk, mergeCommitTree); + walk.setHead(addTree(walk, mergeCommitTree)); int dciPos = walk.addTree(new DirCacheBuildIterator(builder)); walk.addTree(workingTree); workingTree.setDirCacheIterator(walk, dciPos); @@ -356,6 +350,14 @@ public class DirCacheCheckout { conflicts.removeAll(removed); } + private int addTree(TreeWalk tw, ObjectId id) throws MissingObjectException, + IncorrectObjectTypeException, IOException { + if (id == null) { + return tw.addTree(new EmptyTreeIterator()); + } + return tw.addTree(id); + } + /** * Processing an entry in the context of {@link #prescanOneTree()} when only * one tree is given @@ -382,17 +384,14 @@ public class DirCacheCheckout { // failOnConflict is false. Putting something to conflicts // would mean we delete it. Instead we want the mergeCommit // content to be checked out. - update(m.getEntryPathString(), m.getEntryObjectId(), - m.getEntryFileMode()); + update(m); } } else - update(m.getEntryPathString(), m.getEntryObjectId(), - m.getEntryFileMode()); + update(m); } else if (f == null || !m.idEqual(i)) { // The working tree file is missing or the merge content differs // from index content - update(m.getEntryPathString(), m.getEntryObjectId(), - m.getEntryFileMode()); + update(m); } else if (i.getDirCacheEntry() != null) { // The index contains a file (and not a folder) if (f.isModified(i.getDirCacheEntry(), true, @@ -400,8 +399,7 @@ public class DirCacheCheckout { || i.getDirCacheEntry().getStage() != 0) // The working tree file is dirty or the index contains a // conflict - update(m.getEntryPathString(), m.getEntryObjectId(), - m.getEntryFileMode()); + update(m); else { // update the timestamp of the index with the one from the // file if not set, as we are sure to be in sync here. @@ -802,7 +800,7 @@ public class DirCacheCheckout { if (f != null && isModifiedSubtree_IndexWorkingtree(name)) { conflict(name, dce, h, m); // 1 } else { - update(name, mId, mMode); // 2 + update(1, name, mId, mMode); // 2 } break; @@ -828,7 +826,7 @@ public class DirCacheCheckout { // are found later break; case 0xD0F: // 19 - update(name, mId, mMode); + update(1, name, mId, mMode); break; case 0xDF0: // conflict without a rule case 0x0FD: // 15 @@ -839,7 +837,7 @@ public class DirCacheCheckout { if (isModifiedSubtree_IndexWorkingtree(name)) conflict(name, dce, h, m); // 8 else - update(name, mId, mMode); // 7 + update(1, name, mId, mMode); // 7 } else conflict(name, dce, h, m); // 9 break; @@ -859,7 +857,7 @@ public class DirCacheCheckout { break; case 0x0DF: // 16 17 if (!isModifiedSubtree_IndexWorkingtree(name)) - update(name, mId, mMode); + update(1, name, mId, mMode); else conflict(name, dce, h, m); break; @@ -929,7 +927,7 @@ public class DirCacheCheckout { // At least one of Head, Index, Merge is not empty // -> only Merge contains something for this path. Use it! // Potentially update the file - update(name, mId, mMode); // 1 + update(1, name, mId, mMode); // 1 else if (m == null) // Nothing in Merge // Something in Head @@ -947,7 +945,7 @@ public class DirCacheCheckout { // find in Merge. Potentially updates the file. if (equalIdAndMode(hId, hMode, mId, mMode)) { if (initialCheckout || force) { - update(name, mId, mMode); + update(1, name, mId, mMode); } else { keep(name, dce, f); } @@ -1131,7 +1129,7 @@ public class DirCacheCheckout { // TODO check that we don't overwrite some unsaved // file content - update(name, mId, mMode); + update(1, name, mId, mMode); } else if (dce != null && (f != null && f.isModified(dce, true, this.walk.getObjectReader()))) { @@ -1150,7 +1148,7 @@ public class DirCacheCheckout { // -> Standard case when switching between branches: // Nothing new in index but something different in // Merge. Update index and file - update(name, mId, mMode); + update(1, name, mId, mMode); } } else { // Head differs from index or merge is same as index @@ -1237,12 +1235,17 @@ public class DirCacheCheckout { removed.add(path); } - private void update(String path, ObjectId mId, FileMode mode) - throws IOException { + private void update(CanonicalTreeParser tree) throws IOException { + update(0, tree.getEntryPathString(), tree.getEntryObjectId(), + tree.getEntryFileMode()); + } + + private void update(int index, String path, ObjectId mId, + FileMode mode) throws IOException { if (!FileMode.TREE.equals(mode)) { updated.put(path, new CheckoutMetadata( - walk.getEolStreamType(CHECKOUT_OP), - walk.getFilterCommand(Constants.ATTR_FILTER_TYPE_SMUDGE))); + walk.getCheckoutEolStreamType(index), + walk.getSmudgeCommand(index))); DirCacheEntry entry = new DirCacheEntry(path, DirCacheEntry.STAGE_0); entry.setObjectId(mId); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/NoRemoteRepositoryException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/NoRemoteRepositoryException.java index 58f70f5d4b..1dd976cec9 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/errors/NoRemoteRepositoryException.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/NoRemoteRepositoryException.java @@ -29,4 +29,19 @@ public class NoRemoteRepositoryException extends TransportException { public NoRemoteRepositoryException(URIish uri, String s) { super(uri, s); } + + /** + * Constructs an exception indicating a repository does not exist. + * + * @param uri + * URI used for transport + * @param s + * message + * @param cause + * root cause exception + * @since 5.13 + */ + public NoRemoteRepositoryException(URIish uri, String s, Throwable cause) { + super(uri, s, cause); + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/BareSuperprojectWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/BareSuperprojectWriter.java new file mode 100644 index 0000000000..e6626aece3 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/BareSuperprojectWriter.java @@ -0,0 +1,319 @@ +/* + * Copyright (C) 2021, Google Inc. and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.gitrepo; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.eclipse.jgit.lib.Constants.R_TAGS; + +import java.io.IOException; +import java.net.URI; +import java.text.MessageFormat; +import java.util.List; + +import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.JGitInternalException; +import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.dircache.DirCacheBuilder; +import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.gitrepo.RepoCommand.ManifestErrorException; +import org.eclipse.jgit.gitrepo.RepoCommand.RemoteFile; +import org.eclipse.jgit.gitrepo.RepoCommand.RemoteReader; +import org.eclipse.jgit.gitrepo.RepoCommand.RemoteUnavailableException; +import org.eclipse.jgit.gitrepo.RepoProject.CopyFile; +import org.eclipse.jgit.gitrepo.RepoProject.LinkFile; +import org.eclipse.jgit.gitrepo.internal.RepoText; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.Config; +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.PersonIdent; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.RefUpdate.Result; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.util.FileUtils; + +/** + * Writes .gitmodules and gitlinks of parsed manifest projects into a bare + * repository. + * + * To write on a regular repository, see {@link RegularSuperprojectWriter}. + */ +class BareSuperprojectWriter { + private static final int LOCK_FAILURE_MAX_RETRIES = 5; + + // Retry exponentially with delays in this range + private static final int LOCK_FAILURE_MIN_RETRY_DELAY_MILLIS = 50; + + private static final int LOCK_FAILURE_MAX_RETRY_DELAY_MILLIS = 5000; + + private final Repository repo; + + private final URI targetUri; + + private final String targetBranch; + + private final RemoteReader callback; + + private final BareWriterConfig config; + + private final PersonIdent author; + + private List<ExtraContent> extraContents; + + static class BareWriterConfig { + boolean ignoreRemoteFailures = false; + + boolean recordRemoteBranch = true; + + boolean recordSubmoduleLabels = true; + + boolean recordShallowSubmodules = true; + + static BareWriterConfig getDefault() { + return new BareWriterConfig(); + } + + private BareWriterConfig() { + } + } + + static class ExtraContent { + final String path; + + final String content; + + ExtraContent(String path, String content) { + this.path = path; + this.content = content; + } + } + + BareSuperprojectWriter(Repository repo, URI targetUri, + String targetBranch, + PersonIdent author, RemoteReader callback, + BareWriterConfig config, + List<ExtraContent> extraContents) { + assert (repo.isBare()); + this.repo = repo; + this.targetUri = targetUri; + this.targetBranch = targetBranch; + this.author = author; + this.callback = callback; + this.config = config; + this.extraContents = extraContents; + } + + RevCommit write(List<RepoProject> repoProjects) + throws GitAPIException { + DirCache index = DirCache.newInCore(); + ObjectInserter inserter = repo.newObjectInserter(); + + try (RevWalk rw = new RevWalk(repo)) { + prepareIndex(repoProjects, index, inserter); + ObjectId treeId = index.writeTree(inserter); + long prevDelay = 0; + for (int i = 0; i < LOCK_FAILURE_MAX_RETRIES - 1; i++) { + try { + return commitTreeOnCurrentTip(inserter, rw, treeId); + } catch (ConcurrentRefUpdateException e) { + prevDelay = FileUtils.delay(prevDelay, + LOCK_FAILURE_MIN_RETRY_DELAY_MILLIS, + LOCK_FAILURE_MAX_RETRY_DELAY_MILLIS); + Thread.sleep(prevDelay); + repo.getRefDatabase().refresh(); + } + } + // In the last try, just propagate the exceptions + return commitTreeOnCurrentTip(inserter, rw, treeId); + } catch (IOException | InterruptedException e) { + throw new ManifestErrorException(e); + } + } + + private void prepareIndex(List<RepoProject> projects, DirCache index, + ObjectInserter inserter) throws IOException, GitAPIException { + Config cfg = new Config(); + StringBuilder attributes = new StringBuilder(); + DirCacheBuilder builder = index.builder(); + for (RepoProject proj : projects) { + String name = proj.getName(); + String path = proj.getPath(); + String url = proj.getUrl(); + ObjectId objectId; + if (ObjectId.isId(proj.getRevision())) { + objectId = ObjectId.fromString(proj.getRevision()); + } else { + objectId = callback.sha1(url, proj.getRevision()); + if (objectId == null && !config.ignoreRemoteFailures) { + throw new RemoteUnavailableException(url); + } + if (config.recordRemoteBranch) { + // "branch" field is only for non-tag references. + // Keep tags in "ref" field as hint for other tools. + String field = proj.getRevision().startsWith(R_TAGS) ? "ref" //$NON-NLS-1$ + : "branch"; //$NON-NLS-1$ + cfg.setString("submodule", name, field, //$NON-NLS-1$ + proj.getRevision()); + } + + if (config.recordShallowSubmodules + && proj.getRecommendShallow() != null) { + // The shallow recommendation is losing information. + // As the repo manifests stores the recommended + // depth in the 'clone-depth' field, while + // git core only uses a binary 'shallow = true/false' + // hint, we'll map any depth to 'shallow = true' + cfg.setBoolean("submodule", name, "shallow", //$NON-NLS-1$ //$NON-NLS-2$ + true); + } + } + if (config.recordSubmoduleLabels) { + StringBuilder rec = new StringBuilder(); + rec.append("/"); //$NON-NLS-1$ + rec.append(path); + for (String group : proj.getGroups()) { + rec.append(" "); //$NON-NLS-1$ + rec.append(group); + } + rec.append("\n"); //$NON-NLS-1$ + attributes.append(rec.toString()); + } + + URI submodUrl = URI.create(url); + if (targetUri != null) { + submodUrl = RepoCommand.relativize(targetUri, submodUrl); + } + cfg.setString("submodule", name, "path", path); //$NON-NLS-1$ //$NON-NLS-2$ + cfg.setString("submodule", name, "url", //$NON-NLS-1$ //$NON-NLS-2$ + submodUrl.toString()); + + // create gitlink + if (objectId != null) { + DirCacheEntry dcEntry = new DirCacheEntry(path); + dcEntry.setObjectId(objectId); + dcEntry.setFileMode(FileMode.GITLINK); + builder.add(dcEntry); + + for (CopyFile copyfile : proj.getCopyFiles()) { + RemoteFile rf = callback.readFileWithMode(url, + proj.getRevision(), copyfile.src); + objectId = inserter.insert(Constants.OBJ_BLOB, + rf.getContents()); + dcEntry = new DirCacheEntry(copyfile.dest); + dcEntry.setObjectId(objectId); + dcEntry.setFileMode(rf.getFileMode()); + builder.add(dcEntry); + } + for (LinkFile linkfile : proj.getLinkFiles()) { + String link; + if (linkfile.dest.contains("/")) { //$NON-NLS-1$ + link = FileUtils.relativizeGitPath( + linkfile.dest.substring(0, + linkfile.dest.lastIndexOf('/')), + proj.getPath() + "/" + linkfile.src); //$NON-NLS-1$ + } else { + link = proj.getPath() + "/" + linkfile.src; //$NON-NLS-1$ + } + + objectId = inserter.insert(Constants.OBJ_BLOB, + link.getBytes(UTF_8)); + dcEntry = new DirCacheEntry(linkfile.dest); + dcEntry.setObjectId(objectId); + dcEntry.setFileMode(FileMode.SYMLINK); + builder.add(dcEntry); + } + } + } + String content = cfg.toText(); + + // create a new DirCacheEntry for .gitmodules file. + DirCacheEntry dcEntry = new DirCacheEntry( + Constants.DOT_GIT_MODULES); + ObjectId objectId = inserter.insert(Constants.OBJ_BLOB, + content.getBytes(UTF_8)); + dcEntry.setObjectId(objectId); + dcEntry.setFileMode(FileMode.REGULAR_FILE); + builder.add(dcEntry); + + if (config.recordSubmoduleLabels) { + // create a new DirCacheEntry for .gitattributes file. + DirCacheEntry dcEntryAttr = new DirCacheEntry( + Constants.DOT_GIT_ATTRIBUTES); + ObjectId attrId = inserter.insert(Constants.OBJ_BLOB, + attributes.toString().getBytes(UTF_8)); + dcEntryAttr.setObjectId(attrId); + dcEntryAttr.setFileMode(FileMode.REGULAR_FILE); + builder.add(dcEntryAttr); + } + + for (ExtraContent ec : extraContents) { + DirCacheEntry extraDcEntry = new DirCacheEntry(ec.path); + + ObjectId oid = inserter.insert(Constants.OBJ_BLOB, + ec.content.getBytes(UTF_8)); + extraDcEntry.setObjectId(oid); + extraDcEntry.setFileMode(FileMode.REGULAR_FILE); + builder.add(extraDcEntry); + } + + builder.finish(); + } + + private RevCommit commitTreeOnCurrentTip(ObjectInserter inserter, + RevWalk rw, ObjectId treeId) + throws IOException, ConcurrentRefUpdateException { + ObjectId headId = repo.resolve(targetBranch + "^{commit}"); //$NON-NLS-1$ + if (headId != null + && rw.parseCommit(headId).getTree().getId().equals(treeId)) { + // No change. Do nothing. + return rw.parseCommit(headId); + } + + CommitBuilder commit = new CommitBuilder(); + commit.setTreeId(treeId); + if (headId != null) { + commit.setParentIds(headId); + } + commit.setAuthor(author); + commit.setCommitter(author); + commit.setMessage(RepoText.get().repoCommitMessage); + + ObjectId commitId = inserter.insert(commit); + inserter.flush(); + + RefUpdate ru = repo.updateRef(targetBranch); + ru.setNewObjectId(commitId); + ru.setExpectedOldObjectId(headId != null ? headId : ObjectId.zeroId()); + Result rc = ru.update(rw); + switch (rc) { + case NEW: + case FORCED: + case FAST_FORWARD: + // Successful. Do nothing. + break; + case REJECTED: + case LOCK_FAILURE: + throw new ConcurrentRefUpdateException(MessageFormat.format( + JGitText.get().cannotLock, targetBranch), ru.getRef(), rc); + default: + throw new JGitInternalException( + MessageFormat.format(JGitText.get().updatingRefFailed, + targetBranch, commitId.name(), rc)); + } + + return rw.parseCommit(commitId); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RegularSuperprojectWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RegularSuperprojectWriter.java new file mode 100644 index 0000000000..afab9943a7 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RegularSuperprojectWriter.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2021, Google Inc. and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.gitrepo; + +import static org.eclipse.jgit.lib.Constants.DEFAULT_REMOTE_NAME; +import static org.eclipse.jgit.lib.Constants.R_REMOTES; + +import java.io.IOException; +import java.util.List; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.SubmoduleAddCommand; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.gitrepo.RepoCommand.ManifestErrorException; +import org.eclipse.jgit.gitrepo.RepoProject.CopyFile; +import org.eclipse.jgit.gitrepo.RepoProject.LinkFile; +import org.eclipse.jgit.gitrepo.internal.RepoText; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ProgressMonitor; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; + +/** + * Writes .gitmodules and gitlinks of parsed manifest projects into a regular + * repository (using git submodule commands) + * + * To write on a bare repository, use {@link BareSuperprojectWriter} + */ +class RegularSuperprojectWriter { + + private Repository repo; + + private ProgressMonitor monitor; + + RegularSuperprojectWriter(Repository repo, ProgressMonitor monitor) { + this.repo = repo; + this.monitor = monitor; + } + + RevCommit write(List<RepoProject> repoProjects) + throws GitAPIException { + try (Git git = new Git(repo)) { + for (RepoProject proj : repoProjects) { + addSubmodule(proj.getName(), proj.getUrl(), proj.getPath(), + proj.getRevision(), proj.getCopyFiles(), + proj.getLinkFiles(), git); + } + return git.commit().setMessage(RepoText.get().repoCommitMessage) + .call(); + } catch (IOException e) { + throw new ManifestErrorException(e); + } + } + + private void addSubmodule(String name, String url, String path, + String revision, List<CopyFile> copyfiles, List<LinkFile> linkfiles, + Git git) throws GitAPIException, IOException { + assert (!repo.isBare()); + assert (git != null); + if (!linkfiles.isEmpty()) { + throw new UnsupportedOperationException( + JGitText.get().nonBareLinkFilesNotSupported); + } + + SubmoduleAddCommand add = git.submoduleAdd().setName(name).setPath(path) + .setURI(url); + if (monitor != null) { + add.setProgressMonitor(monitor); + } + + Repository subRepo = add.call(); + if (revision != null) { + try (Git sub = new Git(subRepo)) { + sub.checkout().setName(findRef(revision, subRepo)).call(); + } + subRepo.close(); + git.add().addFilepattern(path).call(); + } + for (CopyFile copyfile : copyfiles) { + copyfile.copy(); + git.add().addFilepattern(copyfile.dest).call(); + } + } + + private static String findRef(String ref, Repository repo) + throws IOException { + if (!ObjectId.isId(ref)) { + Ref r = repo.exactRef(R_REMOTES + DEFAULT_REMOTE_NAME + "/" + ref); //$NON-NLS-1$ + if (r != null) { + return r.getName(); + } + } + return ref; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java index e0a822479f..6e943e5d36 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java @@ -9,11 +9,6 @@ */ package org.eclipse.jgit.gitrepo; -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.eclipse.jgit.lib.Constants.DEFAULT_REMOTE_NAME; -import static org.eclipse.jgit.lib.Constants.R_REMOTES; -import static org.eclipse.jgit.lib.Constants.R_TAGS; - import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -31,34 +26,21 @@ import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.GitCommand; -import org.eclipse.jgit.api.SubmoduleAddCommand; -import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.InvalidRefNameException; -import org.eclipse.jgit.api.errors.JGitInternalException; -import org.eclipse.jgit.dircache.DirCache; -import org.eclipse.jgit.dircache.DirCacheBuilder; -import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.gitrepo.BareSuperprojectWriter.ExtraContent; import org.eclipse.jgit.gitrepo.ManifestParser.IncludedFileReader; -import org.eclipse.jgit.gitrepo.RepoProject.CopyFile; -import org.eclipse.jgit.gitrepo.RepoProject.LinkFile; import org.eclipse.jgit.gitrepo.internal.RepoText; import org.eclipse.jgit.internal.JGitText; -import org.eclipse.jgit.lib.CommitBuilder; -import org.eclipse.jgit.lib.Config; 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.PersonIdent; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefDatabase; -import org.eclipse.jgit.lib.RefUpdate; -import org.eclipse.jgit.lib.RefUpdate.Result; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; -import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.util.FileUtils; @@ -80,12 +62,7 @@ import org.eclipse.jgit.util.FileUtils; * @since 3.4 */ public class RepoCommand extends GitCommand<RevCommit> { - private static final int LOCK_FAILURE_MAX_RETRIES = 5; - - // Retry exponentially with delays in this range - private static final int LOCK_FAILURE_MIN_RETRY_DELAY_MILLIS = 50; - private static final int LOCK_FAILURE_MAX_RETRY_DELAY_MILLIS = 5000; private String manifestPath; private String baseUri; @@ -93,17 +70,18 @@ public class RepoCommand extends GitCommand<RevCommit> { private String groupsParam; private String branch; private String targetBranch = Constants.HEAD; - private boolean recordRemoteBranch = true; - private boolean recordSubmoduleLabels = true; - private boolean recordShallowSubmodules = true; private PersonIdent author; private RemoteReader callback; private InputStream inputStream; private IncludedFileReader includedReader; - private boolean ignoreRemoteFailures = false; + + private BareSuperprojectWriter.BareWriterConfig bareWriterConfig = BareSuperprojectWriter.BareWriterConfig + .getDefault(); private ProgressMonitor monitor; + private final List<ExtraContent> extraContents = new ArrayList<>(); + /** * A callback to get ref sha1 of a repository from its uri. * @@ -269,14 +247,14 @@ public class RepoCommand extends GitCommand<RevCommit> { } @SuppressWarnings("serial") - private static class ManifestErrorException extends GitAPIException { + static class ManifestErrorException extends GitAPIException { ManifestErrorException(Throwable cause) { super(RepoText.get().invalidManifest, cause); } } @SuppressWarnings("serial") - private static class RemoteUnavailableException extends GitAPIException { + static class RemoteUnavailableException extends GitAPIException { RemoteUnavailableException(String uri) { super(MessageFormat.format(RepoText.get().errorRemoteUnavailable, uri)); } @@ -421,7 +399,7 @@ public class RepoCommand extends GitCommand<RevCommit> { * @since 4.2 */ public RepoCommand setRecordRemoteBranch(boolean enable) { - this.recordRemoteBranch = enable; + this.bareWriterConfig.recordRemoteBranch = enable; return this; } @@ -436,7 +414,7 @@ public class RepoCommand extends GitCommand<RevCommit> { * @since 4.4 */ public RepoCommand setRecordSubmoduleLabels(boolean enable) { - this.recordSubmoduleLabels = enable; + this.bareWriterConfig.recordSubmoduleLabels = enable; return this; } @@ -451,7 +429,7 @@ public class RepoCommand extends GitCommand<RevCommit> { * @since 4.4 */ public RepoCommand setRecommendShallow(boolean enable) { - this.recordShallowSubmodules = enable; + this.bareWriterConfig.recordShallowSubmodules = enable; return this; } @@ -485,7 +463,7 @@ public class RepoCommand extends GitCommand<RevCommit> { * @since 4.3 */ public RepoCommand setIgnoreRemoteFailures(boolean ignore) { - this.ignoreRemoteFailures = ignore; + this.bareWriterConfig.ignoreRemoteFailures = ignore; return this; } @@ -534,6 +512,22 @@ public class RepoCommand extends GitCommand<RevCommit> { return this; } + /** + * Create a file with the given content in the destination repository + * + * @param path + * where to create the file in the destination repository + * @param contents + * content for the create file + * @return this command + * + * @since 6.1 + */ + public RepoCommand addToDestination(String path, String contents) { + this.extraContents.add(new ExtraContent(path, contents)); + return this; + } + /** {@inheritDoc} */ @Override public RevCommit call() throws GitAPIException { @@ -570,240 +564,18 @@ public class RepoCommand extends GitCommand<RevCommit> { } if (repo.isBare()) { - if (author == null) - author = new PersonIdent(repo); - if (callback == null) - callback = new DefaultRemoteReader(); List<RepoProject> renamedProjects = renameProjects(filteredProjects); - - DirCache index = DirCache.newInCore(); - ObjectInserter inserter = repo.newObjectInserter(); - - try (RevWalk rw = new RevWalk(repo)) { - prepareIndex(renamedProjects, index, inserter); - ObjectId treeId = index.writeTree(inserter); - long prevDelay = 0; - for (int i = 0; i < LOCK_FAILURE_MAX_RETRIES - 1; i++) { - try { - return commitTreeOnCurrentTip( - inserter, rw, treeId); - } catch (ConcurrentRefUpdateException e) { - prevDelay = FileUtils.delay(prevDelay, - LOCK_FAILURE_MIN_RETRY_DELAY_MILLIS, - LOCK_FAILURE_MAX_RETRY_DELAY_MILLIS); - Thread.sleep(prevDelay); - repo.getRefDatabase().refresh(); - } - } - // In the last try, just propagate the exceptions - return commitTreeOnCurrentTip(inserter, rw, treeId); - } catch (IOException | InterruptedException e) { - throw new ManifestErrorException(e); - } - } - try (Git git = new Git(repo)) { - for (RepoProject proj : filteredProjects) { - addSubmodule(proj.getName(), proj.getUrl(), proj.getPath(), - proj.getRevision(), proj.getCopyFiles(), - proj.getLinkFiles(), git); - } - return git.commit().setMessage(RepoText.get().repoCommitMessage) - .call(); - } catch (IOException e) { - throw new ManifestErrorException(e); - } - } - - private void prepareIndex(List<RepoProject> projects, DirCache index, - ObjectInserter inserter) throws IOException, GitAPIException { - Config cfg = new Config(); - StringBuilder attributes = new StringBuilder(); - DirCacheBuilder builder = index.builder(); - for (RepoProject proj : projects) { - String name = proj.getName(); - String path = proj.getPath(); - String url = proj.getUrl(); - ObjectId objectId; - if (ObjectId.isId(proj.getRevision())) { - objectId = ObjectId.fromString(proj.getRevision()); - } else { - objectId = callback.sha1(url, proj.getRevision()); - if (objectId == null && !ignoreRemoteFailures) { - throw new RemoteUnavailableException(url); - } - if (recordRemoteBranch) { - // "branch" field is only for non-tag references. - // Keep tags in "ref" field as hint for other tools. - String field = proj.getRevision().startsWith(R_TAGS) ? "ref" //$NON-NLS-1$ - : "branch"; //$NON-NLS-1$ - cfg.setString("submodule", name, field, //$NON-NLS-1$ - proj.getRevision()); - } - - if (recordShallowSubmodules - && proj.getRecommendShallow() != null) { - // The shallow recommendation is losing information. - // As the repo manifests stores the recommended - // depth in the 'clone-depth' field, while - // git core only uses a binary 'shallow = true/false' - // hint, we'll map any depth to 'shallow = true' - cfg.setBoolean("submodule", name, "shallow", //$NON-NLS-1$ //$NON-NLS-2$ - true); - } - } - if (recordSubmoduleLabels) { - StringBuilder rec = new StringBuilder(); - rec.append("/"); //$NON-NLS-1$ - rec.append(path); - for (String group : proj.getGroups()) { - rec.append(" "); //$NON-NLS-1$ - rec.append(group); - } - rec.append("\n"); //$NON-NLS-1$ - attributes.append(rec.toString()); - } - - URI submodUrl = URI.create(url); - if (targetUri != null) { - submodUrl = relativize(targetUri, submodUrl); - } - cfg.setString("submodule", name, "path", path); //$NON-NLS-1$ //$NON-NLS-2$ - cfg.setString("submodule", name, "url", //$NON-NLS-1$ //$NON-NLS-2$ - submodUrl.toString()); - - // create gitlink - if (objectId != null) { - DirCacheEntry dcEntry = new DirCacheEntry(path); - dcEntry.setObjectId(objectId); - dcEntry.setFileMode(FileMode.GITLINK); - builder.add(dcEntry); - - for (CopyFile copyfile : proj.getCopyFiles()) { - RemoteFile rf = callback.readFileWithMode(url, - proj.getRevision(), copyfile.src); - objectId = inserter.insert(Constants.OBJ_BLOB, - rf.getContents()); - dcEntry = new DirCacheEntry(copyfile.dest); - dcEntry.setObjectId(objectId); - dcEntry.setFileMode(rf.getFileMode()); - builder.add(dcEntry); - } - for (LinkFile linkfile : proj.getLinkFiles()) { - String link; - if (linkfile.dest.contains("/")) { //$NON-NLS-1$ - link = FileUtils.relativizeGitPath( - linkfile.dest.substring(0, - linkfile.dest.lastIndexOf('/')), - proj.getPath() + "/" + linkfile.src); //$NON-NLS-1$ - } else { - link = proj.getPath() + "/" + linkfile.src; //$NON-NLS-1$ - } - - objectId = inserter.insert(Constants.OBJ_BLOB, - link.getBytes(UTF_8)); - dcEntry = new DirCacheEntry(linkfile.dest); - dcEntry.setObjectId(objectId); - dcEntry.setFileMode(FileMode.SYMLINK); - builder.add(dcEntry); - } - } - } - String content = cfg.toText(); - - // create a new DirCacheEntry for .gitmodules file. - DirCacheEntry dcEntry = new DirCacheEntry( - Constants.DOT_GIT_MODULES); - ObjectId objectId = inserter.insert(Constants.OBJ_BLOB, - content.getBytes(UTF_8)); - dcEntry.setObjectId(objectId); - dcEntry.setFileMode(FileMode.REGULAR_FILE); - builder.add(dcEntry); - - if (recordSubmoduleLabels) { - // create a new DirCacheEntry for .gitattributes file. - DirCacheEntry dcEntryAttr = new DirCacheEntry( - Constants.DOT_GIT_ATTRIBUTES); - ObjectId attrId = inserter.insert(Constants.OBJ_BLOB, - attributes.toString().getBytes(UTF_8)); - dcEntryAttr.setObjectId(attrId); - dcEntryAttr.setFileMode(FileMode.REGULAR_FILE); - builder.add(dcEntryAttr); - } - - builder.finish(); - } - - private RevCommit commitTreeOnCurrentTip(ObjectInserter inserter, - RevWalk rw, ObjectId treeId) - throws IOException, ConcurrentRefUpdateException { - ObjectId headId = repo.resolve(targetBranch + "^{commit}"); //$NON-NLS-1$ - if (headId != null && rw.parseCommit(headId).getTree().getId().equals(treeId)) { - // No change. Do nothing. - return rw.parseCommit(headId); + BareSuperprojectWriter writer = new BareSuperprojectWriter(repo, targetUri, + targetBranch, + author == null ? new PersonIdent(repo) : author, + callback == null ? new DefaultRemoteReader() : callback, + bareWriterConfig, extraContents); + return writer.write(renamedProjects); } - CommitBuilder commit = new CommitBuilder(); - commit.setTreeId(treeId); - if (headId != null) - commit.setParentIds(headId); - commit.setAuthor(author); - commit.setCommitter(author); - commit.setMessage(RepoText.get().repoCommitMessage); - - ObjectId commitId = inserter.insert(commit); - inserter.flush(); - - RefUpdate ru = repo.updateRef(targetBranch); - ru.setNewObjectId(commitId); - ru.setExpectedOldObjectId(headId != null ? headId : ObjectId.zeroId()); - Result rc = ru.update(rw); - switch (rc) { - case NEW: - case FORCED: - case FAST_FORWARD: - // Successful. Do nothing. - break; - case REJECTED: - case LOCK_FAILURE: - throw new ConcurrentRefUpdateException(MessageFormat - .format(JGitText.get().cannotLock, targetBranch), - ru.getRef(), rc); - default: - throw new JGitInternalException(MessageFormat.format( - JGitText.get().updatingRefFailed, - targetBranch, commitId.name(), rc)); - } - return rw.parseCommit(commitId); - } - - private void addSubmodule(String name, String url, String path, - String revision, List<CopyFile> copyfiles, List<LinkFile> linkfiles, - Git git) throws GitAPIException, IOException { - assert (!repo.isBare()); - assert (git != null); - if (!linkfiles.isEmpty()) { - throw new UnsupportedOperationException( - JGitText.get().nonBareLinkFilesNotSupported); - } - - SubmoduleAddCommand add = git.submoduleAdd().setName(name).setPath(path) - .setURI(url); - if (monitor != null) - add.setProgressMonitor(monitor); - - Repository subRepo = add.call(); - if (revision != null) { - try (Git sub = new Git(subRepo)) { - sub.checkout().setName(findRef(revision, subRepo)).call(); - } - subRepo.close(); - git.add().addFilepattern(path).call(); - } - for (CopyFile copyfile : copyfiles) { - copyfile.copy(); - git.add().addFilepattern(copyfile.dest).call(); - } + RegularSuperprojectWriter writer = new RegularSuperprojectWriter(repo, monitor); + return writer.write(filteredProjects); } /** @@ -910,13 +682,4 @@ public class RepoCommand extends GitCommand<RevCommit> { return URI.create(j.toString()); } - private static String findRef(String ref, Repository repo) - throws IOException { - if (!ObjectId.isId(ref)) { - Ref r = repo.exactRef(R_REMOTES + DEFAULT_REMOTE_NAME + "/" + ref); //$NON-NLS-1$ - if (r != null) - return r.getName(); - } - return ref; - } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PrePushHook.java b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PrePushHook.java index 535c6b9483..43dbc37f4f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PrePushHook.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/PrePushHook.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 Obeo. and others + * Copyright (C) 2015, 2022 Obeo and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -38,6 +38,8 @@ public class PrePushHook extends GitHook<String> { private String refs; + private boolean dryRun; + /** * Constructor for PrePushHook * <p> @@ -145,6 +147,27 @@ public class PrePushHook extends GitHook<String> { } /** + * Sets whether the push is a dry run. + * + * @param dryRun + * {@code true} if the push is a dry run, {@code false} otherwise + * @since 6.2 + */ + public void setDryRun(boolean dryRun) { + this.dryRun = dryRun; + } + + /** + * Tells whether the push is a dry run. + * + * @return {@code true} if the push is a dry run, {@code false} otherwise + * @since 6.2 + */ + protected boolean isDryRun() { + return dryRun; + } + + /** * Set Refs * * @param toRefs 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 f7ebe4f40f..efdb8e42e3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -238,12 +238,14 @@ public class JGitText extends TranslationBundle { /***/ public String couldNotFindSixTabsInLine; /***/ public String couldNotGetAdvertisedRef; /***/ public String couldNotGetRepoStatistics; + /***/ public String couldNotHashByteArrayWithSha256; /***/ public String couldNotLockHEAD; /***/ public String couldNotPersistCookies; /***/ public String couldNotReadCookieFile; /***/ public String couldNotReadIndexInOneGo; /***/ public String couldNotReadObjectWhileParsingCommit; /***/ public String couldNotRewindToUpstreamCommit; + /***/ public String couldNotSignStringWithKey; /***/ public String couldNotURLEncodeToUTF8; /***/ public String countingObjects; /***/ public String createBranchFailedUnknownReason; @@ -265,6 +267,9 @@ public class JGitText extends TranslationBundle { /***/ public String deletingNotSupported; /***/ public String destinationIsNotAWildcard; /***/ public String detachedHeadDetected; + /***/ public String diffToolNotGivenError; + /***/ public String diffToolNotSpecifiedInGitAttributesError; + /***/ public String diffToolNullError; /***/ public String dirCacheDoesNotHaveABackingFile; /***/ public String dirCacheFileIsNotLocked; /***/ public String dirCacheIsNotLocked; @@ -382,6 +387,8 @@ public class JGitText extends TranslationBundle { /***/ public String inMemoryBufferLimitExceeded; /***/ public String inputDidntMatchLength; /***/ public String inputStreamMustSupportMark; + /***/ public String integerValueNotInRange; + /***/ public String integerValueNotInRangeSubSection; /***/ public String integerValueOutOfRange; /***/ public String internalRevisionError; /***/ public String internalServerError; @@ -389,9 +396,11 @@ public class JGitText extends TranslationBundle { /***/ public String inTheFuture; /***/ public String invalidAdvertisementOf; /***/ public String invalidAncestryLength; + /***/ public String invalidAwsApiSignatureVersion; /***/ public String invalidBooleanValue; /***/ public String invalidChannel; /***/ public String invalidCommitParentNumber; + /***/ public String invalidCoreAbbrev; /***/ public String invalidDepth; /***/ public String invalidEncoding; /***/ public String invalidEncryption; @@ -418,6 +427,7 @@ public class JGitText extends TranslationBundle { /***/ public String invalidModeFor; /***/ public String invalidModeForPath; /***/ public String invalidNameContainsDotDot; + /***/ public String invalidNegativeAndForce; /***/ public String invalidObject; /***/ public String invalidOldIdSent; /***/ public String invalidPacketLineHeader; @@ -481,11 +491,14 @@ public class JGitText extends TranslationBundle { /***/ public String mergeUsingStrategyResultedInDescription; /***/ public String mergeRecursiveConflictsWhenMergingCommonAncestors; /***/ public String mergeRecursiveTooManyMergeBasesFor; + /***/ public String mergeToolNotGivenError; + /***/ public String mergeToolNullError; /***/ public String messageAndTaggerNotAllowedInUnannotatedTags; /***/ public String minutesAgo; /***/ public String mismatchOffset; /***/ public String mismatchCRC; /***/ public String missingAccesskey; + /***/ public String missingAwsRegion; /***/ public String missingConfigurationForKey; /***/ public String missingCookieFile; /***/ public String missingCRC; @@ -592,6 +605,11 @@ public class JGitText extends TranslationBundle { /***/ public String pushCertificateInvalidFieldValue; /***/ public String pushCertificateInvalidHeader; /***/ public String pushCertificateInvalidSignature; + /***/ public String pushDefaultNothing; + /***/ public String pushDefaultNoUpstream; + /***/ public String pushDefaultSimple; + /***/ public String pushDefaultTriangularUpstream; + /***/ public String pushDefaultUnknown; /***/ public String pushIsNotSupportedForBundleTransport; /***/ public String pushNotPermitted; /***/ public String pushOptionsNotSupported; @@ -767,6 +785,7 @@ public class JGitText extends TranslationBundle { /***/ public String unableToSignCommitNoSecretKey; /***/ public String unauthorized; /***/ public String unencodeableFile; + /***/ public String unexpectedAwsApiSignatureVersion; /***/ public String unexpectedCompareResult; /***/ public String unexpectedEndOfConfigFile; /***/ public String unexpectedEndOfInput; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diff/FilteredRenameDetector.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diff/FilteredRenameDetector.java new file mode 100644 index 0000000000..d65624fc6a --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diff/FilteredRenameDetector.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2022, Simeon Andreev and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.diff; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.eclipse.jgit.diff.DiffEntry; +import org.eclipse.jgit.diff.DiffEntry.ChangeType; +import org.eclipse.jgit.diff.RenameDetector; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.treewalk.filter.PathFilter; + +/** + * Provides rename detection in special cases such as blame, where only a subset + * of the renames detected by {@link RenameDetector} is of interest. + */ +public class FilteredRenameDetector { + + private final RenameDetector renameDetector; + + /** + * @param repository + * The repository in which to check for renames. + */ + public FilteredRenameDetector(Repository repository) { + this(new RenameDetector(repository)); + } + + /** + * @param renameDetector + * The {@link RenameDetector} to use when checking for renames. + */ + public FilteredRenameDetector(RenameDetector renameDetector) { + this.renameDetector = renameDetector; + } + + /** + * @param diffs + * The set of changes to check. + * @param pathFilter + * Filter out changes that didn't affect this path. + * @return The subset of changes that affect only the filtered path. + * @throws IOException + */ + public List<DiffEntry> compute(List<DiffEntry> diffs, + PathFilter pathFilter) throws IOException { + return compute(diffs, Arrays.asList(pathFilter)); + } + + /** + * Tries to avoid computation overhead in {@link RenameDetector#compute()} + * by filtering diffs related to the path filters only. + * <p> + * Note: current implementation only optimizes added or removed diffs, + * further optimization is possible. + * + * @param changes + * The set of changes to check. + * @param pathFilters + * Filter out changes that didn't affect these paths. + * @return The subset of changes that affect only the filtered paths. + * @throws IOException + * @see RenameDetector#compute() + */ + public List<DiffEntry> compute(List<DiffEntry> changes, + List<PathFilter> pathFilters) throws IOException { + + if (pathFilters == null) { + throw new IllegalArgumentException("Must specify path filters"); //$NON-NLS-1$ + } + + Set<String> paths = new HashSet<>(pathFilters.size()); + for (PathFilter pathFilter : pathFilters) { + paths.add(pathFilter.getPath()); + } + + List<DiffEntry> filtered = new ArrayList<>(); + + // For new path: skip ADD's that don't match given paths + for (DiffEntry diff : changes) { + ChangeType changeType = diff.getChangeType(); + if (changeType != ChangeType.ADD + || paths.contains(diff.getNewPath())) { + filtered.add(diff); + } + } + + renameDetector.reset(); + renameDetector.addAll(filtered); + List<DiffEntry> sourceChanges = renameDetector.compute(); + + filtered.clear(); + + // For old path: skip DELETE's that don't match given paths + for (DiffEntry diff : changes) { + ChangeType changeType = diff.getChangeType(); + if (changeType != ChangeType.DELETE + || paths.contains(diff.getOldPath())) { + filtered.add(diff); + } + } + + renameDetector.reset(); + renameDetector.addAll(filtered); + List<DiffEntry> targetChanges = renameDetector.compute(); + + List<DiffEntry> result = new ArrayList<>(); + + for (DiffEntry sourceChange : sourceChanges) { + if (paths.contains(sourceChange.getNewPath())) { + result.add(sourceChange); + } + } + for (DiffEntry targetChange : targetChanges) { + if (paths.contains(targetChange.getOldPath())) { + result.add(targetChange); + } + } + + renameDetector.reset(); + return result; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java new file mode 100644 index 0000000000..ebef5247e6 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java @@ -0,0 +1,243 @@ +/* + * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com> + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.diffmergetool; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Map; + +import org.eclipse.jgit.errors.NoWorkTreeException; +import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.FS.ExecutionResult; +import org.eclipse.jgit.util.FS_POSIX; +import org.eclipse.jgit.util.FS_Win32; +import org.eclipse.jgit.util.FS_Win32_Cygwin; +import org.eclipse.jgit.util.StringUtils; +import org.eclipse.jgit.util.SystemReader; + +/** + * Runs a command with help of FS. + */ +public class CommandExecutor { + + private FS fs; + + private boolean checkExitCode; + + private File commandFile; + + private boolean useMsys2; + + /** + * @param fs + * the file system + * @param checkExitCode + * should the exit code be checked for errors ? + */ + public CommandExecutor(FS fs, boolean checkExitCode) { + this.fs = fs; + this.checkExitCode = checkExitCode; + } + + /** + * @param command + * the command string + * @param workingDir + * the working directory + * @param env + * the environment + * @return the execution result + * @throws ToolException + * @throws InterruptedException + * @throws IOException + */ + public ExecutionResult run(String command, File workingDir, + Map<String, String> env) + throws ToolException, IOException, InterruptedException { + String[] commandArray = createCommandArray(command); + try { + ProcessBuilder pb = fs.runInShell(commandArray[0], + Arrays.copyOfRange(commandArray, 1, commandArray.length)); + pb.directory(workingDir); + Map<String, String> envp = pb.environment(); + if (env != null) { + envp.putAll(env); + } + ExecutionResult result = fs.execute(pb, null); + int rc = result.getRc(); + if (rc != 0) { + boolean execError = isCommandExecutionError(rc); + if (checkExitCode || execError) { + throw new ToolException( + "JGit: tool execution return code: " + rc + "\n" //$NON-NLS-1$ //$NON-NLS-2$ + + "checkExitCode: " + checkExitCode + "\n" //$NON-NLS-1$ //$NON-NLS-2$ + + "execError: " + execError + "\n" //$NON-NLS-1$ //$NON-NLS-2$ + + "stderr: \n" //$NON-NLS-1$ + + new String( + result.getStderr().toByteArray(), + SystemReader.getInstance() + .getDefaultCharset()), + result, execError); + } + } + return result; + } finally { + deleteCommandArray(); + } + } + + /** + * @param path + * the executable path + * @param workingDir + * the working directory + * @param env + * the environment + * @return the execution result + * @throws ToolException + * @throws InterruptedException + * @throws IOException + */ + public boolean checkExecutable(String path, File workingDir, + Map<String, String> env) + throws ToolException, IOException, InterruptedException { + checkUseMsys2(path); + String command = null; + if (fs instanceof FS_Win32 && !useMsys2) { + Path p = Paths.get(path); + // Win32 (and not cygwin or MSYS2) where accepts only command / exe + // name as parameter + // so check if exists and executable in this case + if (p.isAbsolute() && Files.isExecutable(p)) { + return true; + } + // try where command for all other cases + command = "where " + ExternalToolUtils.quotePath(path); //$NON-NLS-1$ + } else { + command = "which " + ExternalToolUtils.quotePath(path); //$NON-NLS-1$ + } + boolean available = true; + try { + ExecutionResult rc = run(command, workingDir, env); + if (rc.getRc() != 0) { + available = false; + } + } catch (IOException | InterruptedException | NoWorkTreeException + | ToolException e) { + // no op: is true to not hide possible tools from user + } + return available; + } + + private void deleteCommandArray() { + deleteCommandFile(); + } + + private String[] createCommandArray(String command) + throws ToolException, IOException { + String[] commandArray = null; + checkUseMsys2(command); + createCommandFile(command); + if (fs instanceof FS_POSIX) { + commandArray = new String[1]; + commandArray[0] = commandFile.getCanonicalPath(); + } else if (fs instanceof FS_Win32) { + if (useMsys2) { + commandArray = new String[3]; + commandArray[0] = "bash.exe"; //$NON-NLS-1$ + commandArray[1] = "-c"; //$NON-NLS-1$ + commandArray[2] = commandFile.getCanonicalPath().replace("\\", //$NON-NLS-1$ + "/"); //$NON-NLS-1$ + } else { + commandArray = new String[1]; + commandArray[0] = commandFile.getCanonicalPath(); + } + } else if (fs instanceof FS_Win32_Cygwin) { + commandArray = new String[1]; + commandArray[0] = commandFile.getCanonicalPath().replace("\\", "/"); //$NON-NLS-1$ //$NON-NLS-2$ + } else { + throw new ToolException( + "JGit: file system not supported: " + fs.toString()); //$NON-NLS-1$ + } + return commandArray; + } + + private void checkUseMsys2(String command) { + useMsys2 = false; + String useMsys2Str = System.getProperty("jgit.usemsys2bash"); //$NON-NLS-1$ + if (!StringUtils.isEmptyOrNull(useMsys2Str)) { + if (useMsys2Str.equalsIgnoreCase("auto")) { //$NON-NLS-1$ + useMsys2 = command.contains(".sh"); //$NON-NLS-1$ + } else { + useMsys2 = Boolean.parseBoolean(useMsys2Str); + } + } + } + + private void createCommandFile(String command) + throws ToolException, IOException { + String fileExtension = null; + if (useMsys2 || fs instanceof FS_POSIX + || fs instanceof FS_Win32_Cygwin) { + fileExtension = ".sh"; //$NON-NLS-1$ + } else if (fs instanceof FS_Win32) { + fileExtension = ".cmd"; //$NON-NLS-1$ + command = "@echo off" + System.lineSeparator() + command //$NON-NLS-1$ + + System.lineSeparator() + "exit /B %ERRORLEVEL%"; //$NON-NLS-1$ + } else { + throw new ToolException( + "JGit: file system not supported: " + fs.toString()); //$NON-NLS-1$ + } + commandFile = File.createTempFile(".__", //$NON-NLS-1$ + "__jgit_tool" + fileExtension); //$NON-NLS-1$ + try (OutputStream outStream = new FileOutputStream(commandFile)) { + byte[] strToBytes = command + .getBytes(SystemReader.getInstance().getDefaultCharset()); + outStream.write(strToBytes); + outStream.close(); + } + commandFile.setExecutable(true); + } + + private void deleteCommandFile() { + if (commandFile != null && commandFile.exists()) { + commandFile.delete(); + } + } + + private boolean isCommandExecutionError(int rc) { + if (useMsys2 || fs instanceof FS_POSIX + || fs instanceof FS_Win32_Cygwin) { + // 126: permission for executing command denied + // 127: command not found + if ((rc == 126) || (rc == 127)) { + return true; + } + } + else if (fs instanceof FS_Win32) { + // 9009, 0x2331: Program is not recognized as an internal or + // external command, operable program or batch file. Indicates that + // command, application name or path has been misspelled when + // configuring the Action. + if (rc == 9009) { + return true; + } + } + return false; + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineDiffTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineDiffTool.java index 509515c37a..00dec32718 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineDiffTool.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineDiffTool.java @@ -111,7 +111,7 @@ public enum CommandLineDiffTool { * See: <a href= * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a> */ - gvimdiff("gviewdiff", "\"$LOCAL\" \"$REMOTE\""), + gvimdiff("gvimdiff", "\"$LOCAL\" \"$REMOTE\""), /** * See: <a href= * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a> @@ -160,7 +160,7 @@ public enum CommandLineDiffTool { * See: <a href= * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a> */ - vimdiff("viewdiff", gvimdiff), + vimdiff("vimdiff", gvimdiff), /** * See: <a href= * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a> diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineMergeTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineMergeTool.java new file mode 100644 index 0000000000..3a22124328 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineMergeTool.java @@ -0,0 +1,327 @@ +/* + * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com> + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.diffmergetool; + +/** + * Pre-defined merge tools. + * + * Adds same merge tools as also pre-defined in C-Git see "git-core\mergetools\" + * see links to command line parameter description for the tools + * + * <pre> + * araxis + * bc + * bc3 + * codecompare + * deltawalker + * diffmerge + * diffuse + * ecmerge + * emerge + * examdiff + * guiffy + * gvimdiff + * gvimdiff2 + * gvimdiff3 + * kdiff3 + * kompare + * meld + * opendiff + * p4merge + * tkdiff + * tortoisemerge + * vimdiff + * vimdiff2 + * vimdiff3 + * winmerge + * xxdiff + * </pre> + * + */ +@SuppressWarnings("nls") +public enum CommandLineMergeTool { + /** + * See: <a href= + * "https://www.araxis.com/merge/documentation-windows/command-line.en">https://www.araxis.com/merge/documentation-windows/command-line.en</a> + */ + araxis("compare", + "-wait -merge -3 -a1 \"$BASE\" \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", + "-wait -2 \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", + false), + /** + * See: <a href= + * "https://www.scootersoftware.com/v4help/index.html?command_line_reference.html">https://www.scootersoftware.com/v4help/index.html?command_line_reference.html</a> + */ + bc("bcomp", "\"$LOCAL\" \"$REMOTE\" \"$BASE\" --mergeoutput=\"$MERGED\"", + "\"$LOCAL\" \"$REMOTE\" --mergeoutput=\"$MERGED\"", + false), + /** + * See: <a href= + * "https://www.scootersoftware.com/v4help/index.html?command_line_reference.html">https://www.scootersoftware.com/v4help/index.html?command_line_reference.html</a> + */ + bc3("bcompare", bc), + /** + * See: <a href= + * "https://www.devart.com/codecompare/docs/index.html?merging_via_command_line.htm">https://www.devart.com/codecompare/docs/index.html?merging_via_command_line.htm</a> + */ + codecompare("CodeMerge", + "-MF=\"$LOCAL\" -TF=\"$REMOTE\" -BF=\"$BASE\" -RF=\"$MERGED\"", + "-MF=\"$LOCAL\" -TF=\"$REMOTE\" -RF=\"$MERGED\"", + false), + /** + * See: <a href= + * "https://www.deltawalker.com/integrate/command-line">https://www.deltawalker.com/integrate/command-line</a> + * <p> + * Hint: $(pwd) command must be defined + * </p> + */ + deltawalker("DeltaWalker", + "\"$LOCAL\" \"$REMOTE\" \"$BASE\" -pwd=\"$(pwd)\" -merged=\"$MERGED\"", + "\"$LOCAL\" \"$REMOTE\" -pwd=\"$(pwd)\" -merged=\"$MERGED\"", + true), + /** + * See: <a href= + * "https://sourcegear.com/diffmerge/webhelp/sec__clargs__diff.html">https://sourcegear.com/diffmerge/webhelp/sec__clargs__diff.html</a> + */ + diffmerge("diffmerge", //$NON-NLS-1$ + "--merge --result=\"$MERGED\" \"$LOCAL\" \"$BASE\" \"$REMOTE\"", + "--merge --result=\"$MERGED\" \"$LOCAL\" \"$REMOTE\"", + true), + /** + * See: <a href= + * "http://diffuse.sourceforge.net/manual.html#introduction-usage">http://diffuse.sourceforge.net/manual.html#introduction-usage</a> + * <p> + * Hint: check the ' | cat' for the call + * </p> + */ + diffuse("diffuse", "\"$LOCAL\" \"$MERGED\" \"$REMOTE\" \"$BASE\"", + "\"$LOCAL\" \"$MERGED\" \"$REMOTE\"", false), + /** + * See: <a href= + * "http://www.elliecomputing.com/en/OnlineDoc/ecmerge_en/44205167.asp">http://www.elliecomputing.com/en/OnlineDoc/ecmerge_en/44205167.asp</a> + */ + ecmerge("ecmerge", + "--default --mode=merge3 \"$BASE\" \"$LOCAL\" \"$REMOTE\" --to=\"$MERGED\"", + "--default --mode=merge2 \"$LOCAL\" \"$REMOTE\" --to=\"$MERGED\"", + false), + /** + * See: <a href= + * "https://www.gnu.org/software/emacs/manual/html_node/emacs/Overview-of-Emerge.html">https://www.gnu.org/software/emacs/manual/html_node/emacs/Overview-of-Emerge.html</a> + * <p> + * Hint: $(basename) command must be defined + * </p> + */ + emerge("emacs", + "-f emerge-files-with-ancestor-command \"$LOCAL\" \"$REMOTE\" \"$BASE\" \"$(basename \"$MERGED\")\"", + "-f emerge-files-command \"$LOCAL\" \"$REMOTE\" \"$(basename \"$MERGED\")\"", + true), + /** + * See: <a href= + * "https://www.prestosoft.com/ps.asp?page=htmlhelp/edp/command_line_options">https://www.prestosoft.com/ps.asp?page=htmlhelp/edp/command_line_options</a> + */ + examdiff("ExamDiff", + "-merge \"$LOCAL\" \"$BASE\" \"$REMOTE\" -o:\"$MERGED\" -nh", + "-merge \"$LOCAL\" \"$REMOTE\" -o:\"$MERGED\" -nh", + false), + /** + * See: <a href= + * "https://www.guiffy.com/help/GuiffyHelp/GuiffyCmd.html">https://www.guiffy.com/help/GuiffyHelp/GuiffyCmd.html</a> + */ + guiffy("guiffy", "-s \"$LOCAL\" \"$REMOTE\" \"$BASE\" \"$MERGED\"", + "-m \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", true), + /** + * See: <a href= + * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a> + */ + gvimdiff("gvim", + "-f -d -c '4wincmd w | wincmd J' \"$LOCAL\" \"$BASE\" \"$REMOTE\" \"$MERGED\"", + "-f -d -c 'wincmd l' \"$LOCAL\" \"$MERGED\" \"$REMOTE\"", + true), + /** + * See: <a href= + * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a> + */ + gvimdiff2("gvim", "-f -d -c 'wincmd l' \"$LOCAL\" \"$MERGED\" \"$REMOTE\"", + "-f -d -c 'wincmd l' \"$LOCAL\" \"$MERGED\" \"$REMOTE\"", true), + /** + * See: <a href= "http://vimdoc.sourceforge.net/htmldoc/diff.html"></a> + */ + gvimdiff3("gvim", + "-f -d -c 'hid | hid | hid' \"$LOCAL\" \"$REMOTE\" \"$BASE\" \"$MERGED\"", + "-f -d -c 'hid | hid' \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", true), + /** + * See: <a href= + * "http://kdiff3.sourceforge.net/doc/documentation.html">http://kdiff3.sourceforge.net/doc/documentation.html</a> + */ + kdiff3("kdiff3", + "--auto --L1 \"$MERGED (Base)\" --L2 \"$MERGED (Local)\" --L3 \"$MERGED (Remote)\" -o \"$MERGED\" \"$BASE\" \"$LOCAL\" \"$REMOTE\"", + "--auto --L1 \"$MERGED (Local)\" --L2 \"$MERGED (Remote)\" -o \"$MERGED\" \"$LOCAL\" \"$REMOTE\"", + true), + /** + * See: <a href= + * "http://meldmerge.org/help/file-mode.html">http://meldmerge.org/help/file-mode.html</a> + * <p> + * Hint: use meld with output option only (new versions) + * </p> + */ + meld("meld", "--output=\"$MERGED\" \"$LOCAL\" \"$BASE\" \"$REMOTE\"", + "\"$LOCAL\" \"$MERGED\" \"$REMOTE\"", + false), + /** + * See: <a href= + * "http://www.manpagez.com/man/1/opendiff/">http://www.manpagez.com/man/1/opendiff/</a> + * <p> + * Hint: check the ' | cat' for the call + * </p> + */ + opendiff("opendiff", + "\"$LOCAL\" \"$REMOTE\" -ancestor \"$BASE\" -merge \"$MERGED\"", + "\"$LOCAL\" \"$REMOTE\" -merge \"$MERGED\"", + false), + /** + * See: <a href= + * "https://www.perforce.com/manuals/v15.1/cmdref/p4_merge.html">https://www.perforce.com/manuals/v15.1/cmdref/p4_merge.html</a> + * <p> + * Hint: check how to fix "no base present" / create_virtual_base problem + * </p> + */ + p4merge("p4merge", "\"$BASE\" \"$REMOTE\" \"$LOCAL\" \"$MERGED\"", + "\"$REMOTE\" \"$LOCAL\" \"$MERGED\"", false), + /** + * See: <a href= + * "http://linux.math.tifr.res.in/manuals/man/tkdiff.html">http://linux.math.tifr.res.in/manuals/man/tkdiff.html</a> + */ + tkdiff("tkdiff", "-a \"$BASE\" -o \"$MERGED\" \"$LOCAL\" \"$REMOTE\"", + "-o \"$MERGED\" \"$LOCAL\" \"$REMOTE\"", + true), + /** + * See: <a href= + * "https://tortoisegit.org/docs/tortoisegitmerge/tme-automation.html#tme-automation-basics">https://tortoisegit.org/docs/tortoisegitmerge/tme-automation.html#tme-automation-basics</a> + * <p> + * Hint: merge without base is not supported + * </p> + * <p> + * Hint: cannot diff + * </p> + */ + tortoisegitmerge("tortoisegitmerge", + "-base \"$BASE\" -mine \"$LOCAL\" -theirs \"$REMOTE\" -merged \"$MERGED\"", + null, false), + /** + * See: <a href= + * "https://tortoisegit.org/docs/tortoisegitmerge/tme-automation.html#tme-automation-basics">https://tortoisegit.org/docs/tortoisegitmerge/tme-automation.html#tme-automation-basics</a> + * <p> + * Hint: merge without base is not supported + * </p> + * <p> + * Hint: cannot diff + * </p> + */ + tortoisemerge("tortoisemerge", + "-base:\"$BASE\" -mine:\"$LOCAL\" -theirs:\"$REMOTE\" -merged:\"$MERGED\"", + null, false), + /** + * See: <a href= + * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a> + */ + vimdiff("vim", gvimdiff), + /** + * See: <a href= + * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a> + */ + vimdiff2("vim", gvimdiff2), + /** + * See: <a href= + * "http://vimdoc.sourceforge.net/htmldoc/diff.html">http://vimdoc.sourceforge.net/htmldoc/diff.html</a> + */ + vimdiff3("vim", gvimdiff3), + /** + * See: <a href= + * "http://manual.winmerge.org/Command_line.html">http://manual.winmerge.org/Command_line.html</a> + * <p> + * Hint: check how 'mergetool_find_win32_cmd "WinMergeU.exe" "WinMerge"' + * works + * </p> + */ + winmerge("WinMergeU", + "-u -e -dl Local -dr Remote \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", + "-u -e -dl Local -dr Remote \"$LOCAL\" \"$REMOTE\" \"$MERGED\"", + false), + /** + * See: <a href= + * "http://furius.ca/xxdiff/doc/xxdiff-doc.html">http://furius.ca/xxdiff/doc/xxdiff-doc.html</a> + */ + xxdiff("xxdiff", + "-X --show-merged-pane -R 'Accel.SaveAsMerged: \"Ctrl+S\"' -R 'Accel.Search: \"Ctrl+F\"' -R 'Accel.SearchForward: \"Ctrl+G\"' --merged-file \"$MERGED\" \"$LOCAL\" \"$BASE\" \"$REMOTE\"", + "-X -R 'Accel.SaveAsMerged: \"Ctrl+S\"' -R 'Accel.Search: \"Ctrl+F\"' -R 'Accel.SearchForward: \"Ctrl+G\"' --merged-file \"$MERGED\" \"$LOCAL\" \"$REMOTE\"", + false); + + CommandLineMergeTool(String path, String parametersWithBase, + String parametersWithoutBase, + boolean exitCodeTrustable) { + this.path = path; + this.parametersWithBase = parametersWithBase; + this.parametersWithoutBase = parametersWithoutBase; + this.exitCodeTrustable = exitCodeTrustable; + } + + CommandLineMergeTool(CommandLineMergeTool from) { + this(from.getPath(), from.getParameters(true), + from.getParameters(false), from.isExitCodeTrustable()); + } + + CommandLineMergeTool(String path, CommandLineMergeTool from) { + this(path, from.getParameters(true), from.getParameters(false), + from.isExitCodeTrustable()); + } + + private final String path; + + private final String parametersWithBase; + + private final String parametersWithoutBase; + + private final boolean exitCodeTrustable; + + /** + * @return path + */ + public String getPath() { + return path; + } + + /** + * @param withBase + * return parameters with base present? + * @return parameters with or without base present + */ + public String getParameters(boolean withBase) { + if (withBase) { + return parametersWithBase; + } + return parametersWithoutBase; + } + + /** + * @return parameters + */ + public boolean isExitCodeTrustable() { + return exitCodeTrustable; + } + + /** + * @return true if command with base present is valid, false otherwise + */ + public boolean canMergeWithoutBasePresent() { + return parametersWithoutBase != null; + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffToolConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffToolConfig.java index 551f634f2d..c8b04f90f2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffToolConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffToolConfig.java @@ -49,9 +49,10 @@ public class DiffToolConfig { toolName = rc.getString(CONFIG_DIFF_SECTION, null, CONFIG_KEY_TOOL); guiToolName = rc.getString(CONFIG_DIFF_SECTION, null, CONFIG_KEY_GUITOOL); - prompt = rc.getBoolean(CONFIG_DIFFTOOL_SECTION, CONFIG_KEY_PROMPT, + prompt = rc.getBoolean(CONFIG_DIFFTOOL_SECTION, toolName, + CONFIG_KEY_PROMPT, true); - String trustStr = rc.getString(CONFIG_DIFFTOOL_SECTION, null, + String trustStr = rc.getString(CONFIG_DIFFTOOL_SECTION, toolName, CONFIG_KEY_TRUST_EXIT_CODE); if (trustStr != null) { trustExitCode = Boolean.parseBoolean(trustStr) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java index 39729a4eec..d0034df3bc 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java @@ -1,5 +1,6 @@ /* - * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com> + * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com> + * Copyright (C) 2019, Tim Neumann <tim.neumann@advantest.com> * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -10,24 +11,43 @@ package org.eclipse.jgit.internal.diffmergetool; -import java.util.TreeMap; +import java.io.File; +import java.io.IOException; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; import java.util.Set; +import java.util.TreeMap; +import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.lib.internal.BooleanTriState; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.FS.ExecutionResult; +import org.eclipse.jgit.util.StringUtils; /** * Manages diff tools. */ public class DiffTools { + private final FS fs; + + private final File gitDir; + + private final File workTree; + private final DiffToolConfig config; - private Map<String, ExternalDiffTool> predefinedTools; + private final Repository repo; - private Map<String, ExternalDiffTool> userDefinedTools; + private final Map<String, ExternalDiffTool> predefinedTools; + + private final Map<String, ExternalDiffTool> userDefinedTools; /** * Creates the external diff-tools manager for given repository. @@ -36,46 +56,220 @@ public class DiffTools { * the repository */ public DiffTools(Repository repo) { - config = repo.getConfig().get(DiffToolConfig.KEY); - setupPredefinedTools(); - setupUserDefinedTools(); + this(repo, repo.getConfig()); + } + + /** + * Creates the external merge-tools manager for given configuration. + * + * @param config + * the git configuration + */ + public DiffTools(StoredConfig config) { + this(null, config); + } + + private DiffTools(Repository repo, StoredConfig config) { + this.repo = repo; + this.config = config.get(DiffToolConfig.KEY); + this.gitDir = repo == null ? null : repo.getDirectory(); + this.fs = repo == null ? FS.DETECTED : repo.getFS(); + this.workTree = repo == null ? null : repo.getWorkTree(); + predefinedTools = setupPredefinedTools(); + userDefinedTools = setupUserDefinedTools(predefinedTools); } /** * Compare two versions of a file. * - * @param newPath - * the new file path - * @param oldPath - * the old file path - * @param newId - * the new object ID - * @param oldId - * the old object ID + * @param localFile + * The local/left version of the file. + * @param remoteFile + * The remote/right version of the file. * @param toolName - * the selected tool name (can be null) + * Optionally the name of the tool to use. If not given the + * default tool will be used. * @param prompt - * the prompt option + * Optionally a flag whether to prompt the user before compare. + * If not given the default will be used. * @param gui - * the GUI option + * A flag whether to prefer a gui tool. + * @param trustExitCode + * Optionally a flag whether to trust the exit code of the tool. + * If not given the default will be used. + * @param promptHandler + * The handler to use when needing to prompt the user if he wants + * to continue. + * @param noToolHandler + * The handler to use when needing to inform the user, that no + * tool is configured. + * @return the optional result of executing the tool if it was executed + * @throws ToolException + * when the tool fails + */ + public Optional<ExecutionResult> compare(FileElement localFile, + FileElement remoteFile, Optional<String> toolName, + BooleanTriState prompt, boolean gui, BooleanTriState trustExitCode, + PromptContinueHandler promptHandler, + InformNoToolHandler noToolHandler) throws ToolException { + + String toolNameToUse; + + if (toolName == null) { + throw new ToolException(JGitText.get().diffToolNullError); + } + + if (toolName.isPresent()) { + toolNameToUse = toolName.get(); + } else { + toolNameToUse = getDefaultToolName(gui); + } + + if (StringUtils.isEmptyOrNull(toolNameToUse)) { + throw new ToolException(JGitText.get().diffToolNotGivenError); + } + + boolean doPrompt; + if (prompt != BooleanTriState.UNSET) { + doPrompt = prompt == BooleanTriState.TRUE; + } else { + doPrompt = isInteractive(); + } + + if (doPrompt) { + if (!promptHandler.prompt(toolNameToUse)) { + return Optional.empty(); + } + } + + boolean trust; + if (trustExitCode != BooleanTriState.UNSET) { + trust = trustExitCode == BooleanTriState.TRUE; + } else { + trust = config.isTrustExitCode(); + } + + ExternalDiffTool tool = getTool(toolNameToUse); + if (tool == null) { + throw new ToolException( + "External diff tool is not defined: " + toolNameToUse); //$NON-NLS-1$ + } + + return Optional.of( + compare(localFile, remoteFile, tool, trust)); + } + + /** + * Compare two versions of a file. + * + * @param localFile + * the local file element + * @param remoteFile + * the remote file element + * @param tool + * the selected tool * @param trustExitCode * the "trust exit code" option - * @return the return code from executed tool + * @return the execution result from tool + * @throws ToolException */ - public int compare(String newPath, String oldPath, String newId, - String oldId, String toolName, BooleanTriState prompt, - BooleanTriState gui, BooleanTriState trustExitCode) { - return 0; + public ExecutionResult compare(FileElement localFile, + FileElement remoteFile, ExternalDiffTool tool, + boolean trustExitCode) throws ToolException { + try { + if (tool == null) { + throw new ToolException(JGitText + .get().diffToolNotSpecifiedInGitAttributesError); + } + // prepare the command (replace the file paths) + String command = ExternalToolUtils.prepareCommand(tool.getCommand(), + localFile, remoteFile, null, null); + // prepare the environment + Map<String, String> env = ExternalToolUtils.prepareEnvironment( + gitDir, localFile, remoteFile, null, null); + // execute the tool + CommandExecutor cmdExec = new CommandExecutor(fs, trustExitCode); + return cmdExec.run(command, workTree, env); + } catch (IOException | InterruptedException e) { + throw new ToolException(e); + } finally { + localFile.cleanTemporaries(); + remoteFile.cleanTemporaries(); + } } /** - * @return the tool names + * Get user defined tool names. + * + * @return the user defined tool names */ - public Set<String> getToolNames() { - return config.getToolNames(); + public Set<String> getUserDefinedToolNames() { + return userDefinedTools.keySet(); } /** + * Get predefined tool names. + * + * @return the predefined tool names + */ + public Set<String> getPredefinedToolNames() { + return predefinedTools.keySet(); + } + + /** + * Get all tool names. + * + * @return the all tool names (default or available tool name is the first + * in the set) + */ + public Set<String> getAllToolNames() { + String defaultName = getDefaultToolName(false); + if (defaultName == null) { + defaultName = getFirstAvailableTool(); + } + return ExternalToolUtils.createSortedToolSet(defaultName, + getUserDefinedToolNames(), getPredefinedToolNames()); + } + + /** + * Provides {@link Optional} with the name of an external diff tool if + * specified in git configuration for a path. + * + * The formed git configuration results from global rules as well as merged + * rules from info and worktree attributes. + * + * Triggers {@link TreeWalk} until specified path found in the tree. + * + * @param path + * path to the node in repository to parse git attributes for + * @return name of the difftool if set + * @throws ToolException + */ + public Optional<String> getExternalToolFromAttributes(final String path) + throws ToolException { + return ExternalToolUtils.getExternalToolFromAttributes(repo, path, + ExternalToolUtils.KEY_DIFF_TOOL); + } + + /** + * Checks the availability of the predefined tools in the system. + * + * @return set of predefined available tools + */ + public Set<String> getPredefinedAvailableTools() { + Map<String, ExternalDiffTool> defTools = getPredefinedTools(true); + Set<String> availableTools = new LinkedHashSet<>(); + for (Entry<String, ExternalDiffTool> elem : defTools.entrySet()) { + if (elem.getValue().isAvailable()) { + availableTools.add(elem.getKey()); + } + } + return availableTools; + } + + /** + * Get user defined tools map. + * * @return the user defined tools */ public Map<String, ExternalDiffTool> getUserDefinedTools() { @@ -83,61 +277,106 @@ public class DiffTools { } /** - * @return the available predefined tools + * Get predefined tools map. + * + * @param checkAvailability + * true: for checking if tools can be executed; ATTENTION: this + * check took some time, do not execute often (store the map for + * other actions); false: availability is NOT checked: + * isAvailable() returns default false is this case! + * @return the predefined tools with optionally checked availability (long + * running operation) */ - public Map<String, ExternalDiffTool> getAvailableTools() { + public Map<String, ExternalDiffTool> getPredefinedTools( + boolean checkAvailability) { + if (checkAvailability) { + for (ExternalDiffTool tool : predefinedTools.values()) { + PreDefinedDiffTool predefTool = (PreDefinedDiffTool) tool; + predefTool.setAvailable(ExternalToolUtils.isToolAvailable(fs, + gitDir, workTree, predefTool.getPath())); + } + } return Collections.unmodifiableMap(predefinedTools); } /** - * @return the NOT available predefined tools + * Get first available tool name. + * + * @return the name of first available predefined tool or null */ - public Map<String, ExternalDiffTool> getNotAvailableTools() { - return Collections.unmodifiableMap(new TreeMap<>()); + public String getFirstAvailableTool() { + for (ExternalDiffTool tool : predefinedTools.values()) { + if (ExternalToolUtils.isToolAvailable(fs, gitDir, workTree, + tool.getPath())) { + return tool.getName(); + } + } + return null; } /** + * Get default (gui-)tool name. + * * @param gui * use the diff.guitool setting ? * @return the default tool name */ - public String getDefaultToolName(BooleanTriState gui) { - return gui != BooleanTriState.UNSET ? "my_gui_tool" //$NON-NLS-1$ - : "my_default_toolname"; //$NON-NLS-1$ + public String getDefaultToolName(boolean gui) { + String guiToolName; + if (gui) { + guiToolName = config.getDefaultGuiToolName(); + if (guiToolName != null) { + return guiToolName; + } + } + return config.getDefaultToolName(); } /** + * Is interactive diff (prompt enabled) ? + * * @return is interactive (config prompt enabled) ? */ public boolean isInteractive() { - return false; + return config.isPrompt(); + } + + private ExternalDiffTool getTool(final String name) { + ExternalDiffTool tool = userDefinedTools.get(name); + if (tool == null) { + tool = predefinedTools.get(name); + } + return tool; } - private void setupPredefinedTools() { - predefinedTools = new TreeMap<>(); + private static Map<String, ExternalDiffTool> setupPredefinedTools() { + Map<String, ExternalDiffTool> tools = new TreeMap<>(); for (CommandLineDiffTool tool : CommandLineDiffTool.values()) { - predefinedTools.put(tool.name(), new PreDefinedDiffTool(tool)); + tools.put(tool.name(), new PreDefinedDiffTool(tool)); } + return tools; } - private void setupUserDefinedTools() { - userDefinedTools = new TreeMap<>(); + private Map<String, ExternalDiffTool> setupUserDefinedTools( + Map<String, ExternalDiffTool> predefTools) { + Map<String, ExternalDiffTool> tools = new TreeMap<>(); Map<String, ExternalDiffTool> userTools = config.getTools(); for (String name : userTools.keySet()) { ExternalDiffTool userTool = userTools.get(name); // if difftool.<name>.cmd is defined we have user defined tool if (userTool.getCommand() != null) { - userDefinedTools.put(name, userTool); + tools.put(name, userTool); } else if (userTool.getPath() != null) { // if difftool.<name>.path is defined we just overload the path // of predefined tool - PreDefinedDiffTool predefTool = (PreDefinedDiffTool) predefinedTools + PreDefinedDiffTool predefTool = (PreDefinedDiffTool) predefTools .get(name); if (predefTool != null) { predefTool.setPath(userTool.getPath()); } } } + return tools; } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalDiffTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalDiffTool.java index f2d7e828cb..e01b892a53 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalDiffTool.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalDiffTool.java @@ -30,4 +30,10 @@ public interface ExternalDiffTool { */ String getCommand(); + /** + * @return availability of the tool: true if tool can be executed and false + * if not + */ + boolean isAvailable(); + } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalMergeTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalMergeTool.java new file mode 100644 index 0000000000..0c3ddf9afe --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalMergeTool.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com> + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.diffmergetool; + +import org.eclipse.jgit.lib.internal.BooleanTriState; + +/** + * The merge tool interface. + */ +public interface ExternalMergeTool extends ExternalDiffTool { + + /** + * @return the tool "trust exit code" option + */ + BooleanTriState getTrustExitCode(); + + /** + * @param withBase + * get command with base present (true) or without base present + * (false) + * @return the tool command + */ + String getCommand(boolean withBase); + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java new file mode 100644 index 0000000000..b2dd846d70 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com> + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.internal.diffmergetool; + +import java.util.TreeMap; +import java.io.File; +import java.io.IOException; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import org.eclipse.jgit.attributes.Attributes; +import org.eclipse.jgit.errors.RevisionSyntaxException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.treewalk.FileTreeIterator; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.WorkingTreeIterator; +import org.eclipse.jgit.treewalk.filter.NotIgnoredFilter; +import org.eclipse.jgit.util.FS; + +/** + * Utilities for diff- and merge-tools. + */ +public class ExternalToolUtils { + + /** + * Key for merge tool git configuration section + */ + public static final String KEY_MERGE_TOOL = "mergetool"; //$NON-NLS-1$ + + /** + * Key for diff tool git configuration section + */ + public static final String KEY_DIFF_TOOL = "difftool"; //$NON-NLS-1$ + + /** + * Prepare command for execution. + * + * @param command + * the input "command" string + * @param localFile + * the local file (ours) + * @param remoteFile + * the remote file (theirs) + * @param mergedFile + * the merged file (worktree) + * @param baseFile + * the base file (can be null) + * @return the prepared (with replaced variables) command string + * @throws IOException + */ + public static String prepareCommand(String command, FileElement localFile, + FileElement remoteFile, FileElement mergedFile, + FileElement baseFile) throws IOException { + if (localFile != null) { + command = localFile.replaceVariable(command); + } + if (remoteFile != null) { + command = remoteFile.replaceVariable(command); + } + if (mergedFile != null) { + command = mergedFile.replaceVariable(command); + } + if (baseFile != null) { + command = baseFile.replaceVariable(command); + } + return command; + } + + /** + * Prepare environment needed for execution. + * + * @param gitDir + * the .git directory + * @param localFile + * the local file (ours) + * @param remoteFile + * the remote file (theirs) + * @param mergedFile + * the merged file (worktree) + * @param baseFile + * the base file (can be null) + * @return the environment map with variables and values (file paths) + * @throws IOException + */ + public static Map<String, String> prepareEnvironment(File gitDir, + FileElement localFile, FileElement remoteFile, + FileElement mergedFile, FileElement baseFile) throws IOException { + Map<String, String> env = new TreeMap<>(); + if (gitDir != null) { + env.put(Constants.GIT_DIR_KEY, gitDir.getAbsolutePath()); + } + if (localFile != null) { + localFile.addToEnv(env); + } + if (remoteFile != null) { + remoteFile.addToEnv(env); + } + if (mergedFile != null) { + mergedFile.addToEnv(env); + } + if (baseFile != null) { + baseFile.addToEnv(env); + } + return env; + } + + /** + * @param path + * the path to be quoted + * @return quoted path if it contains spaces + */ + @SuppressWarnings("nls") + public static String quotePath(String path) { + // handling of spaces in path + if (path.contains(" ")) { + // add quotes before if needed + if (!path.startsWith("\"")) { + path = "\"" + path; + } + // add quotes after if needed + if (!path.endsWith("\"")) { + path = path + "\""; + } + } + return path; + } + + /** + * @param fs + * the file system abstraction + * @param gitDir + * the .git directory + * @param directory + * the working directory + * @param path + * the tool path + * @return true if tool available and false otherwise + */ + public static boolean isToolAvailable(FS fs, File gitDir, File directory, + String path) { + boolean available = true; + try { + CommandExecutor cmdExec = new CommandExecutor(fs, false); + available = cmdExec.checkExecutable(path, directory, + prepareEnvironment(gitDir, null, null, null, null)); + } catch (Exception e) { + available = false; + } + return available; + } + + /** + * @param defaultName + * the default tool name + * @param userDefinedNames + * the user defined tool names + * @param preDefinedNames + * the pre defined tool names + * @return the sorted tool names set: first element is default tool name if + * valid, then user defined tool names and then pre defined tool + * names + */ + public static Set<String> createSortedToolSet(String defaultName, + Set<String> userDefinedNames, Set<String> preDefinedNames) { + Set<String> names = new LinkedHashSet<>(); + if (defaultName != null) { + // remove defaultName from both sets + Set<String> namesPredef = new LinkedHashSet<>(); + Set<String> namesUser = new LinkedHashSet<>(); + namesUser.addAll(userDefinedNames); + namesUser.remove(defaultName); + namesPredef.addAll(preDefinedNames); + namesPredef.remove(defaultName); + // add defaultName as first in set + names.add(defaultName); + names.addAll(namesUser); + names.addAll(namesPredef); + } else { + names.addAll(userDefinedNames); + names.addAll(preDefinedNames); + } + return names; + } + + /** + * Provides {@link Optional} with the name of an external tool if specified + * in git configuration for a path. + * + * The formed git configuration results from global rules as well as merged + * rules from info and worktree attributes. + * + * Triggers {@link TreeWalk} until specified path found in the tree. + * + * @param repository + * target repository to traverse into + * @param path + * path to the node in repository to parse git attributes for + * @param toolKey + * config key name for the tool + * @return attribute value for the given tool key if set + * @throws ToolException + */ + public static Optional<String> getExternalToolFromAttributes( + final Repository repository, final String path, + final String toolKey) throws ToolException { + try { + WorkingTreeIterator treeIterator = new FileTreeIterator(repository); + try (TreeWalk walk = new TreeWalk(repository)) { + walk.addTree(treeIterator); + walk.setFilter(new NotIgnoredFilter(0)); + while (walk.next()) { + String treePath = walk.getPathString(); + if (treePath.equals(path)) { + Attributes attrs = walk.getAttributes(); + if (attrs.containsKey(toolKey)) { + return Optional.of(attrs.getValue(toolKey)); + } + } + if (walk.isSubtree()) { + walk.enterSubtree(); + } + } + // no external tool specified + return Optional.empty(); + } + + } catch (RevisionSyntaxException | IOException e) { + throw new ToolException(e); + } + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java new file mode 100644 index 0000000000..ba8ca54c58 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java @@ -0,0 +1,262 @@ +/* + * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com> + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.diffmergetool; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Map; + +import org.eclipse.jgit.diff.DiffEntry; + +/** + * The element used as left or right file for compare. + * + */ +public class FileElement { + + /** + * The file element type. + * + */ + public enum Type { + /** + * The local file element (ours). + */ + LOCAL, + /** + * The remote file element (theirs). + */ + REMOTE, + /** + * The merged file element (path in worktree). + */ + MERGED, + /** + * The base file element (of ours and theirs). + */ + BASE, + /** + * The backup file element (copy of merged / conflicted). + */ + BACKUP + } + + private final String path; + + private final Type type; + + private final File workDir; + + private InputStream stream; + + private File tempFile; + + /** + * Creates file element for path. + * + * @param path + * the file path + * @param type + * the element type + */ + public FileElement(String path, Type type) { + this(path, type, null); + } + + /** + * Creates file element for path. + * + * @param path + * the file path + * @param type + * the element type + * @param workDir + * the working directory of the path (can be null, then current + * working dir is used) + */ + public FileElement(String path, Type type, File workDir) { + this(path, type, workDir, null); + } + + /** + * @param path + * the file path + * @param type + * the element type + * @param workDir + * the working directory of the path (can be null, then current + * working dir is used) + * @param stream + * the object stream to load and write on demand, @see getFile(), + * to tempFile once (can be null) + */ + public FileElement(String path, Type type, File workDir, + InputStream stream) { + this.path = path; + this.type = type; + this.workDir = workDir; + this.stream = stream; + } + + /** + * @return the file path + */ + public String getPath() { + return path; + } + + /** + * @return the element type + */ + public Type getType() { + return type; + } + + /** + * Return + * <ul> + * <li>a temporary file if already created and stream is not valid</li> + * <li>OR a real file from work tree: if no temp file was created (@see + * createTempFile()) and if no stream was set</li> + * <li>OR an empty temporary file if path is "/dev/null"</li> + * <li>OR a temporary file with stream content if stream is valid (not + * null); stream is closed and invalidated (set to null) after write to temp + * file, so stream is used only once during first call!</li> + * </ul> + * + * @return the object stream + * @throws IOException + */ + public File getFile() throws IOException { + // if we have already temp file and no stream + // then just return this temp file (it was filled from outside) + if ((tempFile != null) && (stream == null)) { + return tempFile; + } + File file = new File(workDir, path); + // if we have a stream or file is missing (path is "/dev/null") + // then optionally create temporary file and fill it with stream content + if ((stream != null) || isNullPath()) { + if (tempFile == null) { + tempFile = getTempFile(file, type.name(), null); + } + if (stream != null) { + copyFromStream(tempFile, stream); + } + // invalidate the stream, because it is used once + stream = null; + return tempFile; + } + return file; + } + + /** + * Check if path id "/dev/null" + * + * @return true if path is "/dev/null" + */ + public boolean isNullPath() { + return path.equals(DiffEntry.DEV_NULL); + } + + /** + * Create temporary file in given or system temporary directory. + * + * @param directory + * the directory for the file (can be null); if null system + * temporary directory is used + * @return temporary file in directory or in the system temporary directory + * @throws IOException + */ + public File createTempFile(File directory) throws IOException { + if (tempFile == null) { + tempFile = getTempFile(new File(path), type.name(), directory); + } + return tempFile; + } + + /** + * Delete and invalidate temporary file if necessary. + */ + public void cleanTemporaries() { + if (tempFile != null && tempFile.exists()) { + tempFile.delete(); + } + tempFile = null; + } + + /** + * Replace variable in input. + * + * @param input + * the input string + * @return the replaced input string + * @throws IOException + */ + public String replaceVariable(String input) throws IOException { + return input.replace("$" + type.name(), getFile().getPath()); //$NON-NLS-1$ + } + + /** + * Add variable to environment map. + * + * @param env + * the environment where this element should be added + * @throws IOException + */ + public void addToEnv(Map<String, String> env) throws IOException { + env.put(type.name(), getFile().getPath()); + } + + private static File getTempFile(final File file, final String midName, + final File workingDir) throws IOException { + String[] fileNameAndExtension = splitBaseFileNameAndExtension(file); + // TODO: avoid long random file name (number generated by + // createTempFile) + return File.createTempFile( + fileNameAndExtension[0] + "_" + midName + "_", //$NON-NLS-1$ //$NON-NLS-2$ + fileNameAndExtension[1], workingDir); + } + + private static void copyFromStream(final File file, + final InputStream stream) + throws IOException, FileNotFoundException { + try (OutputStream outStream = new FileOutputStream(file)) { + int read = 0; + byte[] bytes = new byte[8 * 1024]; + while ((read = stream.read(bytes)) != -1) { + outStream.write(bytes, 0, read); + } + } finally { + // stream can only be consumed once --> close it and invalidate + stream.close(); + } + } + + private static String[] splitBaseFileNameAndExtension(File file) { + String[] result = new String[2]; + result[0] = file.getName(); + result[1] = ""; //$NON-NLS-1$ + int idx = result[0].lastIndexOf("."); //$NON-NLS-1$ + // if "." was found (>-1) and last-index is not first char (>0), then + // split (same behavior like cgit) + if (idx > 0) { + result[1] = result[0].substring(idx, result[0].length()); + result[0] = result[0].substring(0, idx); + } + return result; + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/InformNoToolHandler.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/InformNoToolHandler.java new file mode 100644 index 0000000000..36b290d37d --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/InformNoToolHandler.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2018-2019, Tim Neumann <Tim.Neumann@advantest.com> + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.diffmergetool; + +import java.util.List; + +/** + * A handler for when the diff/merge tool manager wants to inform the user that + * no tool has been configured and one of the default tools will be used. + */ +public interface InformNoToolHandler { + /** + * Inform the user, that no tool is configured and that one of the given + * tools is used. + * + * @param toolNames + * The tools which are tried + */ + void inform(List<String> toolNames); +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java new file mode 100644 index 0000000000..9625d5f101 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com> + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.diffmergetool; + +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CMD; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_GUITOOL; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_KEEP_BACKUP; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_KEEP_TEMPORARIES; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PATH; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PROMPT; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TOOL; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_TRUST_EXIT_CODE; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_WRITE_TO_TEMP; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGETOOL_SECTION; +import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_MERGE_SECTION; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.Config.SectionParser; +import org.eclipse.jgit.lib.internal.BooleanTriState; + +/** + * Keeps track of merge tool related configuration options. + */ +public class MergeToolConfig { + + /** Key for {@link Config#get(SectionParser)}. */ + public static final Config.SectionParser<MergeToolConfig> KEY = MergeToolConfig::new; + + private final String toolName; + + private final String guiToolName; + + private final boolean prompt; + + private final boolean keepBackup; + + private final boolean keepTemporaries; + + private final boolean writeToTemp; + + private final Map<String, ExternalMergeTool> tools; + + private MergeToolConfig(Config rc) { + toolName = rc.getString(CONFIG_MERGE_SECTION, null, CONFIG_KEY_TOOL); + guiToolName = rc.getString(CONFIG_MERGE_SECTION, null, + CONFIG_KEY_GUITOOL); + prompt = rc.getBoolean(CONFIG_MERGETOOL_SECTION, toolName, + CONFIG_KEY_PROMPT, true); + keepBackup = rc.getBoolean(CONFIG_MERGETOOL_SECTION, + CONFIG_KEY_KEEP_BACKUP, true); + keepTemporaries = rc.getBoolean(CONFIG_MERGETOOL_SECTION, + CONFIG_KEY_KEEP_TEMPORARIES, false); + writeToTemp = rc.getBoolean(CONFIG_MERGETOOL_SECTION, + CONFIG_KEY_WRITE_TO_TEMP, false); + tools = new HashMap<>(); + Set<String> subsections = rc.getSubsections(CONFIG_MERGETOOL_SECTION); + for (String name : subsections) { + String cmd = rc.getString(CONFIG_MERGETOOL_SECTION, name, + CONFIG_KEY_CMD); + String path = rc.getString(CONFIG_MERGETOOL_SECTION, name, + CONFIG_KEY_PATH); + BooleanTriState trustExitCode = BooleanTriState.FALSE; + String trustStr = rc.getString(CONFIG_MERGETOOL_SECTION, name, + CONFIG_KEY_TRUST_EXIT_CODE); + if (trustStr != null) { + trustExitCode = Boolean.valueOf(trustStr).booleanValue() + ? BooleanTriState.TRUE + : BooleanTriState.FALSE; + } else { + trustExitCode = BooleanTriState.UNSET; + } + if ((cmd != null) || (path != null)) { + tools.put(name, new UserDefinedMergeTool(name, path, cmd, + trustExitCode)); + } + } + } + + /** + * @return the default merge tool name (merge.tool) + */ + public String getDefaultToolName() { + return toolName; + } + + /** + * @return the default GUI merge tool name (merge.guitool) + */ + public String getDefaultGuiToolName() { + return guiToolName; + } + + /** + * @return the merge tool "prompt" option (mergetool.prompt) + */ + public boolean isPrompt() { + return prompt; + } + + /** + * @return the tool "keep backup" option + */ + public boolean isKeepBackup() { + return keepBackup; + } + + /** + * @return the tool "keepTemporaries" option + */ + public boolean isKeepTemporaries() { + return keepTemporaries; + } + + /** + * @return the tool "write to temp" option + */ + public boolean isWriteToTemp() { + return writeToTemp; + } + + /** + * @return the tools map + */ + public Map<String, ExternalMergeTool> getTools() { + return tools; + } + + /** + * @return the tool names + */ + public Set<String> getToolNames() { + return tools.keySet(); + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java new file mode 100644 index 0000000000..b903201264 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java @@ -0,0 +1,452 @@ +/* + * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com> + * Copyright (C) 2019, Tim Neumann <tim.neumann@advantest.com> + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.internal.diffmergetool; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; +import java.util.TreeMap; + +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.internal.diffmergetool.FileElement.Type; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.StoredConfig; +import org.eclipse.jgit.lib.internal.BooleanTriState; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.StringUtils; +import org.eclipse.jgit.util.FS.ExecutionResult; + +/** + * Manages merge tools. + */ +public class MergeTools { + + private final FS fs; + + private final File gitDir; + + private final File workTree; + + private final MergeToolConfig config; + + private final Repository repo; + + private final Map<String, ExternalMergeTool> predefinedTools; + + private final Map<String, ExternalMergeTool> userDefinedTools; + + /** + * Creates the external merge-tools manager for given repository. + * + * @param repo + * the repository + */ + public MergeTools(Repository repo) { + this(repo, repo.getConfig()); + } + + /** + * Creates the external diff-tools manager for given configuration. + * + * @param config + * the git configuration + */ + public MergeTools(StoredConfig config) { + this(null, config); + } + + private MergeTools(Repository repo, StoredConfig config) { + this.repo = repo; + this.config = config.get(MergeToolConfig.KEY); + this.gitDir = repo == null ? null : repo.getDirectory(); + this.fs = repo == null ? FS.DETECTED : repo.getFS(); + this.workTree = repo == null ? null : repo.getWorkTree(); + predefinedTools = setupPredefinedTools(); + userDefinedTools = setupUserDefinedTools(predefinedTools); + } + + /** + * Merge two versions of a file with optional base file. + * + * @param localFile + * The local/left version of the file. + * @param remoteFile + * The remote/right version of the file. + * @param mergedFile + * The file for the result. + * @param baseFile + * The base version of the file. May be null. + * @param tempDir + * The tmepDir used for the files. May be null. + * @param toolName + * Optionally the name of the tool to use. If not given the + * default tool will be used. + * @param prompt + * Optionally a flag whether to prompt the user before compare. + * If not given the default will be used. + * @param gui + * A flag whether to prefer a gui tool. + * @param promptHandler + * The handler to use when needing to prompt the user if he wants + * to continue. + * @param noToolHandler + * The handler to use when needing to inform the user, that no + * tool is configured. + * @return the optional result of executing the tool if it was executed + * @throws ToolException + * when the tool fails + */ + public Optional<ExecutionResult> merge(FileElement localFile, + FileElement remoteFile, FileElement mergedFile, + FileElement baseFile, File tempDir, Optional<String> toolName, + BooleanTriState prompt, boolean gui, + PromptContinueHandler promptHandler, + InformNoToolHandler noToolHandler) throws ToolException { + + String toolNameToUse; + + if (toolName == null) { + throw new ToolException(JGitText.get().diffToolNullError); + } + + if (toolName.isPresent()) { + toolNameToUse = toolName.get(); + } else { + toolNameToUse = getDefaultToolName(gui); + + if (StringUtils.isEmptyOrNull(toolNameToUse)) { + noToolHandler.inform(new ArrayList<>(predefinedTools.keySet())); + toolNameToUse = getFirstAvailableTool(); + } + } + + if (StringUtils.isEmptyOrNull(toolNameToUse)) { + throw new ToolException(JGitText.get().diffToolNotGivenError); + } + + boolean doPrompt; + if (prompt != BooleanTriState.UNSET) { + doPrompt = prompt == BooleanTriState.TRUE; + } else { + doPrompt = isInteractive(); + } + + if (doPrompt) { + if (!promptHandler.prompt(toolNameToUse)) { + return Optional.empty(); + } + } + + ExternalMergeTool tool = getTool(toolNameToUse); + if (tool == null) { + throw new ToolException( + "External merge tool is not defined: " + toolNameToUse); //$NON-NLS-1$ + } + + return Optional.of(merge(localFile, remoteFile, mergedFile, baseFile, + tempDir, tool)); + } + + /** + * Merge two versions of a file with optional base file. + * + * @param localFile + * the local file element + * @param remoteFile + * the remote file element + * @param mergedFile + * the merged file element + * @param baseFile + * the base file element (can be null) + * @param tempDir + * the temporary directory (needed for backup and auto-remove, + * can be null) + * @param tool + * the selected tool + * @return the execution result from tool + * @throws ToolException + */ + public ExecutionResult merge(FileElement localFile, FileElement remoteFile, + FileElement mergedFile, FileElement baseFile, File tempDir, + ExternalMergeTool tool) throws ToolException { + FileElement backup = null; + ExecutionResult result = null; + try { + // create additional backup file (copy worktree file) + backup = createBackupFile(mergedFile, + tempDir != null ? tempDir : workTree); + // prepare the command (replace the file paths) + String command = ExternalToolUtils.prepareCommand( + tool.getCommand(baseFile != null), localFile, remoteFile, + mergedFile, baseFile); + // prepare the environment + Map<String, String> env = ExternalToolUtils.prepareEnvironment( + gitDir, localFile, remoteFile, mergedFile, baseFile); + boolean trust = tool.getTrustExitCode() == BooleanTriState.TRUE; + // execute the tool + CommandExecutor cmdExec = new CommandExecutor(fs, trust); + result = cmdExec.run(command, workTree, env); + // keep backup as .orig file + if (backup != null) { + keepBackupFile(mergedFile.getPath(), backup); + } + return result; + } catch (IOException | InterruptedException e) { + throw new ToolException(e); + } finally { + // always delete backup file (ignore that it was may be already + // moved to keep-backup file) + if (backup != null) { + backup.cleanTemporaries(); + } + // if the tool returns an error and keepTemporaries is set to true, + // then these temporary files will be preserved + if (!((result == null) && config.isKeepTemporaries())) { + // delete the files + localFile.cleanTemporaries(); + remoteFile.cleanTemporaries(); + if (baseFile != null) { + baseFile.cleanTemporaries(); + } + // delete temporary directory if needed + if (config.isWriteToTemp() && (tempDir != null) + && tempDir.exists()) { + tempDir.delete(); + } + } + } + } + + private FileElement createBackupFile(FileElement from, File toParentDir) + throws IOException { + FileElement backup = null; + Path path = Paths.get(from.getPath()); + if (Files.exists(path)) { + backup = new FileElement(from.getPath(), Type.BACKUP); + Files.copy(path, backup.createTempFile(toParentDir).toPath(), + StandardCopyOption.REPLACE_EXISTING); + } + return backup; + } + + /** + * Create temporary directory. + * + * @return the created temporary directory if (mergetol.writeToTemp == true) + * or null if not configured or false. + * @throws IOException + */ + public File createTempDirectory() throws IOException { + return config.isWriteToTemp() + ? Files.createTempDirectory("jgit-mergetool-").toFile() //$NON-NLS-1$ + : null; + } + + /** + * Get user defined tool names. + * + * @return the user defined tool names + */ + public Set<String> getUserDefinedToolNames() { + return userDefinedTools.keySet(); + } + + /** + * @return the predefined tool names + */ + public Set<String> getPredefinedToolNames() { + return predefinedTools.keySet(); + } + + /** + * Get all tool names. + * + * @return the all tool names (default or available tool name is the first + * in the set) + */ + public Set<String> getAllToolNames() { + String defaultName = getDefaultToolName(false); + if (defaultName == null) { + defaultName = getFirstAvailableTool(); + } + return ExternalToolUtils.createSortedToolSet(defaultName, + getUserDefinedToolNames(), getPredefinedToolNames()); + } + + /** + * Provides {@link Optional} with the name of an external merge tool if + * specified in git configuration for a path. + * + * The formed git configuration results from global rules as well as merged + * rules from info and worktree attributes. + * + * Triggers {@link TreeWalk} until specified path found in the tree. + * + * @param path + * path to the node in repository to parse git attributes for + * @return name of the difftool if set + * @throws ToolException + */ + public Optional<String> getExternalToolFromAttributes(final String path) + throws ToolException { + return ExternalToolUtils.getExternalToolFromAttributes(repo, path, + ExternalToolUtils.KEY_MERGE_TOOL); + } + + /** + * Checks the availability of the predefined tools in the system. + * + * @return set of predefined available tools + */ + public Set<String> getPredefinedAvailableTools() { + Map<String, ExternalMergeTool> defTools = getPredefinedTools(true); + Set<String> availableTools = new LinkedHashSet<>(); + for (Entry<String, ExternalMergeTool> elem : defTools.entrySet()) { + if (elem.getValue().isAvailable()) { + availableTools.add(elem.getKey()); + } + } + return availableTools; + } + + /** + * @return the user defined tools + */ + public Map<String, ExternalMergeTool> getUserDefinedTools() { + return Collections.unmodifiableMap(userDefinedTools); + } + + /** + * Get predefined tools map. + * + * @param checkAvailability + * true: for checking if tools can be executed; ATTENTION: this + * check took some time, do not execute often (store the map for + * other actions); false: availability is NOT checked: + * isAvailable() returns default false is this case! + * @return the predefined tools with optionally checked availability (long + * running operation) + */ + public Map<String, ExternalMergeTool> getPredefinedTools( + boolean checkAvailability) { + if (checkAvailability) { + for (ExternalMergeTool tool : predefinedTools.values()) { + PreDefinedMergeTool predefTool = (PreDefinedMergeTool) tool; + predefTool.setAvailable(ExternalToolUtils.isToolAvailable(fs, + gitDir, workTree, predefTool.getPath())); + } + } + return Collections.unmodifiableMap(predefinedTools); + } + + /** + * Get first available tool name. + * + * @return the name of first available predefined tool or null + */ + public String getFirstAvailableTool() { + String name = null; + for (ExternalMergeTool tool : predefinedTools.values()) { + if (ExternalToolUtils.isToolAvailable(fs, gitDir, workTree, + tool.getPath())) { + name = tool.getName(); + break; + } + } + return name; + } + + /** + * Is interactive merge (prompt enabled) ? + * + * @return is interactive (config prompt enabled) ? + */ + public boolean isInteractive() { + return config.isPrompt(); + } + + /** + * Get the default (gui-)tool name. + * + * @param gui + * use the diff.guitool setting ? + * @return the default tool name + */ + public String getDefaultToolName(boolean gui) { + return gui ? config.getDefaultGuiToolName() + : config.getDefaultToolName(); + } + + private ExternalMergeTool getTool(final String name) { + ExternalMergeTool tool = userDefinedTools.get(name); + if (tool == null) { + tool = predefinedTools.get(name); + } + return tool; + } + + private void keepBackupFile(String mergedFilePath, FileElement backup) + throws IOException { + if (config.isKeepBackup()) { + Path backupPath = backup.getFile().toPath(); + Files.move(backupPath, + backupPath.resolveSibling( + Paths.get(mergedFilePath).getFileName() + ".orig"), //$NON-NLS-1$ + StandardCopyOption.REPLACE_EXISTING); + } + } + + private Map<String, ExternalMergeTool> setupPredefinedTools() { + Map<String, ExternalMergeTool> tools = new TreeMap<>(); + for (CommandLineMergeTool tool : CommandLineMergeTool.values()) { + tools.put(tool.name(), new PreDefinedMergeTool(tool)); + } + return tools; + } + + private Map<String, ExternalMergeTool> setupUserDefinedTools( + Map<String, ExternalMergeTool> predefTools) { + Map<String, ExternalMergeTool> tools = new TreeMap<>(); + Map<String, ExternalMergeTool> userTools = config.getTools(); + for (String name : userTools.keySet()) { + ExternalMergeTool userTool = userTools.get(name); + // if mergetool.<name>.cmd is defined we have user defined tool + if (userTool.getCommand() != null) { + tools.put(name, userTool); + } else if (userTool.getPath() != null) { + // if mergetool.<name>.path is defined we just overload the path + // of predefined tool + PreDefinedMergeTool predefTool = (PreDefinedMergeTool) predefTools + .get(name); + if (predefTool != null) { + predefTool.setPath(userTool.getPath()); + if (userTool.getTrustExitCode() != BooleanTriState.UNSET) { + predefTool + .setTrustExitCode(userTool.getTrustExitCode()); + } + } + } + } + return tools; + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedDiffTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedDiffTool.java index 1c69fb4911..e1169a2d60 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedDiffTool.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedDiffTool.java @@ -46,17 +46,6 @@ public class PreDefinedDiffTool extends UserDefinedDiffTool { */ @Override public void setPath(String path) { - // handling of spaces in path - if (path.contains(" ")) { //$NON-NLS-1$ - // add quotes before if needed - if (!path.startsWith("\"")) { //$NON-NLS-1$ - path = "\"" + path; //$NON-NLS-1$ - } - // add quotes after if needed - if (!path.endsWith("\"")) { //$NON-NLS-1$ - path = path + "\""; //$NON-NLS-1$ - } - } super.setPath(path); } @@ -67,7 +56,7 @@ public class PreDefinedDiffTool extends UserDefinedDiffTool { */ @Override public String getCommand() { - return getPath() + " " + super.getCommand(); //$NON-NLS-1$ + return ExternalToolUtils.quotePath(getPath()) + " " + super.getCommand(); //$NON-NLS-1$ } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedMergeTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedMergeTool.java new file mode 100644 index 0000000000..7b28d32820 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedMergeTool.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com> + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.diffmergetool; + +import org.eclipse.jgit.lib.internal.BooleanTriState; + +/** + * The pre-defined merge tool. + */ +public class PreDefinedMergeTool extends UserDefinedMergeTool { + + /** + * the tool parameters without base + */ + private final String parametersWithoutBase; + + /** + * Creates the pre-defined merge tool + * + * @param name + * the name + * @param path + * the path + * @param parametersWithBase + * the tool parameters that are used together with path as + * command and "base is present" ($BASE) + * @param parametersWithoutBase + * the tool parameters that are used together with path as + * command and "base is present" ($BASE) + * @param trustExitCode + * the "trust exit code" option + */ + public PreDefinedMergeTool(String name, String path, + String parametersWithBase, String parametersWithoutBase, + BooleanTriState trustExitCode) { + super(name, path, parametersWithBase, trustExitCode); + this.parametersWithoutBase = parametersWithoutBase; + } + + /** + * Creates the pre-defined merge tool + * + * @param tool + * the command line merge tool + * + */ + public PreDefinedMergeTool(CommandLineMergeTool tool) { + this(tool.name(), tool.getPath(), tool.getParameters(true), + tool.getParameters(false), + tool.isExitCodeTrustable() ? BooleanTriState.TRUE + : BooleanTriState.FALSE); + } + + /** + * @param trustExitCode + * the "trust exit code" option + */ + @Override + public void setTrustExitCode(BooleanTriState trustExitCode) { + super.setTrustExitCode(trustExitCode); + } + + /** + * @return the tool command (with base present) + */ + @Override + public String getCommand() { + return getCommand(true); + } + + /** + * @param withBase + * get command with base present (true) or without base present + * (false) + * @return the tool command + */ + @Override + public String getCommand(boolean withBase) { + return ExternalToolUtils.quotePath(getPath()) + " " //$NON-NLS-1$ + + (withBase ? super.getCommand() : parametersWithoutBase); + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PromptContinueHandler.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PromptContinueHandler.java new file mode 100644 index 0000000000..6ad33df2a0 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PromptContinueHandler.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2018-2019, Tim Neumann <Tim.Neumann@advantest.com> + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.diffmergetool; + +/** + * A handler for when the diff/merge tool manager wants to prompt the user + * whether to continue + */ +public interface PromptContinueHandler { + /** + * Prompt the user whether to continue with the next file by opening a given + * tool. + * + * @param toolName + * The name of the tool to open + * @return Whether the user wants to continue + */ + boolean prompt(String toolName); +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java new file mode 100644 index 0000000000..73d3588906 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com> + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.diffmergetool; + +import org.eclipse.jgit.util.FS.ExecutionResult; +import org.eclipse.jgit.util.SystemReader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Tool exception for differentiation. + * + */ +public class ToolException extends Exception { + + private final static Logger LOG = LoggerFactory + .getLogger(ToolException.class); + + private final ExecutionResult result; + + private final boolean commandExecutionError; + + /** + * the serial version UID + */ + private static final long serialVersionUID = 1L; + + /** + * + */ + public ToolException() { + this(null, null, false); + } + + /** + * @param message + * the exception message + */ + public ToolException(String message) { + this(message, null, false); + } + + /** + * @param message + * the exception message + * @param result + * the execution result + * @param commandExecutionError + * is command execution error happened ? + */ + public ToolException(String message, ExecutionResult result, + boolean commandExecutionError) { + super(message); + this.result = result; + this.commandExecutionError = commandExecutionError; + } + + /** + * @param message + * the exception message + * @param cause + * the cause for throw + */ + public ToolException(String message, Throwable cause) { + super(message, cause); + result = null; + commandExecutionError = false; + } + + /** + * @param cause + * the cause for throw + */ + public ToolException(Throwable cause) { + super(cause); + result = null; + commandExecutionError = false; + } + + /** + * @return true if result is valid, false else + */ + public boolean isResult() { + return result != null; + } + + /** + * @return the execution result + */ + public ExecutionResult getResult() { + return result; + } + + /** + * @return true if command execution error appears, false otherwise + */ + public boolean isCommandExecutionError() { + return commandExecutionError; + } + + /** + * @return the result Stderr + */ + public String getResultStderr() { + if (result == null) { + return ""; //$NON-NLS-1$ + } + try { + return new String(result.getStderr().toByteArray(), + SystemReader.getInstance().getDefaultCharset()); + } catch (Exception e) { + LOG.warn("Failed to retrieve standard error output", e); //$NON-NLS-1$ + } + return ""; //$NON-NLS-1$ + } + + /** + * @return the result Stdout + */ + public String getResultStdout() { + if (result == null) { + return ""; //$NON-NLS-1$ + } + try { + return new String(result.getStdout().toByteArray(), + SystemReader.getInstance().getDefaultCharset()); + } catch (Exception e) { + LOG.warn("Failed to retrieve standard output", e); //$NON-NLS-1$ + } + return ""; //$NON-NLS-1$ + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedDiffTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedDiffTool.java index 012296eb35..eb72d01cdb 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedDiffTool.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedDiffTool.java @@ -15,6 +15,8 @@ package org.eclipse.jgit.internal.diffmergetool; */ public class UserDefinedDiffTool implements ExternalDiffTool { + private boolean available; + /** * the diff tool name */ @@ -99,6 +101,23 @@ public class UserDefinedDiffTool implements ExternalDiffTool { } /** + * @return availability of the tool: true if tool can be executed and false + * if not + */ + @Override + public boolean isAvailable() { + return available; + } + + /** + * @param available + * true if tool can be found and false if not + */ + public void setAvailable(boolean available) { + this.available = available; + } + + /** * Overrides the path for the given tool. Equivalent to setting * {@code difftool.<tool>.path}. * diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedMergeTool.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedMergeTool.java new file mode 100644 index 0000000000..1dd2f0d793 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedMergeTool.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com> + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.diffmergetool; + +import org.eclipse.jgit.lib.internal.BooleanTriState; + +/** + * The user-defined merge tool. + */ +public class UserDefinedMergeTool extends UserDefinedDiffTool + implements ExternalMergeTool { + + /** + * the merge tool "trust exit code" option + */ + private BooleanTriState trustExitCode; + + /** + * Creates the merge tool + * + * @param name + * the name + * @param path + * the path + * @param cmd + * the command + * @param trustExitCode + * the "trust exit code" option + */ + public UserDefinedMergeTool(String name, String path, String cmd, + BooleanTriState trustExitCode) { + super(name, path, cmd); + this.trustExitCode = trustExitCode; + } + /** + * @return the "trust exit code" flag + */ + @Override + public BooleanTriState getTrustExitCode() { + return trustExitCode; + } + + /** + * @param trustExitCode + * the new "trust exit code" flag + */ + protected void setTrustExitCode(BooleanTriState trustExitCode) { + this.trustExitCode = trustExitCode; + } + + /** + * @param withBase + * not used, because user-defined merge tool can only define one + * cmd -> it must handle with and without base present (empty) + * @return the tool command + */ + @Override + public String getCommand(boolean withBase) { + return getCommand(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCache.java index 54c527c03c..b30d50921a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCache.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCache.java @@ -12,6 +12,10 @@ package org.eclipse.jgit.internal.storage.dfs; import java.io.IOException; +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReferenceArray; @@ -166,6 +170,12 @@ public final class DfsBlockCache { /** Limits of cache hot count per pack file extension. */ private final int[] cacheHotLimits = new int[PackExt.values().length]; + /** Consumer of loading and eviction events of indexes. */ + private final DfsBlockCacheConfig.IndexEventConsumer indexEventConsumer; + + /** Stores timestamps of the last eviction of indexes. */ + private final Map<EvictKey, Long> indexEvictionMap = new ConcurrentHashMap<>(); + @SuppressWarnings("unchecked") private DfsBlockCache(DfsBlockCacheConfig cfg) { tableSize = tableSize(cfg); @@ -213,6 +223,7 @@ public final class DfsBlockCache { cacheHotLimits[i] = DfsBlockCacheConfig.DEFAULT_CACHE_HOT_MAX; } } + indexEventConsumer = cfg.getIndexEventConsumer(); } boolean shouldCopyThroughCache(long length) { @@ -461,6 +472,7 @@ public final class DfsBlockCache { live -= dead.size; getStat(liveBytes, dead.key).addAndGet(-dead.size); getStat(statEvict, dead.key).incrementAndGet(); + reportIndexEvicted(dead); } while (maxBytes < live); clockHand = prev; } @@ -515,11 +527,13 @@ public final class DfsBlockCache { <T> Ref<T> getOrLoadRef( DfsStreamKey key, long position, RefLoader<T> loader) throws IOException { + long start = System.nanoTime(); int slot = slot(key, position); HashEntry e1 = table.get(slot); Ref<T> ref = scanRef(e1, key, position); if (ref != null) { getStat(statHit, key).incrementAndGet(); + reportIndexRequested(ref, true /* cacheHit */, start); return ref; } @@ -532,6 +546,8 @@ public final class DfsBlockCache { ref = scanRef(e2, key, position); if (ref != null) { getStat(statHit, key).incrementAndGet(); + reportIndexRequested(ref, true /* cacheHit */, + start); return ref; } } @@ -556,6 +572,7 @@ public final class DfsBlockCache { } finally { regionLock.unlock(); } + reportIndexRequested(ref, false /* cacheHit */, start); return ref; } @@ -682,8 +699,9 @@ public final class DfsBlockCache { } private static HashEntry clean(HashEntry top) { - while (top != null && top.ref.next == null) + while (top != null && top.ref.next == null) { top = top.next; + } if (top == null) { return null; } @@ -691,6 +709,44 @@ public final class DfsBlockCache { return n == top.next ? top : new HashEntry(n, top.ref); } + private void reportIndexRequested(Ref<?> ref, boolean cacheHit, + long start) { + if (indexEventConsumer == null + || !isIndexOrBitmapExtPos(ref.key.packExtPos)) { + return; + } + EvictKey evictKey = new EvictKey(ref); + Long prevEvictedTime = indexEvictionMap.get(evictKey); + long now = System.nanoTime(); + long sinceLastEvictionNanos = prevEvictedTime == null ? 0L + : now - prevEvictedTime.longValue(); + indexEventConsumer.acceptRequestedEvent(ref.key.packExtPos, cacheHit, + (now - start) / 1000L /* micros */, ref.size, + Duration.ofNanos(sinceLastEvictionNanos)); + } + + private void reportIndexEvicted(Ref<?> dead) { + if (indexEventConsumer == null + || !indexEventConsumer.shouldReportEvictedEvent() + || !isIndexOrBitmapExtPos(dead.key.packExtPos)) { + return; + } + EvictKey evictKey = new EvictKey(dead); + Long prevEvictedTime = indexEvictionMap.get(evictKey); + long now = System.nanoTime(); + long sinceLastEvictionNanos = prevEvictedTime == null ? 0L + : now - prevEvictedTime.longValue(); + indexEvictionMap.put(evictKey, Long.valueOf(now)); + indexEventConsumer.acceptEvictedEvent(dead.key.packExtPos, dead.size, + dead.totalHitCount.get(), + Duration.ofNanos(sinceLastEvictionNanos)); + } + + private static boolean isIndexOrBitmapExtPos(int packExtPos) { + return packExtPos == PackExt.INDEX.getPosition() + || packExtPos == PackExt.BITMAP_INDEX.getPosition(); + } + private static final class HashEntry { /** Next entry in the hash table's chain list. */ final HashEntry next; @@ -712,6 +768,7 @@ public final class DfsBlockCache { Ref next; private volatile int hotCount; + private AtomicInteger totalHitCount = new AtomicInteger(); Ref(DfsStreamKey key, long position, long size, T v) { this.key = key; @@ -736,6 +793,7 @@ public final class DfsBlockCache { int cap = DfsBlockCache .getInstance().cacheHotLimits[key.packExtPos]; hotCount = Math.min(cap, hotCount + 1); + totalHitCount.incrementAndGet(); } void markColder() { @@ -747,6 +805,34 @@ public final class DfsBlockCache { } } + private static final class EvictKey { + private final int keyHash; + private final int packExtPos; + private final long position; + + EvictKey(Ref<?> ref) { + keyHash = ref.key.hash; + packExtPos = ref.key.packExtPos; + position = ref.position; + } + + @Override + public boolean equals(Object object) { + if (object instanceof EvictKey) { + EvictKey other = (EvictKey) object; + return keyHash == other.keyHash + && packExtPos == other.packExtPos + && position == other.position; + } + return false; + } + + @Override + public int hashCode() { + return DfsBlockCache.getInstance().hash(keyHash, position); + } + } + @FunctionalInterface interface RefLoader<T> { Ref<T> load() throws IOException; @@ -763,4 +849,4 @@ public final class DfsBlockCache { */ ReadableChannel get() throws IOException; } -} +}
\ No newline at end of file diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfig.java index 2716f79a1a..69a37058bf 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfig.java @@ -18,6 +18,7 @@ import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CONCURRENCY_LEVEL; import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_STREAM_RATIO; import java.text.MessageFormat; +import java.time.Duration; import java.util.Collections; import java.util.Map; import java.util.function.Consumer; @@ -46,9 +47,10 @@ public class DfsBlockCacheConfig { private int concurrencyLevel; private Consumer<Long> refLock; - private Map<PackExt, Integer> cacheHotMap; + private IndexEventConsumer indexEventConsumer; + /** * Create a default configuration. */ @@ -216,6 +218,28 @@ public class DfsBlockCacheConfig { } /** + * Get the consumer of cache index events. + * + * @return consumer of cache index events. + */ + public IndexEventConsumer getIndexEventConsumer() { + return indexEventConsumer; + } + + /** + * Set the consumer of cache index events. + * + * @param indexEventConsumer + * consumer of cache index events. + * @return {@code this} + */ + public DfsBlockCacheConfig setIndexEventConsumer( + IndexEventConsumer indexEventConsumer) { + this.indexEventConsumer = indexEventConsumer; + return this; + } + + /** * Update properties by setting fields from the configuration. * <p> * If a property is not defined in the configuration, then it is left @@ -272,4 +296,52 @@ public class DfsBlockCacheConfig { } return this; } -} + + /** Consumer of DfsBlockCache loading and eviction events for indexes. */ + public interface IndexEventConsumer { + /** + * Accept an event of an index requested. It could be loaded from either + * cache or storage. + * + * @param packExtPos + * position in {@code PackExt} enum + * @param cacheHit + * true if an index was already in cache. Otherwise, the + * index was loaded from storage into the cache in the + * current request, + * @param loadMicros + * time to load an index from cache or storage in + * microseconds + * @param bytes + * number of bytes loaded + * @param lastEvictionDuration + * time since last eviction, 0 if was not evicted yet + */ + void acceptRequestedEvent(int packExtPos, boolean cacheHit, + long loadMicros, long bytes, Duration lastEvictionDuration); + + /** + * Accept an event of an index evicted from cache. + * + * @param packExtPos + * position in {@code PackExt} enum + * @param bytes + * number of bytes evicted + * @param totalCacheHitCount + * number of times an index was accessed while in cache + * @param lastEvictionDuration + * time since last eviction, 0 if was not evicted yet + */ + default void acceptEvictedEvent(int packExtPos, long bytes, + int totalCacheHitCount, Duration lastEvictionDuration) { + // Off by default. + } + + /** + * @return true if reporting evicted events is enabled. + */ + default boolean shouldReportEvictedEvent() { + return false; + } + } +}
\ No newline at end of file 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 5b6894da9c..99da222395 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 @@ -165,6 +165,15 @@ public class InMemoryRepository extends DfsRepository { } }; } + + @Override + public long getApproximateObjectCount() { + long count = 0; + for (DfsPackDescription p : packs) { + count += p.getObjectCount(); + } + return count; + } } private static class MemPack extends DfsPackDescription { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/LargePackedWholeObject.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/LargePackedWholeObject.java index b0612f9395..cd4f168d86 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/LargePackedWholeObject.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/LargePackedWholeObject.java @@ -73,7 +73,6 @@ final class LargePackedWholeObject extends ObjectLoader { public ObjectStream openStream() throws MissingObjectException, IOException { PackInputStream packIn; // ctx is closed by PackInputStream, or explicitly in the finally block - @SuppressWarnings("resource") DfsReader ctx = db.newReader(); try { try { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/CachedObjectDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/CachedObjectDirectory.java index 7dedeb57ab..094fdc1559 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/CachedObjectDirectory.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/CachedObjectDirectory.java @@ -263,4 +263,17 @@ class CachedObjectDirectory extends FileObjectDatabase { private AlternateHandle.Id getAlternateId() { return wrapped.getAlternateId(); } + + @Override + public long getApproximateObjectCount() { + long count = 0; + for (Pack p : getPacks()) { + try { + count += p.getObjectCount(); + } catch (IOException e) { + return -1; + } + } + return count; + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableStack.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableStack.java index f02c8613a0..5152367d23 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableStack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableStack.java @@ -40,6 +40,7 @@ import org.eclipse.jgit.internal.storage.reftable.ReftableReader; import org.eclipse.jgit.internal.storage.reftable.ReftableWriter; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.util.FileUtils; +import org.eclipse.jgit.util.SystemReader; /** * A mutable stack of reftables on local filesystem storage. Not thread-safe. @@ -527,11 +528,19 @@ public class FileReftableStack implements AutoCloseable { return false; } + reload(); for (File f : deleteOnSuccess) { - Files.delete(f.toPath()); + try { + Files.delete(f.toPath()); + } catch (IOException e) { + // Ignore: this can happen on Windows in case of concurrent processes. + // leave the garbage and continue. + if (!SystemReader.getInstance().isWindows()) { + throw e; + } + } } - reload(); return true; } finally { if (tmpTable != null) { 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 9cdea597f9..3ebce6c409 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 @@ -594,6 +594,7 @@ public class FileRepository extends Repository { } /** {@inheritDoc} */ + @SuppressWarnings("FutureReturnValueIgnored") @Override public void autoGC(ProgressMonitor monitor) { GC gc = new GC(this); @@ -664,18 +665,20 @@ public class FileRepository extends Repository { if (writeLogs) { List<ReflogEntry> logs = oldDb.getReflogReader(r.getName()) - .getReverseEntries(); + .getReverseEntries(); Collections.reverse(logs); for (ReflogEntry e : logs) { logWriter.log(r.getName(), e); } - } + } } try (RevWalk rw = new RevWalk(this)) { bru.execute(rw, NullProgressMonitor.INSTANCE); } + oldDb.close(); + List<String> failed = new ArrayList<>(); for (ReceiveCommand cmd : bru.getCommands()) { if (cmd.getResult() != ReceiveCommand.Result.OK) { 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 627facca02..06c8cad3ac 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 @@ -212,6 +212,20 @@ public class ObjectDirectory extends FileObjectDatabase { return packed.getPacks(); } + /** {@inheritDoc} */ + @Override + public long getApproximateObjectCount() { + long count = 0; + for (Pack p : getPacks()) { + try { + count += p.getIndex().getObjectCount(); + } catch (IOException e) { + return -1; + } + } + return count; + } + /** * {@inheritDoc} * <p> diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/Pack.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/Pack.java index 289c732f4e..6e74136c1b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/Pack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/Pack.java @@ -50,7 +50,6 @@ import org.eclipse.jgit.errors.UnsupportedPackIndexVersionException; import org.eclipse.jgit.errors.UnsupportedPackVersionException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.internal.storage.pack.BinaryDelta; -import org.eclipse.jgit.internal.storage.pack.ObjectToPack; import org.eclipse.jgit.internal.storage.pack.PackOutputStream; import org.eclipse.jgit.lib.AbbreviatedObjectId; import org.eclipse.jgit.lib.AnyObjectId; @@ -384,7 +383,7 @@ public class Pack implements Iterable<PackIndex.MutableEntry> { final void copyAsIs(PackOutputStream out, LocalObjectToPack src, boolean validate, WindowCursor curs) throws IOException, StoredObjectRepresentationNotAvailableException { - beginCopyAsIs(src); + beginCopyAsIs(); try { copyAsIs2(out, src, validate, curs); } finally { @@ -604,7 +603,7 @@ public class Pack implements Iterable<PackIndex.MutableEntry> { throw new EOFException(); } - private synchronized void beginCopyAsIs(ObjectToPack otp) + private synchronized void beginCopyAsIs() throws StoredObjectRepresentationNotAvailableException { if (++activeCopyRawData == 1 && activeWindows == 0) { try { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFileSnapshot.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFileSnapshot.java index 17bd863528..a784af8c3f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFileSnapshot.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFileSnapshot.java @@ -15,6 +15,7 @@ import java.io.RandomAccessFile; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.util.Equality; class PackFileSnapshot extends FileSnapshot { @@ -61,7 +62,8 @@ class PackFileSnapshot extends FileSnapshot { } boolean isChecksumChanged(File packFile) { - return wasChecksumChanged = checksum != MISSING_CHECKSUM + return wasChecksumChanged = !Equality.isSameInstance(checksum, + MISSING_CHECKSUM) && !checksum.equals(readChecksum(packFile)); } 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 07e38147f7..4aa2edff38 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 @@ -28,7 +28,6 @@ import static org.eclipse.jgit.lib.Ref.Storage.PACKED; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; import java.io.InterruptedIOException; @@ -892,38 +891,27 @@ public class RefDirectory extends RefDatabase { } private PackedRefList readPackedRefs() throws IOException { - int maxStaleRetries = 5; - int retries = 0; - while (true) { - final FileSnapshot snapshot = FileSnapshot.save(packedRefsFile); - final MessageDigest digest = Constants.newMessageDigest(); - try (BufferedReader br = new BufferedReader(new InputStreamReader( - new DigestInputStream(new FileInputStream(packedRefsFile), - digest), - UTF_8))) { - try { - return new PackedRefList(parsePackedRefs(br), snapshot, - ObjectId.fromRaw(digest.digest())); - } catch (IOException e) { - if (FileUtils.isStaleFileHandleInCausalChain(e) - && retries < maxStaleRetries) { - if (LOG.isDebugEnabled()) { - LOG.debug(MessageFormat.format( - JGitText.get().packedRefsHandleIsStale, - Integer.valueOf(retries)), e); + try { + PackedRefList result = FileUtils.readWithRetries(packedRefsFile, + f -> { + FileSnapshot snapshot = FileSnapshot.save(f); + MessageDigest digest = Constants.newMessageDigest(); + try (BufferedReader br = new BufferedReader( + new InputStreamReader( + new DigestInputStream( + new FileInputStream(f), digest), + UTF_8))) { + return new PackedRefList(parsePackedRefs(br), + snapshot, + ObjectId.fromRaw(digest.digest())); } - retries++; - continue; - } - throw e; - } - } catch (FileNotFoundException noPackedRefs) { - if (packedRefsFile.exists()) { - throw noPackedRefs; - } - // Ignore it and leave the new list empty. - return NO_PACKED_REFS; - } + }); + return result != null ? result : NO_PACKED_REFS; + } catch (IOException e) { + throw e; + } catch (Exception e) { + throw new IOException(MessageFormat + .format(JGitText.get().cannotReadFile, packedRefsFile), e); } } @@ -1090,40 +1078,55 @@ public class RefDirectory extends RefDatabase { } final int limit = 4096; - final byte[] buf; - FileSnapshot otherSnapshot = FileSnapshot.save(path); - try { - buf = IO.readSome(path, limit); - } catch (FileNotFoundException noFile) { - if (path.isFile()) { - throw noFile; + + class LooseItems { + final FileSnapshot snapshot; + + final byte[] buf; + + LooseItems(FileSnapshot snapshot, byte[] buf) { + this.snapshot = snapshot; + this.buf = buf; } - return null; // doesn't exist or no file; not a reference. } - - int n = buf.length; + LooseItems loose = null; + try { + loose = FileUtils.readWithRetries(path, + f -> new LooseItems(FileSnapshot.save(f), + IO.readSome(f, limit))); + } catch (IOException e) { + throw e; + } catch (Exception e) { + throw new IOException( + MessageFormat.format(JGitText.get().cannotReadFile, path), + e); + } + if (loose == null) { + return null; + } + int n = loose.buf.length; if (n == 0) return null; // empty file; not a reference. - if (isSymRef(buf, n)) { + if (isSymRef(loose.buf, n)) { if (n == limit) return null; // possibly truncated ref // trim trailing whitespace - while (0 < n && Character.isWhitespace(buf[n - 1])) + while (0 < n && Character.isWhitespace(loose.buf[n - 1])) n--; if (n < 6) { - String content = RawParseUtils.decode(buf, 0, n); + String content = RawParseUtils.decode(loose.buf, 0, n); throw new IOException(MessageFormat.format(JGitText.get().notARef, name, content)); } - final String target = RawParseUtils.decode(buf, 5, n); + final String target = RawParseUtils.decode(loose.buf, 5, n); if (ref != null && ref.isSymbolic() && ref.getTarget().getName().equals(target)) { assert(currentSnapshot != null); - currentSnapshot.setClean(otherSnapshot); + currentSnapshot.setClean(loose.snapshot); return ref; } - return newSymbolicRef(otherSnapshot, name, target); + return newSymbolicRef(loose.snapshot, name, target); } if (n < OBJECT_ID_STRING_LENGTH) @@ -1131,23 +1134,23 @@ public class RefDirectory extends RefDatabase { final ObjectId id; try { - id = ObjectId.fromString(buf, 0); + id = ObjectId.fromString(loose.buf, 0); if (ref != null && !ref.isSymbolic() && id.equals(ref.getTarget().getObjectId())) { assert(currentSnapshot != null); - currentSnapshot.setClean(otherSnapshot); + currentSnapshot.setClean(loose.snapshot); return ref; } } catch (IllegalArgumentException notRef) { - while (0 < n && Character.isWhitespace(buf[n - 1])) + while (0 < n && Character.isWhitespace(loose.buf[n - 1])) n--; - String content = RawParseUtils.decode(buf, 0, n); + String content = RawParseUtils.decode(loose.buf, 0, n); throw new IOException(MessageFormat.format(JGitText.get().notARef, name, content), notRef); } - return new LooseUnpeeled(otherSnapshot, name, id); + return new LooseUnpeeled(loose.snapshot, name, id); } private static boolean isSymRef(byte[] buf, int n) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/io/CancellableDigestOutputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/io/CancellableDigestOutputStream.java new file mode 100644 index 0000000000..ca2095feec --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/io/CancellableDigestOutputStream.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2022, Tencent. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.storage.io; + +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ProgressMonitor; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.io.OutputStream; +import java.security.MessageDigest; + +/** + * An OutputStream that keeps a digest and checks every N bytes for + * cancellation. + */ +public class CancellableDigestOutputStream extends OutputStream { + + /** The OutputStream checks every this value for cancellation **/ + public static final int BYTES_TO_WRITE_BEFORE_CANCEL_CHECK = 128 * 1024; + + private final ProgressMonitor writeMonitor; + + private final OutputStream out; + + private final MessageDigest md = Constants.newMessageDigest(); + + private long count; + + private long checkCancelAt; + + /** + * Initialize a CancellableDigestOutputStream. + * + * @param writeMonitor + * monitor to update on output progress and check cancel. + * @param out + * target stream to receive all contents. + */ + public CancellableDigestOutputStream(ProgressMonitor writeMonitor, + OutputStream out) { + this.writeMonitor = writeMonitor; + this.out = out; + this.checkCancelAt = BYTES_TO_WRITE_BEFORE_CANCEL_CHECK; + } + + /** + * Get the monitor which is used to update on output progress and check + * cancel. + * + * @return the monitor + */ + public final ProgressMonitor getWriteMonitor() { + return writeMonitor; + } + + /** + * Obtain the current SHA-1 digest. + * + * @return SHA-1 digest + */ + public final byte[] getDigest() { + return md.digest(); + } + + /** + * Get total number of bytes written since stream start. + * + * @return total number of bytes written since stream start. + */ + public final long length() { + return count; + } + + /** {@inheritDoc} */ + @Override + public final void write(int b) throws IOException { + if (checkCancelAt <= count) { + if (writeMonitor.isCancelled()) { + throw new InterruptedIOException(); + } + checkCancelAt = count + BYTES_TO_WRITE_BEFORE_CANCEL_CHECK; + } + + out.write(b); + md.update((byte) b); + count++; + } + + /** {@inheritDoc} */ + @Override + public final void write(byte[] b, int off, int len) throws IOException { + while (0 < len) { + if (checkCancelAt <= count) { + if (writeMonitor.isCancelled()) { + throw new InterruptedIOException(); + } + checkCancelAt = count + BYTES_TO_WRITE_BEFORE_CANCEL_CHECK; + } + + int n = Math.min(len, BYTES_TO_WRITE_BEFORE_CANCEL_CHECK); + out.write(b, off, n); + md.update(b, off, n); + count += n; + + off += n; + len -= n; + } + } + + /** {@inheritDoc} */ + @Override + public void flush() throws IOException { + out.flush(); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/BaseSearch.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/BaseSearch.java index 1c24aff12d..cda456c3cb 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/BaseSearch.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/BaseSearch.java @@ -142,6 +142,7 @@ class BaseSearch { return ptr; } + @SuppressWarnings("ReferenceEquality") private void add(AnyObjectId id, int objectType, int pathHash) { ObjectToPack obj = new ObjectToPack(id, objectType); obj.setEdge(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackOutputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackOutputStream.java index 7104b9453e..2d0fe28dae 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackOutputStream.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackOutputStream.java @@ -17,10 +17,8 @@ import static org.eclipse.jgit.lib.Constants.PACK_SIGNATURE; import java.io.IOException; import java.io.OutputStream; -import java.security.MessageDigest; -import org.eclipse.jgit.internal.JGitText; -import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.internal.storage.io.CancellableDigestOutputStream; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.util.NB; @@ -28,25 +26,14 @@ import org.eclipse.jgit.util.NB; * Custom output stream to support * {@link org.eclipse.jgit.internal.storage.pack.PackWriter}. */ -public final class PackOutputStream extends OutputStream { - private static final int BYTES_TO_WRITE_BEFORE_CANCEL_CHECK = 128 * 1024; - - private final ProgressMonitor writeMonitor; - - private final OutputStream out; +public final class PackOutputStream extends CancellableDigestOutputStream { private final PackWriter packWriter; - private final MessageDigest md = Constants.newMessageDigest(); - - private long count; - private final byte[] headerBuffer = new byte[32]; private final byte[] copyBuffer = new byte[64 << 10]; - private long checkCancelAt; - private boolean ofsDelta; /** @@ -66,48 +53,8 @@ public final class PackOutputStream extends OutputStream { */ public PackOutputStream(final ProgressMonitor writeMonitor, final OutputStream out, final PackWriter pw) { - this.writeMonitor = writeMonitor; - this.out = out; + super(writeMonitor, out); this.packWriter = pw; - this.checkCancelAt = BYTES_TO_WRITE_BEFORE_CANCEL_CHECK; - } - - /** {@inheritDoc} */ - @Override - public final void write(int b) throws IOException { - count++; - out.write(b); - md.update((byte) b); - } - - /** {@inheritDoc} */ - @Override - public final void write(byte[] b, int off, int len) - throws IOException { - while (0 < len) { - final int n = Math.min(len, BYTES_TO_WRITE_BEFORE_CANCEL_CHECK); - count += n; - - if (checkCancelAt <= count) { - if (writeMonitor.isCancelled()) { - throw new IOException( - JGitText.get().packingCancelledDuringObjectsWriting); - } - checkCancelAt = count + BYTES_TO_WRITE_BEFORE_CANCEL_CHECK; - } - - out.write(b, off, n); - md.update(b, off, n); - - off += n; - len -= n; - } - } - - /** {@inheritDoc} */ - @Override - public void flush() throws IOException { - out.flush(); } final void writeFileHeader(int version, long objectCount) @@ -160,7 +107,7 @@ public final class PackOutputStream extends OutputStream { ObjectToPack b = otp.getDeltaBase(); if (b != null && (b.isWritten() & ofsDelta)) { // Non-short-circuit logic is intentional int n = objectHeader(rawLength, OBJ_OFS_DELTA, headerBuffer); - n = ofsDelta(count - b.getOffset(), headerBuffer, n); + n = ofsDelta(length() - b.getOffset(), headerBuffer, n); write(headerBuffer, 0, n); } else if (otp.isDeltaRepresentation()) { int n = objectHeader(rawLength, OBJ_REF_DELTA, headerBuffer); @@ -209,20 +156,6 @@ public final class PackOutputStream extends OutputStream { } void endObject() { - writeMonitor.update(1); - } - - /** - * Get total number of bytes written since stream start. - * - * @return total number of bytes written since stream start. - */ - public final long length() { - return count; - } - - /** @return obtain the current SHA-1 digest. */ - final byte[] getDigest() { - return md.digest(); + getWriteMonitor().update(1); } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java index 648d4a1821..659ccb8c55 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java @@ -502,6 +502,98 @@ public class OpenSshConfigFile implements SshConfigStore { } /** + * Converts an OpenSSH time value into a number of seconds. The format is + * defined by OpenSSH as a sequence of (positive) integers with suffixes for + * seconds, minutes, hours, days, and weeks. + * + * @param value + * to convert + * @return the parsed value as a number of seconds, or -1 if the value is + * not a valid OpenSSH time value + * @see <a href="https://man.openbsd.org/sshd_config.5#TIME_FORMATS">OpenBSD + * man 5 sshd_config, section TIME FORMATS</a> + */ + public static int timeSpec(String value) { + if (value == null) { + return -1; + } + try { + int length = value.length(); + int i = 0; + int seconds = 0; + boolean valueSeen = false; + while (i < length) { + // Skip whitespace + char ch = value.charAt(i); + if (Character.isWhitespace(ch)) { + i++; + continue; + } + if (ch == '+') { + // OpenSSH uses strtol with base 10: a leading plus sign is + // allowed. + i++; + } + int val = 0; + int j = i; + while (j < length) { + ch = value.charAt(j++); + if (ch >= '0' && ch <= '9') { + val = Math.addExact(Math.multiplyExact(val, 10), + ch - '0'); + } else { + j--; + break; + } + } + if (i == j) { + // No digits seen + return -1; + } + i = j; + int multiplier = 1; + if (i < length) { + ch = value.charAt(i++); + switch (ch) { + case 's': + case 'S': + break; + case 'm': + case 'M': + multiplier = 60; + break; + case 'h': + case 'H': + multiplier = 3600; + break; + case 'd': + case 'D': + multiplier = 24 * 3600; + break; + case 'w': + case 'W': + multiplier = 7 * 24 * 3600; + break; + default: + if (Character.isWhitespace(ch)) { + break; + } + // Invalid time spec + return -1; + } + } + seconds = Math.addExact(seconds, + Math.multiplyExact(val, multiplier)); + valueSeen = true; + } + return valueSeen ? seconds : -1; + } catch (ArithmeticException e) { + // Overflow + return -1; + } + } + + /** * Retrieves the local user name as given in the constructor. * * @return the user name @@ -549,6 +641,7 @@ public class OpenSshConfigFile implements SshConfigStore { LIST_KEYS.add(SshConstants.GLOBAL_KNOWN_HOSTS_FILE); LIST_KEYS.add(SshConstants.SEND_ENV); LIST_KEYS.add(SshConstants.USER_KNOWN_HOSTS_FILE); + LIST_KEYS.add(SshConstants.ADD_KEYS_TO_AGENT); // confirm timeSpec } /** @@ -871,7 +964,8 @@ public class OpenSshConfigFile implements SshConfigStore { if (options != null) { // HOSTNAME already done above String value = options.get(SshConstants.IDENTITY_AGENT); - if (value != null) { + if (value != null && !SshConstants.NONE.equals(value) + && !SshConstants.ENV_SSH_AUTH_SOCKET.equals(value)) { value = r.substitute(value, Replacer.DEFAULT_TOKENS, true); value = toFile(value, home).getPath(); options.put(SshConstants.IDENTITY_AGENT, value); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/AbbrevConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AbbrevConfig.java new file mode 100644 index 0000000000..9109cfd769 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AbbrevConfig.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2022, Matthias Sohn <matthias.sohn@sap.com> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.lib; + +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_ABBREV_STRING_LENGTH; +import static org.eclipse.jgit.lib.TypedConfigGetter.UNSET_INT; + +import java.text.MessageFormat; + +import org.eclipse.jgit.api.errors.InvalidConfigurationException; +import org.eclipse.jgit.internal.JGitText; + +/** + * Git configuration option <a + * href=https://git-scm.com/docs/git-config#Documentation/git-config.txt-coreabbrev"> + * core.abbrev</a> + * + * @since 6.1 + */ +public final class AbbrevConfig { + private static final String VALUE_NO = "no"; //$NON-NLS-1$ + + private static final String VALUE_AUTO = "auto"; //$NON-NLS-1$ + + /** + * The minimum value of abbrev + */ + public static final int MIN_ABBREV = 4; + + /** + * Cap configured core.abbrev to range between minimum of 4 and number of + * hex-digits of a full object id. + * + * @param len + * configured number of hex-digits to abbreviate object ids to + * @return core.abbrev capped to range between minimum of 4 and number of + * hex-digits of a full object id + */ + public static int capAbbrev(int len) { + return Math.min(Math.max(MIN_ABBREV, len), + Constants.OBJECT_ID_STRING_LENGTH); + } + + /** + * No abbreviation + */ + public final static AbbrevConfig NO = new AbbrevConfig( + Constants.OBJECT_ID_STRING_LENGTH); + + /** + * Parse string value of core.abbrev git option for a given repository + * + * @param repo + * repository + * @return the parsed AbbrevConfig + * @throws InvalidConfigurationException + * if value of core.abbrev is invalid + */ + public static AbbrevConfig parseFromConfig(Repository repo) + throws InvalidConfigurationException { + Config config = repo.getConfig(); + String value = config.getString(ConfigConstants.CONFIG_CORE_SECTION, + null, ConfigConstants.CONFIG_KEY_ABBREV); + if (value == null || value.equalsIgnoreCase(VALUE_AUTO)) { + return auto(repo); + } + if (value.equalsIgnoreCase(VALUE_NO)) { + return NO; + } + try { + int len = config.getIntInRange(ConfigConstants.CONFIG_CORE_SECTION, + ConfigConstants.CONFIG_KEY_ABBREV, MIN_ABBREV, + Constants.OBJECT_ID_STRING_LENGTH, UNSET_INT); + if (len == UNSET_INT) { + // Unset was checked above. If we get UNSET_INT here, then + // either the value was UNSET_INT, or it was an invalid value + // (not an integer, or out of range), and EGit's + // ReportingTypedGetter caught the exception and has logged a + // warning. In either case we should fall back to some sane + // default. + len = OBJECT_ID_ABBREV_STRING_LENGTH; + } + return new AbbrevConfig(len); + } catch (IllegalArgumentException e) { + throw new InvalidConfigurationException(MessageFormat + .format(JGitText.get().invalidCoreAbbrev, value), e); + } + } + + /** + * An appropriate value is computed based on the approximate number of + * packed objects in a repository, which hopefully is enough for abbreviated + * object names to stay unique for some time. + * + * @param repo + * @return appropriate value computed based on the approximate number of + * packed objects in a repository + */ + private static AbbrevConfig auto(Repository repo) { + long count = repo.getObjectDatabase().getApproximateObjectCount(); + if (count == -1) { + return new AbbrevConfig(OBJECT_ID_ABBREV_STRING_LENGTH); + } + // find msb, round to next power of 2 + int len = 63 - Long.numberOfLeadingZeros(count) + 1; + // With the order of 2^len objects, we expect a collision at + // 2^(len/2). But we also care about hex chars, not bits, and + // there are 4 bits per hex. So all together we need to divide + // by 2; but we also want to round odd numbers up, hence adding + // one before dividing. + len = (len + 1) / 2; + // for small repos use at least fallback length + return new AbbrevConfig(Math.max(len, OBJECT_ID_ABBREV_STRING_LENGTH)); + } + + /** + * All other possible abbreviation lengths. Valid range 4 to number of + * hex-digits of an unabbreviated object id (40 for SHA1 object ids, jgit + * doesn't support SHA256 yet). + */ + private int abbrev; + + /** + * @param abbrev + */ + private AbbrevConfig(int abbrev) { + this.abbrev = capAbbrev(abbrev); + } + + /** + * Get the configured abbreviation length for object ids. + * + * @return the configured abbreviation length for object ids + */ + public int get() { + return abbrev; + } + + @Override + public String toString() { + return Integer.toString(abbrev); + } +}
\ No newline at end of file diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BranchConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BranchConfig.java index 6da6f1204a..aa613d07eb 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BranchConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BranchConfig.java @@ -138,6 +138,18 @@ public class BranchConfig { } /** + * Get the remote this branch is configured to push to. + * + * @return the remote this branch is configured to push to, or {@code null} + * if not defined + * @since 6.1 + */ + public String getPushRemote() { + return config.getString(ConfigConstants.CONFIG_BRANCH_SECTION, + branchName, ConfigConstants.CONFIG_KEY_PUSH_REMOTE); + } + + /** * Get the name of the upstream branch as it is called on the remote * * @return the name of the upstream branch as it is called on the remote, or diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitConfig.java index 22e1f98181..6a9b45b065 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 Julian Ruppel <julian.ruppel@sap.com> + * Copyright (c) 2020, 2022 Julian Ruppel <julian.ruppel@sap.com> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -18,15 +18,18 @@ import java.nio.charset.IllegalCharsetNameException; import java.nio.charset.StandardCharsets; import java.nio.charset.UnsupportedCharsetException; import java.text.MessageFormat; +import java.util.Locale; import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.Config.ConfigEnum; import org.eclipse.jgit.lib.Config.SectionParser; import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.IO; import org.eclipse.jgit.util.RawParseUtils; +import org.eclipse.jgit.util.StringUtils; /** * The standard "commit" configuration parameters. @@ -34,22 +37,95 @@ import org.eclipse.jgit.util.RawParseUtils; * @since 5.13 */ public class CommitConfig { + /** * Key for {@link Config#get(SectionParser)}. */ public static final Config.SectionParser<CommitConfig> KEY = CommitConfig::new; + private static final String CUT = " ------------------------ >8 ------------------------\n"; //$NON-NLS-1$ + + private static final char[] COMMENT_CHARS = { '#', ';', '@', '!', '$', '%', + '^', '&', '|', ':' }; + + /** + * How to clean up commit messages when committing. + * + * @since 6.1 + */ + public enum CleanupMode implements ConfigEnum { + + /** + * {@link #WHITESPACE}, additionally remove comment lines. + */ + STRIP, + + /** + * Remove trailing whitespace and leading and trailing empty lines; + * collapse multiple empty lines to a single one. + */ + WHITESPACE, + + /** + * Make no changes. + */ + VERBATIM, + + /** + * Omit everything from the first "scissor" line on, then apply + * {@link #WHITESPACE}. + */ + SCISSORS, + + /** + * Use {@link #STRIP} for user-edited messages, otherwise + * {@link #WHITESPACE}, unless overridden by a git config setting other + * than DEFAULT. + */ + DEFAULT; + + @Override + public String toConfigValue() { + return name().toLowerCase(Locale.ROOT); + } + + @Override + public boolean matchConfigValue(String in) { + return toConfigValue().equals(in); + } + } + private final static Charset DEFAULT_COMMIT_MESSAGE_ENCODING = StandardCharsets.UTF_8; private String i18nCommitEncoding; private String commitTemplatePath; + private CleanupMode cleanupMode; + + private char commentCharacter = '#'; + + private boolean autoCommentChar = false; + private CommitConfig(Config rc) { commitTemplatePath = rc.getString(ConfigConstants.CONFIG_COMMIT_SECTION, null, ConfigConstants.CONFIG_KEY_COMMIT_TEMPLATE); i18nCommitEncoding = rc.getString(ConfigConstants.CONFIG_SECTION_I18N, null, ConfigConstants.CONFIG_KEY_COMMIT_ENCODING); + cleanupMode = rc.getEnum(ConfigConstants.CONFIG_COMMIT_SECTION, null, + ConfigConstants.CONFIG_KEY_CLEANUP, CleanupMode.DEFAULT); + String comment = rc.getString(ConfigConstants.CONFIG_CORE_SECTION, null, + ConfigConstants.CONFIG_KEY_COMMENT_CHAR); + if (!StringUtils.isEmptyOrNull(comment)) { + if ("auto".equalsIgnoreCase(comment)) { //$NON-NLS-1$ + autoCommentChar = true; + } else { + char first = comment.charAt(0); + if (first > ' ' && first < 127) { + commentCharacter = first; + } + } + } } /** @@ -75,6 +151,93 @@ public class CommitConfig { } /** + * Retrieves the comment character set by git config + * {@code core.commentChar}. + * + * @return the character to use for comments in commit messages + * @since 6.2 + */ + public char getCommentChar() { + return commentCharacter; + } + + /** + * Determines the comment character to use for a particular text. If + * {@code core.commentChar} is "auto", tries to determine an unused + * character; if none is found, falls back to '#'. Otherwise returns the + * character given by {@code core.commentChar}. + * + * @param text + * existing text + * + * @return the character to use + * @since 6.2 + */ + public char getCommentChar(String text) { + if (isAutoCommentChar()) { + char toUse = determineCommentChar(text); + if (toUse > 0) { + return toUse; + } + return '#'; + } + return getCommentChar(); + } + + /** + * Tells whether the comment character should be determined by choosing a + * character not occurring in a commit message. + * + * @return {@code true} if git config {@code core.commentChar} is "auto" + * @since 6.2 + */ + public boolean isAutoCommentChar() { + return autoCommentChar; + } + + /** + * Retrieves the {@link CleanupMode} as given by git config + * {@code commit.cleanup}. + * + * @return the {@link CleanupMode}; {@link CleanupMode#DEFAULT} if the git + * config is not set + * @since 6.1 + */ + @NonNull + public CleanupMode getCleanupMode() { + return cleanupMode; + } + + /** + * Computes a non-default {@link CleanupMode} from the given mode and the + * git config. + * + * @param mode + * {@link CleanupMode} to resolve + * @param defaultStrip + * if {@code true} return {@link CleanupMode#STRIP} if the git + * config is also "default", otherwise return + * {@link CleanupMode#WHITESPACE} + * @return the {@code mode}, if it is not {@link CleanupMode#DEFAULT}, + * otherwise the resolved mode, which is never + * {@link CleanupMode#DEFAULT} + * @since 6.1 + */ + @NonNull + public CleanupMode resolve(@NonNull CleanupMode mode, + boolean defaultStrip) { + if (CleanupMode.DEFAULT == mode) { + CleanupMode defaultMode = getCleanupMode(); + if (CleanupMode.DEFAULT == defaultMode) { + return defaultStrip ? CleanupMode.STRIP + : CleanupMode.WHITESPACE; + } + return defaultMode; + } + return mode; + } + + /** * Get the content to the commit template as defined in * {@code commit.template}. If no {@code i18n.commitEncoding} is specified, * UTF-8 fallback is used. @@ -135,4 +298,123 @@ public class CommitConfig { return commitMessageEncoding; } + + /** + * Processes a text according to the given {@link CleanupMode}. + * + * @param text + * text to process + * @param mode + * {@link CleanupMode} to use + * @param commentChar + * comment character (normally {@code #}) to use if {@code mode} + * is {@link CleanupMode#STRIP} or {@link CleanupMode#SCISSORS} + * @return the processed text + * @throws IllegalArgumentException + * if {@code mode} is {@link CleanupMode#DEFAULT} (use + * {@link #resolve(CleanupMode, boolean)} first) + * @since 6.1 + */ + public static String cleanText(@NonNull String text, + @NonNull CleanupMode mode, char commentChar) { + String toProcess = text; + boolean strip = false; + switch (mode) { + case VERBATIM: + return text; + case SCISSORS: + String cut = commentChar + CUT; + if (text.startsWith(cut)) { + return ""; //$NON-NLS-1$ + } + int cutPos = text.indexOf('\n' + cut); + if (cutPos >= 0) { + toProcess = text.substring(0, cutPos + 1); + } + break; + case STRIP: + strip = true; + break; + case WHITESPACE: + break; + case DEFAULT: + default: + // Internal error; no translation + throw new IllegalArgumentException("Invalid clean-up mode " + mode); //$NON-NLS-1$ + } + // WHITESPACE + StringBuilder result = new StringBuilder(); + boolean lastWasEmpty = true; + for (String line : toProcess.split("\n")) { //$NON-NLS-1$ + line = line.stripTrailing(); + if (line.isEmpty()) { + if (!lastWasEmpty) { + result.append('\n'); + lastWasEmpty = true; + } + } else if (!strip || !isComment(line, commentChar)) { + lastWasEmpty = false; + result.append(line).append('\n'); + } + } + int bufferSize = result.length(); + if (lastWasEmpty && bufferSize > 0) { + bufferSize--; + result.setLength(bufferSize); + } + if (bufferSize > 0 && !toProcess.endsWith("\n")) { //$NON-NLS-1$ + if (result.charAt(bufferSize - 1) == '\n') { + result.setLength(bufferSize - 1); + } + } + return result.toString(); + } + + private static boolean isComment(String text, char commentChar) { + int len = text.length(); + for (int i = 0; i < len; i++) { + char ch = text.charAt(i); + if (!Character.isWhitespace(ch)) { + return ch == commentChar; + } + } + return false; + } + + /** + * Determines a comment character by choosing one from a limited set of + * 7-bit ASCII characters that do not occur in the given text at the + * beginning of any line. If none can be determined, {@code (char) 0} is + * returned. + * + * @param text + * to get a comment character for + * @return the comment character, or {@code (char) 0} if none could be + * determined + * @since 6.2 + */ + public static char determineCommentChar(String text) { + if (StringUtils.isEmptyOrNull(text)) { + return '#'; + } + final boolean[] inUse = new boolean[127]; + for (String line : text.split("\n")) { //$NON-NLS-1$ + int len = line.length(); + for (int i = 0; i < len; i++) { + char ch = line.charAt(i); + if (!Character.isWhitespace(ch)) { + if (ch >= 0 && ch < inUse.length) { + inUse[ch] = true; + } + break; + } + } + } + for (char candidate : COMMENT_CHARS) { + if (!inUse[candidate]) { + return candidate; + } + } + return (char) 0; + } }
\ No newline at end of file diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java index 1ce3e312e2..d1d66d280e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java @@ -278,6 +278,54 @@ public class Config { } /** + * Obtain an integer value from the configuration which must be inside given + * range. + * + * @param section + * section the key is grouped within. + * @param name + * name of the key to get. + * @param minValue + * minimum value + * @param maxValue + * maximum value + * @param defaultValue + * default value to return if no value was present. + * @return an integer value from the configuration, or defaultValue. + * @since 6.1 + */ + public int getIntInRange(String section, String name, int minValue, + int maxValue, int defaultValue) { + return typedGetter.getIntInRange(this, section, null, name, minValue, + maxValue, defaultValue); + } + + /** + * Obtain an integer value from the configuration which must be inside given + * range. + * + * @param section + * section the key is grouped within. + * @param subsection + * subsection name, such a remote or branch name. + * @param name + * name of the key to get. + * @param minValue + * minimum value + * @param maxValue + * maximum value + * @param defaultValue + * default value to return if no value was present. + * @return an integer value from the configuration, or defaultValue. + * @since 6.1 + */ + public int getIntInRange(String section, String subsection, String name, + int minValue, int maxValue, int defaultValue) { + return typedGetter.getIntInRange(this, section, subsection, name, + minValue, maxValue, defaultValue); + } + + /** * Obtain an integer value from the configuration. * * @param section diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java index 4b21e4be4e..2342cad0d7 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java @@ -2,7 +2,7 @@ * Copyright (C) 2010, Mathias Kinzler <mathias.kinzler@sap.com> * Copyright (C) 2010, Chris Aniszczyk <caniszczyk@gmail.com> * Copyright (C) 2012-2013, Robin Rosenberg - * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com> and others + * Copyright (C) 2018-2022, Andre Bossert <andre.bossert@siemens.com> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -10,6 +10,7 @@ * * SPDX-License-Identifier: BSD-3-Clause */ + package org.eclipse.jgit.lib; /** @@ -31,14 +32,14 @@ public final class ConfigConstants { public static final String CONFIG_DIFF_SECTION = "diff"; /** - * The "tool" key within "diff" section + * The "tool" key within "diff" or "merge" section * * @since 6.1 */ public static final String CONFIG_KEY_TOOL = "tool"; /** - * The "guitool" key within "diff" section + * The "guitool" key within "diff" or "merge" section * * @since 6.1 */ @@ -52,21 +53,21 @@ public final class ConfigConstants { public static final String CONFIG_DIFFTOOL_SECTION = "difftool"; /** - * The "prompt" key within "difftool" section + * The "prompt" key within "difftool" or "mergetool" section * * @since 6.1 */ public static final String CONFIG_KEY_PROMPT = "prompt"; /** - * The "trustExitCode" key within "difftool" section + * The "trustExitCode" key within "difftool" or "mergetool.<name>." section * * @since 6.1 */ public static final String CONFIG_KEY_TRUST_EXIT_CODE = "trustExitCode"; /** - * The "cmd" key within "difftool.*." section + * The "cmd" key within "difftool.*." or "mergetool.*." section * * @since 6.1 */ @@ -124,6 +125,34 @@ public final class ConfigConstants { public static final String CONFIG_MERGE_SECTION = "merge"; /** + * The "mergetool" section + * + * @since 6.2 + */ + public static final String CONFIG_MERGETOOL_SECTION = "mergetool"; + + /** + * The "keepBackup" key within "mergetool" section + * + * @since 6.2 + */ + public static final String CONFIG_KEY_KEEP_BACKUP = "keepBackup"; + + /** + * The "keepTemporaries" key within "mergetool" section + * + * @since 6.2 + */ + public static final String CONFIG_KEY_KEEP_TEMPORARIES = "keepTemporaries"; + + /** + * The "writeToTemp" key within "mergetool" section + * + * @since 6.2 + */ + public static final String CONFIG_KEY_WRITE_TO_TEMP = "writeToTemp"; + + /** * The "filter" section * @since 4.6 */ @@ -182,6 +211,13 @@ public final class ConfigConstants { public static final String CONFIG_TAG_SECTION = "tag"; /** + * The "cleanup" key + * + * @since 6.1 + */ + public static final String CONFIG_KEY_CLEANUP = "cleanup"; + + /** * The "gpgSign" key * * @since 5.2 @@ -196,6 +232,13 @@ public final class ConfigConstants { public static final String CONFIG_KEY_FORCE_SIGN_ANNOTATED = "forceSignAnnotated"; /** + * The "commentChar" key. + * + * @since 6.2 + */ + public static final String CONFIG_KEY_COMMENT_CHAR = "commentChar"; + + /** * The "hooksPath" key. * * @since 5.6 @@ -322,6 +365,20 @@ public final class ConfigConstants { /** The "remote" key */ public static final String CONFIG_KEY_REMOTE = "remote"; + /** + * The "pushRemote" key. + * + * @since 6.1 + */ + public static final String CONFIG_KEY_PUSH_REMOTE = "pushRemote"; + + /** + * The "pushDefault" key. + * + * @since 6.1 + */ + public static final String CONFIG_KEY_PUSH_DEFAULT = "pushDefault"; + /** The "merge" key */ public static final String CONFIG_KEY_MERGE = "merge"; @@ -801,4 +858,25 @@ public final class ConfigConstants { */ public static final String CONFIG_KEY_SEARCH_FOR_REUSE_TIMEOUT = "searchforreusetimeout"; + /** + * The "push" section. + * + * @since 6.1 + */ + public static final String CONFIG_PUSH_SECTION = "push"; + + /** + * The "default" key. + * + * @since 6.1 + */ + public static final String CONFIG_KEY_DEFAULT = "default"; + + /** + * The "abbrev" key + * + * @since 6.1 + */ + public static final String CONFIG_KEY_ABBREV = "abbrev"; + } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java index 92367ebd0c..cf2e69dbb5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java @@ -48,6 +48,15 @@ public final class Constants { */ public static final int OBJECT_ID_STRING_LENGTH = OBJECT_ID_LENGTH * 2; + /** + * The historic length of an abbreviated Git object hash string. Git 2.11 + * changed this static number to a dynamically calculated one that scales + * as the repository grows. + * + * @since 6.1 + */ + public static final int OBJECT_ID_ABBREV_STRING_LENGTH = 7; + /** Special name for the "HEAD" symbolic-ref. */ public static final String HEAD = "HEAD"; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java index 9f96bce251..86409403b0 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java @@ -120,6 +120,26 @@ public class DefaultTypedConfigGetter implements TypedConfigGetter { /** {@inheritDoc} */ @Override + public int getIntInRange(Config config, String section, String subsection, + String name, int minValue, int maxValue, int defaultValue) { + int val = getInt(config, section, subsection, name, defaultValue); + if ((val >= minValue && val <= maxValue) || val == UNSET_INT) { + return val; + } + if (subsection == null) { + throw new IllegalArgumentException(MessageFormat.format( + JGitText.get().integerValueNotInRange, section, name, + Integer.valueOf(val), Integer.valueOf(minValue), + Integer.valueOf(maxValue))); + } + throw new IllegalArgumentException(MessageFormat.format( + JGitText.get().integerValueNotInRangeSubSection, section, + subsection, name, Integer.valueOf(val), + Integer.valueOf(minValue), Integer.valueOf(maxValue))); + } + + /** {@inheritDoc} */ + @Override public long getLong(Config config, String section, String subsection, String name, long defaultValue) { final String str = config.getString(section, subsection, name); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifierFactory.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifierFactory.java index 4b1dbedeb1..59775c475b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifierFactory.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifierFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others + * Copyright (C) 2021, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -26,20 +26,41 @@ public abstract class GpgSignatureVerifierFactory { private static final Logger LOG = LoggerFactory .getLogger(GpgSignatureVerifierFactory.class); - private static volatile GpgSignatureVerifierFactory defaultFactory = loadDefault(); + private static class DefaultFactory { - private static GpgSignatureVerifierFactory loadDefault() { - try { - ServiceLoader<GpgSignatureVerifierFactory> loader = ServiceLoader - .load(GpgSignatureVerifierFactory.class); - Iterator<GpgSignatureVerifierFactory> iter = loader.iterator(); - if (iter.hasNext()) { - return iter.next(); + private static volatile GpgSignatureVerifierFactory defaultFactory = loadDefault(); + + private static GpgSignatureVerifierFactory loadDefault() { + try { + ServiceLoader<GpgSignatureVerifierFactory> loader = ServiceLoader + .load(GpgSignatureVerifierFactory.class); + Iterator<GpgSignatureVerifierFactory> iter = loader.iterator(); + if (iter.hasNext()) { + return iter.next(); + } + } catch (ServiceConfigurationError e) { + LOG.error(e.getMessage(), e); } - } catch (ServiceConfigurationError e) { - LOG.error(e.getMessage(), e); + return null; + } + + private DefaultFactory() { + // No instantiation + } + + public static GpgSignatureVerifierFactory getDefault() { + return defaultFactory; + } + + /** + * Sets the default factory. + * + * @param factory + * the new default factory + */ + public static void setDefault(GpgSignatureVerifierFactory factory) { + defaultFactory = factory; } - return null; } /** @@ -48,7 +69,7 @@ public abstract class GpgSignatureVerifierFactory { * @return the default factory or {@code null} if none set */ public static GpgSignatureVerifierFactory getDefault() { - return defaultFactory; + return DefaultFactory.getDefault(); } /** @@ -58,7 +79,7 @@ public abstract class GpgSignatureVerifierFactory { * the new default factory */ public static void setDefault(GpgSignatureVerifierFactory factory) { - defaultFactory = factory; + DefaultFactory.setDefault(factory); } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSigner.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSigner.java index 5b32cf0b5f..b25a61b506 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSigner.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSigner.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018, Salesforce. and others + * Copyright (C) 2018, 2022 Salesforce and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -26,22 +26,38 @@ import org.slf4j.LoggerFactory; * @since 5.3 */ public abstract class GpgSigner { + private static final Logger LOG = LoggerFactory.getLogger(GpgSigner.class); - private static GpgSigner defaultSigner = loadGpgSigner(); + private static class DefaultSigner { + + private static volatile GpgSigner defaultSigner = loadGpgSigner(); - private static GpgSigner loadGpgSigner() { - try { - ServiceLoader<GpgSigner> loader = ServiceLoader - .load(GpgSigner.class); - Iterator<GpgSigner> iter = loader.iterator(); - if (iter.hasNext()) { - return iter.next(); + private static GpgSigner loadGpgSigner() { + try { + ServiceLoader<GpgSigner> loader = ServiceLoader + .load(GpgSigner.class); + Iterator<GpgSigner> iter = loader.iterator(); + if (iter.hasNext()) { + return iter.next(); + } + } catch (ServiceConfigurationError e) { + LOG.error(e.getMessage(), e); } - } catch (ServiceConfigurationError e) { - LOG.error(e.getMessage(), e); + return null; + } + + private DefaultSigner() { + // No instantiation + } + + public static GpgSigner getDefault() { + return defaultSigner; + } + + public static void setDefault(GpgSigner signer) { + defaultSigner = signer; } - return null; } /** @@ -50,7 +66,7 @@ public abstract class GpgSigner { * @return the default signer, or <code>null</code>. */ public static GpgSigner getDefault() { - return defaultSigner; + return DefaultSigner.getDefault(); } /** @@ -61,7 +77,7 @@ public abstract class GpgSigner { * default. */ public static void setDefault(GpgSigner signer) { - GpgSigner.defaultSigner = signer; + DefaultSigner.setDefault(signer); } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java index 28ea927b14..df9fd47efa 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java @@ -568,6 +568,9 @@ public class IndexDiff { if (ignoreSubmoduleMode != IgnoreSubmoduleMode.ALL) { try (SubmoduleWalk smw = new SubmoduleWalk(repository)) { smw.setTree(new DirCacheIterator(dirCache)); + if (filter != null) { + smw.setFilter(filter); + } smw.setBuilderFactory(factory); while (smw.next()) { IgnoreSubmoduleMode localIgnoreSubmoduleMode = ignoreSubmoduleMode; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectDatabase.java index 04262c07ae..70009cba35 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectDatabase.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectDatabase.java @@ -155,4 +155,14 @@ public abstract class ObjectDatabase implements AutoCloseable { public ObjectDatabase newCachedDatabase() { return this; } + + /** + * Get a quick, rough count of objects in this repository. Ignores loose + * objects. Returns {@code -1} if an exception occurs. + * + * @return quick, rough count of objects in this repository, {@code -1} if + * an exception occurs + * @since 6.1 + */ + public abstract long getApproximateObjectCount(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectReader.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectReader.java index a2c7381ce7..26c3ff6718 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectReader.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectReader.java @@ -10,6 +10,8 @@ package org.eclipse.jgit.lib; +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_ABBREV_STRING_LENGTH; + import java.io.IOException; import java.util.ArrayList; import java.util.Collection; @@ -76,7 +78,7 @@ public abstract class ObjectReader implements AutoCloseable { */ public AbbreviatedObjectId abbreviate(AnyObjectId objectId) throws IOException { - return abbreviate(objectId, 7); + return abbreviate(objectId, OBJECT_ID_ABBREV_STRING_LENGTH); } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/PersonIdent.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PersonIdent.java index 428a6b959c..93710299b4 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/PersonIdent.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PersonIdent.java @@ -14,6 +14,8 @@ package org.eclipse.jgit.lib; import java.io.Serializable; import java.text.SimpleDateFormat; +import java.time.Instant; +import java.time.ZoneId; import java.util.Date; import java.util.Locale; import java.util.TimeZone; @@ -206,6 +208,20 @@ public class PersonIdent implements Serializable { } /** + * Copy a {@link org.eclipse.jgit.lib.PersonIdent}, but alter the clone's + * time stamp + * + * @param pi + * original {@link org.eclipse.jgit.lib.PersonIdent} + * @param aWhen + * local time as Instant + * @since 6.1 + */ + public PersonIdent(PersonIdent pi, Instant aWhen) { + this(pi.getName(), pi.getEmailAddress(), aWhen.toEpochMilli(), pi.tzOffset); + } + + /** * Construct a PersonIdent from simple data * * @param aName a {@link java.lang.String} object. @@ -222,6 +238,27 @@ public class PersonIdent implements Serializable { } /** + * Construct a PersonIdent from simple data + * + * @param aName + * a {@link java.lang.String} object. + * @param aEmailAddress + * a {@link java.lang.String} object. + * @param aWhen + * local time stamp + * @param zoneId + * time zone id + * @since 6.1 + */ + public PersonIdent(final String aName, String aEmailAddress, Instant aWhen, + ZoneId zoneId) { + this(aName, aEmailAddress, aWhen.toEpochMilli(), + TimeZone.getTimeZone(zoneId) + .getOffset(aWhen + .toEpochMilli()) / (60 * 1000)); + } + + /** * Copy a PersonIdent, but alter the clone's time stamp * * @param pi @@ -304,6 +341,16 @@ public class PersonIdent implements Serializable { } /** + * Get when attribute as instant + * + * @return timestamp + * @since 6.1 + */ + public Instant getWhenAsInstant() { + return Instant.ofEpochMilli(when); + } + + /** * Get this person's declared time zone * * @return this person's declared time zone; null if time zone is unknown. @@ -313,6 +360,16 @@ public class PersonIdent implements Serializable { } /** + * Get the time zone id + * + * @return the time zone id + * @since 6.1 + */ + public ZoneId getZoneId() { + return getTimeZone().toZoneId(); + } + + /** * Get this person's declared time zone as minutes east of UTC. * * @return this person's declared time zone as minutes east of UTC. If the diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TypedConfigGetter.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TypedConfigGetter.java index 0f2f6cff8a..c4eb8f10d5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TypedConfigGetter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TypedConfigGetter.java @@ -29,6 +29,13 @@ import org.eclipse.jgit.util.FS; public interface TypedConfigGetter { /** + * Use {@code Integer#MIN_VALUE} as unset int value + * + * @since 6.1 + */ + public static final int UNSET_INT = Integer.MIN_VALUE; + + /** * Get a boolean value from a git {@link Config}. * * @param config @@ -87,6 +94,32 @@ public interface TypedConfigGetter { int defaultValue); /** + * Obtain an integer value from a git {@link Config} which must be in given + * range. + * + * @param config + * to get the value from + * @param section + * section the key is grouped within. + * @param subsection + * subsection name, such a remote or branch name. + * @param name + * name of the key to get. + * @param minValue + * minimal value + * @param maxValue + * maximum value + * @param defaultValue + * default value to return if no value was present. Use + * {@code #UNSET_INT} to set the default to unset. + * @return an integer value from the configuration, or defaultValue. + * {@code #UNSET_INT} if unset. + * @since 6.1 + */ + int getIntInRange(Config config, String section, String subsection, + String name, int minValue, int maxValue, int defaultValue); + + /** * Obtain a long value from a git {@link Config}. * * @param config 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 f7966a267f..e0c083f55c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java @@ -92,36 +92,62 @@ public class MergeMessageFormatter { } /** - * Add section with conflicting paths to merge message. + * Add section with conflicting paths to merge message. Lines are prefixed + * with a hash. * * @param message * the original merge message * @param conflictingPaths * the paths with conflicts * @return merge message with conflicting paths added + * @deprecated since 6.1; use + * {@link #formatWithConflicts(String, Iterable, char)} instead */ + @Deprecated public String formatWithConflicts(String message, List<String> conflictingPaths) { + return formatWithConflicts(message, conflictingPaths, '#'); + } + + /** + * Add section with conflicting paths to merge message. + * + * @param message + * the original merge message + * @param conflictingPaths + * the paths with conflicts + * @param commentChar + * comment character to use for prefixing the conflict lines + * @return merge message with conflicting paths added + * @since 6.1 + */ + public String formatWithConflicts(String message, + Iterable<String> conflictingPaths, char commentChar) { StringBuilder sb = new StringBuilder(); String[] lines = message.split("\n"); //$NON-NLS-1$ int firstFooterLine = ChangeIdUtil.indexOfFirstFooterLine(lines); - for (int i = 0; i < firstFooterLine; i++) + for (int i = 0; i < firstFooterLine; i++) { sb.append(lines[i]).append('\n'); - if (firstFooterLine == lines.length && message.length() != 0) + } + if (firstFooterLine == lines.length && message.length() != 0) { sb.append('\n'); - addConflictsMessage(conflictingPaths, sb); - if (firstFooterLine < lines.length) + } + addConflictsMessage(conflictingPaths, sb, commentChar); + if (firstFooterLine < lines.length) { sb.append('\n'); - for (int i = firstFooterLine; i < lines.length; i++) + } + for (int i = firstFooterLine; i < lines.length; i++) { sb.append(lines[i]).append('\n'); + } return sb.toString(); } - private static void addConflictsMessage(List<String> conflictingPaths, - StringBuilder sb) { - sb.append("Conflicts:\n"); //$NON-NLS-1$ + private static void addConflictsMessage(Iterable<String> conflictingPaths, + StringBuilder sb, char commentChar) { + sb.append(commentChar).append(" Conflicts:\n"); //$NON-NLS-1$ for (String conflictingPath : conflictingPaths) { - sb.append('\t').append(conflictingPath).append('\n'); + sb.append(commentChar).append('\t').append(conflictingPath) + .append('\n'); } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java index 7767662867..b9ab1d1b7a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java @@ -3,7 +3,7 @@ * Copyright (C) 2010-2012, Matthias Sohn <matthias.sohn@sap.com> * Copyright (C) 2012, Research In Motion Limited * Copyright (C) 2017, Obeo (mathieu.cartaud@obeo.fr) - * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> and others + * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -276,11 +276,15 @@ public class ResolveMerger extends ThreeWayMerger { private ContentMergeStrategy contentStrategy = ContentMergeStrategy.CONFLICT; /** - * Keeps {@link CheckoutMetadata} for {@link #checkout()} and - * {@link #cleanUp()}. + * Keeps {@link CheckoutMetadata} for {@link #checkout()}. */ private Map<String, CheckoutMetadata> checkoutMetadata; + /** + * Keeps {@link CheckoutMetadata} for {@link #cleanUp()}. + */ + private Map<String, CheckoutMetadata> cleanupMetadata; + private static MergeAlgorithm getMergeAlgorithm(Config config) { SupportedAlgorithm diffAlg = config.getEnum( CONFIG_DIFF_SECTION, null, CONFIG_KEY_ALGORITHM, @@ -383,12 +387,14 @@ public class ResolveMerger extends ThreeWayMerger { } if (!inCore) { checkoutMetadata = new HashMap<>(); + cleanupMetadata = new HashMap<>(); } try { return mergeTrees(mergeBase(), sourceTrees[0], sourceTrees[1], false); } finally { checkoutMetadata = null; + cleanupMetadata = null; if (implicitDirCache) { dircache.unlock(); } @@ -447,7 +453,7 @@ public class ResolveMerger extends ThreeWayMerger { DirCacheEntry entry = dc.getEntry(mpath); if (entry != null) { DirCacheCheckout.checkoutEntry(db, entry, reader, false, - checkoutMetadata.get(mpath)); + cleanupMetadata.get(mpath)); } mpathsIt.remove(); } @@ -501,22 +507,26 @@ public class ResolveMerger extends ThreeWayMerger { * Remembers the {@link CheckoutMetadata} for the given path; it may be * needed in {@link #checkout()} or in {@link #cleanUp()}. * + * @param map + * to add the metadata to * @param path * of the current node * @param attributes - * for the current node + * to use for determining the metadata * @throws IOException * if the smudge filter cannot be determined - * @since 5.1 + * @since 6.1 */ - protected void addCheckoutMetadata(String path, Attributes attributes) + protected void addCheckoutMetadata(Map<String, CheckoutMetadata> map, + String path, Attributes attributes) throws IOException { - if (checkoutMetadata != null) { + if (map != null) { EolStreamType eol = EolStreamTypeUtil.detectStreamType( - OperationType.CHECKOUT_OP, workingTreeOptions, attributes); + OperationType.CHECKOUT_OP, workingTreeOptions, + attributes); CheckoutMetadata data = new CheckoutMetadata(eol, - tw.getFilterCommand(Constants.ATTR_FILTER_TYPE_SMUDGE)); - checkoutMetadata.put(path, data); + tw.getSmudgeCommand(attributes)); + map.put(path, data); } } @@ -529,15 +539,17 @@ public class ResolveMerger extends ThreeWayMerger { * @param entry * to add * @param attributes - * for the current entry + * the {@link Attributes} of the trees * @throws IOException * if the {@link CheckoutMetadata} cannot be determined - * @since 5.1 + * @since 6.1 */ protected void addToCheckout(String path, DirCacheEntry entry, - Attributes attributes) throws IOException { + Attributes[] attributes) + throws IOException { toBeCheckedOut.put(path, entry); - addCheckoutMetadata(path, attributes); + addCheckoutMetadata(cleanupMetadata, path, attributes[T_OURS]); + addCheckoutMetadata(checkoutMetadata, path, attributes[T_THEIRS]); } /** @@ -549,7 +561,7 @@ public class ResolveMerger extends ThreeWayMerger { * @param isFile * whether it is a file * @param attributes - * for the entry + * to use for determining the {@link CheckoutMetadata} * @throws IOException * if the {@link CheckoutMetadata} cannot be determined * @since 5.1 @@ -558,7 +570,7 @@ public class ResolveMerger extends ThreeWayMerger { Attributes attributes) throws IOException { toBeDeleted.add(path); if (isFile) { - addCheckoutMetadata(path, attributes); + addCheckoutMetadata(cleanupMetadata, path, attributes); } } @@ -599,7 +611,7 @@ public class ResolveMerger extends ThreeWayMerger { * see * {@link org.eclipse.jgit.merge.ResolveMerger#mergeTrees(AbstractTreeIterator, RevTree, RevTree, boolean)} * @param attributes - * the attributes defined for this entry + * the {@link Attributes} for the three trees * @return <code>false</code> if the merge will fail because the index entry * didn't match ours or the working-dir file was dirty and a * conflict occurred @@ -607,12 +619,12 @@ public class ResolveMerger extends ThreeWayMerger { * @throws org.eclipse.jgit.errors.IncorrectObjectTypeException * @throws org.eclipse.jgit.errors.CorruptObjectException * @throws java.io.IOException - * @since 4.9 + * @since 6.1 */ protected boolean processEntry(CanonicalTreeParser base, CanonicalTreeParser ours, CanonicalTreeParser theirs, DirCacheBuildIterator index, WorkingTreeIterator work, - boolean ignoreConflicts, Attributes attributes) + boolean ignoreConflicts, Attributes[] attributes) throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, IOException { enterSubtree = true; @@ -729,7 +741,7 @@ public class ResolveMerger extends ThreeWayMerger { // Base, ours, and theirs all contain a folder: don't delete return true; } - addDeletion(tw.getPathString(), nonTree(modeO), attributes); + addDeletion(tw.getPathString(), nonTree(modeO), attributes[T_OURS]); return true; } @@ -772,7 +784,7 @@ public class ResolveMerger extends ThreeWayMerger { if (nonTree(modeO) && nonTree(modeT)) { // Check worktree before modifying files boolean worktreeDirty = isWorktreeDirty(work, ourDce); - if (!attributes.canBeContentMerged() && worktreeDirty) { + if (!attributes[T_OURS].canBeContentMerged() && worktreeDirty) { return false; } @@ -791,7 +803,7 @@ public class ResolveMerger extends ThreeWayMerger { mergeResults.put(tw.getPathString(), result); unmergedPaths.add(tw.getPathString()); return true; - } else if (!attributes.canBeContentMerged()) { + } else if (!attributes[T_OURS].canBeContentMerged()) { // File marked as binary switch (getContentMergeStrategy()) { case OURS: @@ -842,13 +854,16 @@ public class ResolveMerger extends ThreeWayMerger { if (ignoreConflicts) { result.setContainsConflicts(false); } - updateIndex(base, ours, theirs, result, attributes); + updateIndex(base, ours, theirs, result, attributes[T_OURS]); String currentPath = tw.getPathString(); if (result.containsConflicts() && !ignoreConflicts) { unmergedPaths.add(currentPath); } modifiedFiles.add(currentPath); - addCheckoutMetadata(currentPath, attributes); + addCheckoutMetadata(cleanupMetadata, currentPath, + attributes[T_OURS]); + addCheckoutMetadata(checkoutMetadata, currentPath, + attributes[T_THEIRS]); } else if (modeO != modeT) { // OURS or THEIRS has been deleted if (((modeO != 0 && !tw.idEqual(T_BASE, T_OURS)) || (modeT != 0 && !tw @@ -881,7 +896,8 @@ public class ResolveMerger extends ThreeWayMerger { // markers). But also stage 0 of the index is filled // with that content. result.setContainsConflicts(false); - updateIndex(base, ours, theirs, result, attributes); + updateIndex(base, ours, theirs, result, + attributes[T_OURS]); } else { add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0); @@ -896,11 +912,9 @@ public class ResolveMerger extends ThreeWayMerger { if (isWorktreeDirty(work, ourDce)) { return false; } - if (nonTree(modeT)) { - if (e != null) { - addToCheckout(tw.getPathString(), e, - attributes); - } + if (nonTree(modeT) && e != null) { + addToCheckout(tw.getPathString(), e, + attributes); } } @@ -945,14 +959,16 @@ public class ResolveMerger extends ThreeWayMerger { */ private MergeResult<RawText> contentMerge(CanonicalTreeParser base, CanonicalTreeParser ours, CanonicalTreeParser theirs, - Attributes attributes, ContentMergeStrategy strategy) + Attributes[] attributes, ContentMergeStrategy strategy) throws BinaryBlobException, IOException { + // TW: The attributes here are used to determine the LFS smudge filter. + // Is doing a content merge on LFS items really a good idea?? RawText baseText = base == null ? RawText.EMPTY_TEXT - : getRawText(base.getEntryObjectId(), attributes); + : getRawText(base.getEntryObjectId(), attributes[T_BASE]); RawText ourText = ours == null ? RawText.EMPTY_TEXT - : getRawText(ours.getEntryObjectId(), attributes); + : getRawText(ours.getEntryObjectId(), attributes[T_OURS]); RawText theirsText = theirs == null ? RawText.EMPTY_TEXT - : getRawText(theirs.getEntryObjectId(), attributes); + : getRawText(theirs.getEntryObjectId(), attributes[T_THEIRS]); mergeAlgorithm.setContentMergeStrategy(strategy); return mergeAlgorithm.merge(RawTextComparator.DEFAULT, baseText, ourText, theirsText); @@ -1342,7 +1358,7 @@ public class ResolveMerger extends ThreeWayMerger { tw = new NameConflictTreeWalk(db, reader); tw.addTree(baseTree); - tw.addTree(headTree); + tw.setHead(tw.addTree(headTree)); tw.addTree(mergeTree); int dciPos = tw.addTree(buildIt); if (workingTreeIterator != null) { @@ -1403,6 +1419,13 @@ public class ResolveMerger extends ThreeWayMerger { boolean hasAttributeNodeProvider = treeWalk .getAttributesNodeProvider() != null; while (treeWalk.next()) { + Attributes[] attributes = { NO_ATTRIBUTES, NO_ATTRIBUTES, + NO_ATTRIBUTES }; + if (hasAttributeNodeProvider) { + attributes[T_BASE] = treeWalk.getAttributes(T_BASE); + attributes[T_OURS] = treeWalk.getAttributes(T_OURS); + attributes[T_THEIRS] = treeWalk.getAttributes(T_THEIRS); + } if (!processEntry( treeWalk.getTree(T_BASE, CanonicalTreeParser.class), treeWalk.getTree(T_OURS, CanonicalTreeParser.class), @@ -1410,9 +1433,7 @@ public class ResolveMerger extends ThreeWayMerger { treeWalk.getTree(T_INDEX, DirCacheBuildIterator.class), hasWorkingTreeIterator ? treeWalk.getTree(T_FILE, WorkingTreeIterator.class) : null, - ignoreConflicts, hasAttributeNodeProvider - ? treeWalk.getAttributes() - : NO_ATTRIBUTES)) { + ignoreConflicts, attributes)) { cleanUp(); return false; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommit.java b/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommit.java index 94e7c53adb..c11fca13d8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommit.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommit.java @@ -138,6 +138,7 @@ public class PlotCommit<L extends PlotLane> extends RevCommit { * the commit to test. * @return true if the given commit built on top of this commit. */ + @SuppressWarnings("ReferenceEquality") public final boolean isChild(PlotCommit c) { for (PlotCommit a : children) if (a == c) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommitList.java b/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommitList.java index 18ea7560fd..458f240982 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommitList.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommitList.java @@ -92,6 +92,7 @@ public class PlotCommitList<L extends PlotLane> extends } /** {@inheritDoc} */ + @SuppressWarnings("ReferenceEquality") @Override protected void enter(int index, PlotCommit<L> currCommit) { setupChildren(currCommit); @@ -188,6 +189,7 @@ public class PlotCommitList<L extends PlotLane> extends * may be null if <code>currCommit</code> is the first commit on * the lane */ + @SuppressWarnings("ReferenceEquality") private void handleBlockedLanes(final int index, final PlotCommit currCommit, final PlotCommit childOnLane) { for (PlotCommit child : currCommit.children) { @@ -214,6 +216,7 @@ public class PlotCommitList<L extends PlotLane> extends } // Handles the case where currCommit is a non-first parent of the child + @SuppressWarnings("ReferenceEquality") private PlotLane handleMerge(final int index, final PlotCommit currCommit, final PlotCommit childOnLane, PlotCommit child, PlotLane laneToUse) { @@ -287,6 +290,7 @@ public class PlotCommitList<L extends PlotLane> extends * @param child * @param laneToContinue */ + @SuppressWarnings("ReferenceEquality") private void drawLaneToChild(final int commitIndex, PlotCommit child, PlotLane laneToContinue) { for (int r = commitIndex - 1; r >= 0; r--) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ObjectWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ObjectWalk.java index e6f9580bf7..4e48a5c328 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ObjectWalk.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ObjectWalk.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others + * Copyright (C) 2008, 2022 Shawn O. Pearce <spearce@spearce.org> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -139,7 +139,7 @@ public class ObjectWalk extends RevWalk { * the repository the walker will obtain data from. */ public ObjectWalk(Repository repo) { - this(repo.newObjectReader()); + this(repo.newObjectReader(), true); } /** @@ -151,7 +151,11 @@ public class ObjectWalk extends RevWalk { * required. */ public ObjectWalk(ObjectReader or) { - super(or); + this(or, false); + } + + private ObjectWalk(ObjectReader or, boolean closeReader) { + super(or, closeReader); setRetainBody(false); rootObjects = new ArrayList<>(); pendingObjects = new BlockObjQueue(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java index 8d571f5b14..a25948e50b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java @@ -143,8 +143,19 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { */ static final int TOPO_QUEUED = 1 << 6; + /** + * Set on a RevCommit when a {@link TreeRevFilter} has been applied. + * <p> + * This flag is processed by the {@link RewriteGenerator} to check if a + * {@link TreeRevFilter} has been applied. + * + * @see TreeRevFilter + * @see RewriteGenerator + */ + static final int TREE_REV_FILTER_APPLIED = 1 << 7; + /** Number of flag bits we keep internal for our own use. See above flags. */ - static final int RESERVED_FLAGS = 7; + static final int RESERVED_FLAGS = 8; private static final int APP_FLAGS = -1 & ~((1 << RESERVED_FLAGS) - 1); @@ -215,7 +226,7 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable { this(or, false); } - private RevWalk(ObjectReader or, boolean closeReader) { + RevWalk(ObjectReader or, boolean closeReader) { reader = or; idBuffer = new MutableObjectId(); objects = new ObjectIdOwnerMap<>(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteGenerator.java index a928c2e79b..1adef07ad9 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteGenerator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteGenerator.java @@ -24,14 +24,7 @@ import org.eclipse.jgit.errors.MissingObjectException; * commit that matched the revision walker's filters. * <p> * This generator is the second phase of a path limited revision walk and - * assumes it is receiving RevCommits from {@link TreeRevFilter}, - * after they have been fully buffered by {@link AbstractRevQueue}. The full - * buffering is necessary to allow the simple loop used within our own - * {@link #rewrite(RevCommit)} to pull completely through a strand of - * {@link RevWalk#REWRITE} colored commits and come up with a simplification - * that makes the DAG dense. Not fully buffering the commits first would cause - * this loop to abort early, due to commits not being parsed and colored - * correctly. + * assumes it is receiving RevCommits from {@link TreeRevFilter}. * * @see TreeRevFilter */ @@ -43,9 +36,12 @@ class RewriteGenerator extends Generator { private final Generator source; + private final FIFORevQueue pending; + RewriteGenerator(Generator s) { super(s.firstParent); source = s; + pending = new FIFORevQueue(s.firstParent); } @Override @@ -58,13 +54,23 @@ class RewriteGenerator extends Generator { return source.outputType() & ~NEEDS_REWRITE; } + @SuppressWarnings("ReferenceEquality") @Override RevCommit next() throws MissingObjectException, IncorrectObjectTypeException, IOException { - final RevCommit c = source.next(); + RevCommit c = pending.next(); + if (c == null) { - return null; + c = source.next(); + if (c == null) { + // We are done: Both the source generator and our internal list + // are completely exhausted. + return null; + } } + + applyFilterToParents(c); + boolean rewrote = false; final RevCommit[] pList = c.parents; final int nParents = pList.length; @@ -90,10 +96,41 @@ class RewriteGenerator extends Generator { return c; } - private RevCommit rewrite(RevCommit p) { + /** + * Makes sure that the {@link TreeRevFilter} has been applied to all parents + * of this commit by the previous {@link PendingGenerator}. + * + * @param c + * @throws MissingObjectException + * @throws IncorrectObjectTypeException + * @throws IOException + */ + private void applyFilterToParents(RevCommit c) + throws MissingObjectException, IncorrectObjectTypeException, + IOException { + for (RevCommit parent : c.parents) { + while ((parent.flags & RevWalk.TREE_REV_FILTER_APPLIED) == 0) { + + RevCommit n = source.next(); + + if (n != null) { + pending.add(n); + } else { + // Source generator is exhausted; filter has been applied to + // all commits + return; + } + + } + + } + } + + private RevCommit rewrite(RevCommit p) throws MissingObjectException, + IncorrectObjectTypeException, IOException { for (;;) { - final RevCommit[] pList = p.parents; - if (pList.length > 1) { + + if (p.parents.length > 1) { // This parent is a merge, so keep it. // return p; @@ -113,14 +150,16 @@ class RewriteGenerator extends Generator { return p; } - if (pList.length == 0) { + if (p.parents.length == 0) { // We can't go back any further, other than to // just delete the parent entirely. // return null; } - p = pList[0]; + applyFilterToParents(p.parents[0]); + p = p.parents[0]; + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/StartGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/StartGenerator.java index bfcea6ea8f..a79901ca10 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/StartGenerator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/StartGenerator.java @@ -125,12 +125,6 @@ class StartGenerator extends Generator { } if ((g.outputType() & NEEDS_REWRITE) != 0) { - // Correction for an upstream NEEDS_REWRITE is to buffer - // fully and then apply a rewrite generator that can - // pull through the rewrite chain and produce a dense - // output graph. - // - g = new FIFORevQueue(g); g = new RewriteGenerator(g); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TreeRevFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TreeRevFilter.java index 822fc5320c..92d72268d1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TreeRevFilter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TreeRevFilter.java @@ -41,6 +41,8 @@ public class TreeRevFilter extends RevFilter { private static final int UNINTERESTING = RevWalk.UNINTERESTING; + private static final int FILTER_APPLIED = RevWalk.TREE_REV_FILTER_APPLIED; + private final int rewriteFlag; private final TreeWalk pathFilter; @@ -101,6 +103,7 @@ public class TreeRevFilter extends RevFilter { public boolean include(RevWalk walker, RevCommit c) throws StopWalkException, MissingObjectException, IncorrectObjectTypeException, IOException { + c.flags |= FILTER_APPLIED; // Reset the tree filter to scan this commit and parents. // RevCommit[] pList = c.parents; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileBasedConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileBasedConfig.java index 7b5f00e4fe..cba5e1697c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileBasedConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileBasedConfig.java @@ -20,7 +20,6 @@ import static java.nio.charset.StandardCharsets.UTF_8; import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.FileNotFoundException; import java.io.IOException; import java.text.MessageFormat; @@ -37,15 +36,11 @@ import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FileUtils; import org.eclipse.jgit.util.IO; import org.eclipse.jgit.util.RawParseUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * The configuration file that is stored in the file of the file system. */ public class FileBasedConfig extends StoredConfig { - private static final Logger LOG = LoggerFactory - .getLogger(FileBasedConfig.class); private final File configFile; @@ -115,16 +110,15 @@ public class FileBasedConfig extends StoredConfig { */ @Override public void load() throws IOException, ConfigInvalidException { - final int maxRetries = 5; - int retryDelayMillis = 20; - int retries = 0; - while (true) { - final FileSnapshot oldSnapshot = snapshot; - final FileSnapshot newSnapshot; - // don't use config in this snapshot to avoid endless recursion - newSnapshot = FileSnapshot.saveNoConfig(getFile()); - try { - final byte[] in = IO.readFully(getFile()); + try { + FileSnapshot[] lastSnapshot = { null }; + Boolean wasRead = FileUtils.readWithRetries(getFile(), f -> { + final FileSnapshot oldSnapshot = snapshot; + final FileSnapshot newSnapshot; + // don't use config in this snapshot to avoid endless recursion + newSnapshot = FileSnapshot.saveNoConfig(f); + lastSnapshot[0] = newSnapshot; + final byte[] in = IO.readFully(f); final ObjectId newHash = hash(in); if (hash.equals(newHash)) { if (oldSnapshot.equals(newSnapshot)) { @@ -145,47 +139,17 @@ public class FileBasedConfig extends StoredConfig { snapshot = newSnapshot; hash = newHash; } - return; - } catch (FileNotFoundException noFile) { - // might be locked by another process (see exception Javadoc) - if (retries < maxRetries && configFile.exists()) { - if (LOG.isDebugEnabled()) { - LOG.debug(MessageFormat.format( - JGitText.get().configHandleMayBeLocked, - Integer.valueOf(retries)), noFile); - } - try { - Thread.sleep(retryDelayMillis); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - retries++; - retryDelayMillis *= 2; // max wait 1260 ms - continue; - } - if (configFile.exists()) { - throw noFile; - } + return Boolean.TRUE; + }); + if (wasRead == null) { clear(); - snapshot = newSnapshot; - return; - } catch (IOException e) { - if (FileUtils.isStaleFileHandle(e) - && retries < maxRetries) { - if (LOG.isDebugEnabled()) { - LOG.debug(MessageFormat.format( - JGitText.get().configHandleIsStale, - Integer.valueOf(retries)), e); - } - retries++; - continue; - } - throw new IOException(MessageFormat - .format(JGitText.get().cannotReadFile, getFile()), e); - } catch (ConfigInvalidException e) { - throw new ConfigInvalidException(MessageFormat - .format(JGitText.get().cannotReadFile, getFile()), e); + snapshot = lastSnapshot[0]; } + } catch (IOException e) { + throw e; + } catch (Exception e) { + throw new ConfigInvalidException(MessageFormat + .format(JGitText.get().cannotReadFile, getFile()), e); } } @@ -216,9 +180,10 @@ public class FileBasedConfig extends StoredConfig { } final LockFile lf = new LockFile(getFile()); - if (!lf.lock()) - throw new LockFailedException(getFile()); try { + if (!lf.lock()) { + throw new LockFailedException(getFile()); + } lf.setNeedSnapshotNoConfig(true); lf.write(out); if (!lf.commit()) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/AmazonS3.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AmazonS3.java index d56b5b320d..81a70af2d2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/AmazonS3.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AmazonS3.java @@ -86,6 +86,12 @@ import org.xml.sax.helpers.DefaultHandler; public class AmazonS3 { private static final Set<String> SIGNED_HEADERS; + private static final String AWS_API_V2 = "2"; //$NON-NLS-1$ + + private static final String AWS_API_V4 = "4"; //$NON-NLS-1$ + + private static final String AWS_S3_SERVICE_NAME = "s3"; //$NON-NLS-1$ + private static final String HMAC = "HmacSHA1"; //$NON-NLS-1$ private static final String X_AMZ_ACL = "x-amz-acl"; //$NON-NLS-1$ @@ -135,11 +141,17 @@ public class AmazonS3 { } } + /** AWS API Signature Version. */ + private final String awsApiSignatureVersion; + /** AWSAccessKeyId, public string that identifies the user's account. */ private final String publicKey; /** Decoded form of the private AWSSecretAccessKey, to sign requests. */ - private final SecretKeySpec privateKey; + private final SecretKeySpec secretKeySpec; + + /** AWSSecretAccessKey, private string used to access a user's account. */ + private final char[] secretKey; // store as char[] for security /** Our HTTP proxy support, in case we are behind a firewall. */ private final ProxySelector proxySelector; @@ -159,8 +171,12 @@ public class AmazonS3 { /** S3 Bucket Domain. */ private final String domain; + /** S3 Region. */ + private final String region; + /** Property names used in amazon connection configuration file. */ interface Keys { + String AWS_API_SIGNATURE_VERSION = "aws.api.signature.version"; //$NON-NLS-1$ String ACCESS_KEY = "accesskey"; //$NON-NLS-1$ String SECRET_KEY = "secretkey"; //$NON-NLS-1$ String PASSWORD = "password"; //$NON-NLS-1$ @@ -168,6 +184,7 @@ public class AmazonS3 { String CRYPTO_VER = "crypto.version"; //$NON-NLS-1$ String ACL = "acl"; //$NON-NLS-1$ String DOMAIN = "domain"; //$NON-NLS-1$ + String REGION = "region"; //$NON-NLS-1$ String HTTP_RETRY = "httpclient.retry-max"; //$NON-NLS-1$ String TMP_DIR = "tmpdir"; //$NON-NLS-1$ } @@ -180,6 +197,12 @@ public class AmazonS3 { * For example: * * <pre> + * # AWS API signature version, must be one of: + * # 2 - deprecated (not supported in all AWS regions) + * # 4 - latest (supported in all AWS regions) + * # Defaults to 2. + * aws.api.signature.version: 4 + * * # AWS Access and Secret Keys (required) * accesskey: <YourAWSAccessKey> * secretkey: <YourAWSSecretKey> @@ -192,6 +215,9 @@ public class AmazonS3 { * # AWS S3 Region Domain (defaults to s3.amazonaws.com) * domain: s3.amazonaws.com * + * # AWS S3 Region (required if aws.api.signature.version = 4) + * region: us-west-2 + * * # Number of times to retry after internal error from S3. * httpclient.retry-max: 3 * @@ -204,16 +230,34 @@ public class AmazonS3 { * connection properties. */ public AmazonS3(final Properties props) { + awsApiSignatureVersion = props + .getProperty(Keys.AWS_API_SIGNATURE_VERSION, AWS_API_V2); + if (awsApiSignatureVersion.equals(AWS_API_V4)) { + region = props.getProperty(Keys.REGION); + if (region == null) { + throw new IllegalArgumentException( + JGitText.get().missingAwsRegion); + } + } else if (awsApiSignatureVersion.equals(AWS_API_V2)) { + region = null; + } else { + throw new IllegalArgumentException(MessageFormat.format( + JGitText.get().invalidAwsApiSignatureVersion, + awsApiSignatureVersion)); + } + domain = props.getProperty(Keys.DOMAIN, "s3.amazonaws.com"); //$NON-NLS-1$ publicKey = props.getProperty(Keys.ACCESS_KEY); if (publicKey == null) throw new IllegalArgumentException(JGitText.get().missingAccesskey); - final String secret = props.getProperty(Keys.SECRET_KEY); - if (secret == null) + final String secretKeyStr = props.getProperty(Keys.SECRET_KEY); + if (secretKeyStr == null) { throw new IllegalArgumentException(JGitText.get().missingSecretkey); - privateKey = new SecretKeySpec(Constants.encodeASCII(secret), HMAC); + } + secretKeySpec = new SecretKeySpec(Constants.encodeASCII(secretKeyStr), HMAC); + secretKey = secretKeyStr.toCharArray(); final String pacl = props.getProperty(Keys.ACL, "PRIVATE"); //$NON-NLS-1$ if (StringUtils.equalsIgnoreCase("PRIVATE", pacl)) //$NON-NLS-1$ @@ -258,7 +302,7 @@ public class AmazonS3 { throws IOException { for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) { final HttpURLConnection c = open("GET", bucket, key); //$NON-NLS-1$ - authorize(c); + authorize(c, Collections.emptyMap(), 0, null); switch (HttpSupport.response(c)) { case HttpURLConnection.HTTP_OK: encryption.validate(c, X_AMZ_META); @@ -339,7 +383,7 @@ public class AmazonS3 { throws IOException { for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) { final HttpURLConnection c = open("DELETE", bucket, key); //$NON-NLS-1$ - authorize(c); + authorize(c, Collections.emptyMap(), 0, null); switch (HttpSupport.response(c)) { case HttpURLConnection.HTTP_NO_CONTENT: return; @@ -385,13 +429,16 @@ public class AmazonS3 { } final String md5str = Base64.encodeBytes(newMD5().digest(data)); + final String bodyHash = awsApiSignatureVersion.equals(AWS_API_V4) + ? AwsRequestSignerV4.calculateBodyHash(data) + : null; final String lenstr = String.valueOf(data.length); for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) { final HttpURLConnection c = open("PUT", bucket, key); //$NON-NLS-1$ c.setRequestProperty("Content-Length", lenstr); //$NON-NLS-1$ c.setRequestProperty("Content-MD5", md5str); //$NON-NLS-1$ c.setRequestProperty(X_AMZ_ACL, acl); - authorize(c); + authorize(c, Collections.emptyMap(), data.length, bodyHash); c.setDoOutput(true); c.setFixedLengthStreamingMode(data.length); try (OutputStream os = c.getOutputStream()) { @@ -466,6 +513,9 @@ public class AmazonS3 { monitorTask = MessageFormat.format(JGitText.get().progressMonUploading, key); final String md5str = Base64.encodeBytes(csum); + final String bodyHash = awsApiSignatureVersion.equals(AWS_API_V4) + ? AwsRequestSignerV4.calculateBodyHash(buf.toByteArray()) + : null; final long len = buf.length(); for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) { final HttpURLConnection c = open("PUT", bucket, key); //$NON-NLS-1$ @@ -473,7 +523,7 @@ public class AmazonS3 { c.setRequestProperty("Content-MD5", md5str); //$NON-NLS-1$ c.setRequestProperty(X_AMZ_ACL, acl); encryption.request(c, X_AMZ_META); - authorize(c); + authorize(c, Collections.emptyMap(), len, bodyHash); c.setDoOutput(true); monitor.beginTask(monitorTask, (int) (len / 1024)); try (OutputStream os = c.getOutputStream()) { @@ -545,8 +595,13 @@ public class AmazonS3 { urlstr.append('.'); urlstr.append(domain); urlstr.append('/'); - if (key.length() > 0) - HttpSupport.encode(urlstr, key); + if (key.length() > 0) { + if (awsApiSignatureVersion.equals(AWS_API_V2)) { + HttpSupport.encode(urlstr, key); + } else if (awsApiSignatureVersion.equals(AWS_API_V4)) { + urlstr.append(key); + } + } if (!args.isEmpty()) { final Iterator<Map.Entry<String, String>> i; @@ -573,7 +628,18 @@ public class AmazonS3 { return c; } - void authorize(HttpURLConnection c) throws IOException { + void authorize(HttpURLConnection httpURLConnection, + Map<String, String> queryParams, long contentLength, + final String bodyHash) throws IOException { + if (awsApiSignatureVersion.equals(AWS_API_V2)) { + authorizeV2(httpURLConnection); + } else if (awsApiSignatureVersion.equals(AWS_API_V4)) { + AwsRequestSignerV4.sign(httpURLConnection, queryParams, contentLength, bodyHash, AWS_S3_SERVICE_NAME, + region, publicKey, secretKey); + } + } + + void authorizeV2(HttpURLConnection c) throws IOException { final Map<String, List<String>> reqHdr = c.getRequestProperties(); final SortedMap<String, String> sigHdr = new TreeMap<>(); for (Map.Entry<String, List<String>> entry : reqHdr.entrySet()) { @@ -610,7 +676,7 @@ public class AmazonS3 { final String sec; try { final Mac m = Mac.getInstance(HMAC); - m.init(privateKey); + m.init(secretKeySpec); sec = Base64.encodeBytes(m.doFinal(s.toString().getBytes(UTF_8))); } catch (NoSuchAlgorithmException e) { throw new IOException(MessageFormat.format(JGitText.get().noHMACsupport, HMAC, e.getMessage())); @@ -674,7 +740,7 @@ public class AmazonS3 { for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) { final HttpURLConnection c = open("GET", bucket, "", args); //$NON-NLS-1$ //$NON-NLS-2$ - authorize(c); + authorize(c, args, 0, null); switch (HttpSupport.response(c)) { case HttpURLConnection.HTTP_OK: truncated = false; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/AwsRequestSignerV4.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AwsRequestSignerV4.java new file mode 100644 index 0000000000..6b3d39721a --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AwsRequestSignerV4.java @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2022, Workday Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.transport; + +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.util.Hex; +import org.eclipse.jgit.util.HttpSupport; + +/** + * Utility class for signing requests to AWS service endpoints using the V4 + * signing protocol. + * + * Reference implementation: <a href= + * "https://docs.aws.amazon.com/AmazonS3/latest/API/samples/AWSS3SigV4JavaSamples.zip">AWSS3SigV4JavaSamples.zip</a> + * + * @see <a href= + * "https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html">AWS + * Signature Version 4</a> + * + * @since 5.13 + */ +public final class AwsRequestSignerV4 { + + /** AWS version 4 signing algorithm (for authorization header). **/ + private static final String ALGORITHM = "HMAC-SHA256"; //$NON-NLS-1$ + + /** Java Message Authentication Code (MAC) algorithm name. **/ + private static final String MAC_ALGORITHM = "HmacSHA256"; //$NON-NLS-1$ + + /** AWS version 4 signing scheme. **/ + private static final String SCHEME = "AWS4"; //$NON-NLS-1$ + + /** AWS version 4 terminator string. **/ + private static final String TERMINATOR = "aws4_request"; //$NON-NLS-1$ + + /** SHA-256 hash of an empty request body. **/ + private static final String EMPTY_BODY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; //$NON-NLS-1$ + + /** Date format for the 'x-amz-date' header. **/ + private static final DateTimeFormatter AMZ_DATE_FORMAT = DateTimeFormatter + .ofPattern("yyyyMMdd'T'HHmmss'Z'"); //$NON-NLS-1$ + + /** Date format for the string-to-sign's scope. **/ + private static final DateTimeFormatter SCOPE_DATE_FORMAT = DateTimeFormatter + .ofPattern("yyyyMMdd"); //$NON-NLS-1$ + + private AwsRequestSignerV4() { + // Don't instantiate utility class + } + + /** + * Sign the provided request with an AWS4 signature as the 'Authorization' + * header. + * + * @param httpURLConnection + * The request to sign. + * @param queryParameters + * The query parameters being sent in the request. + * @param contentLength + * The content length of the data being sent in the request + * @param bodyHash + * Hex-encoded SHA-256 hash of the data being sent in the request + * @param serviceName + * The signing name of the AWS service (e.g. "s3"). + * @param regionName + * The name of the AWS region that will handle the request (e.g. + * "us-east-1"). + * @param awsAccessKey + * The user's AWS Access Key. + * @param awsSecretKey + * The user's AWS Secret Key. + */ + public static void sign(HttpURLConnection httpURLConnection, + Map<String, String> queryParameters, long contentLength, + String bodyHash, String serviceName, String regionName, + String awsAccessKey, char[] awsSecretKey) { + // get request headers + Map<String, String> headers = new HashMap<>(); + httpURLConnection.getRequestProperties() + .forEach((headerName, headerValues) -> headers.put(headerName, + String.join(",", headerValues))); //$NON-NLS-1$ + + // add required content headers + if (contentLength > 0) { + headers.put(HttpSupport.HDR_CONTENT_LENGTH, + String.valueOf(contentLength)); + } else { + bodyHash = EMPTY_BODY_SHA256; + } + headers.put("x-amz-content-sha256", bodyHash); //$NON-NLS-1$ + + // add the 'x-amz-date' header + OffsetDateTime now = Instant.now().atOffset(ZoneOffset.UTC); + String amzDate = now.format(AMZ_DATE_FORMAT); + headers.put("x-amz-date", amzDate); //$NON-NLS-1$ + + // add the 'host' header + URL endpointUrl = httpURLConnection.getURL(); + int port = endpointUrl.getPort(); + String hostHeader = (port > -1) + ? endpointUrl.getHost().concat(":" + port) //$NON-NLS-1$ + : endpointUrl.getHost(); + headers.put("Host", hostHeader); //$NON-NLS-1$ + + // construct the canonicalized request + String canonicalizedHeaderNames = getCanonicalizeHeaderNames(headers); + String canonicalizedHeaders = getCanonicalizedHeaderString(headers); + String canonicalizedQueryParameters = getCanonicalizedQueryString( + queryParameters); + String httpMethod = httpURLConnection.getRequestMethod(); + String canonicalRequest = httpMethod + '\n' + + getCanonicalizedResourcePath(endpointUrl) + '\n' + + canonicalizedQueryParameters + '\n' + canonicalizedHeaders + + '\n' + canonicalizedHeaderNames + '\n' + bodyHash; + + // construct the string-to-sign + String scopeDate = now.format(SCOPE_DATE_FORMAT); + String scope = scopeDate + '/' + regionName + '/' + serviceName + '/' + + TERMINATOR; + String stringToSign = SCHEME + '-' + ALGORITHM + '\n' + amzDate + '\n' + + scope + '\n' + Hex.toHexString(hash( + canonicalRequest.getBytes(StandardCharsets.UTF_8))); + + // compute the signing key + byte[] secretKey = (SCHEME + new String(awsSecretKey)).getBytes(); + byte[] dateKey = signStringWithKey(scopeDate, secretKey); + byte[] regionKey = signStringWithKey(regionName, dateKey); + byte[] serviceKey = signStringWithKey(serviceName, regionKey); + byte[] signingKey = signStringWithKey(TERMINATOR, serviceKey); + byte[] signature = signStringWithKey(stringToSign, signingKey); + + // construct the authorization header + String credentialsAuthorizationHeader = "Credential=" + awsAccessKey //$NON-NLS-1$ + + '/' + scope; + String signedHeadersAuthorizationHeader = "SignedHeaders=" //$NON-NLS-1$ + + canonicalizedHeaderNames; + String signatureAuthorizationHeader = "Signature=" //$NON-NLS-1$ + + Hex.toHexString(signature); + String authorizationHeader = SCHEME + '-' + ALGORITHM + ' ' + + credentialsAuthorizationHeader + ", " //$NON-NLS-1$ + + signedHeadersAuthorizationHeader + ", " //$NON-NLS-1$ + + signatureAuthorizationHeader; + + // Copy back the updated request headers + headers.forEach(httpURLConnection::setRequestProperty); + + // Add the 'authorization' header + httpURLConnection.setRequestProperty(HttpSupport.HDR_AUTHORIZATION, + authorizationHeader); + } + + /** + * Calculates the hex-encoded SHA-256 hash of the provided byte array. + * + * @param data + * Byte array to hash + * + * @return Hex-encoded SHA-256 hash of the provided byte array. + */ + public static String calculateBodyHash(final byte[] data) { + return (data == null || data.length < 1) ? EMPTY_BODY_SHA256 + : Hex.toHexString(hash(data)); + } + + /** + * Construct a string listing all request headers in sorted case-insensitive + * order, separated by a ';'. + * + * @param headers + * Map containing all request headers. + * + * @return String that lists all request headers in sorted case-insensitive + * order, separated by a ';'. + */ + private static String getCanonicalizeHeaderNames( + Map<String, String> headers) { + return headers.keySet().stream().map(String::toLowerCase).sorted() + .collect(Collectors.joining(";")); //$NON-NLS-1$ + } + + /** + * Constructs the canonical header string for a request. + * + * @param headers + * Map containing all request headers. + * + * @return The canonical headers with values for the request. + */ + private static String getCanonicalizedHeaderString( + Map<String, String> headers) { + if (headers == null || headers.isEmpty()) { + return ""; //$NON-NLS-1$ + } + StringBuilder sb = new StringBuilder(); + headers.keySet().stream().sorted(String.CASE_INSENSITIVE_ORDER) + .forEach(key -> { + String header = key.toLowerCase().replaceAll("\\s+", " "); //$NON-NLS-1$ //$NON-NLS-2$ + String value = headers.get(key).replaceAll("\\s+", " "); //$NON-NLS-1$ //$NON-NLS-2$ + sb.append(header).append(':').append(value).append('\n'); + }); + return sb.toString(); + } + + /** + * Constructs the canonicalized resource path for an AWS service endpoint. + * + * @param url + * The AWS service endpoint URL, including the path to any + * resource. + * + * @return The canonicalized resource path for the AWS service endpoint. + */ + private static String getCanonicalizedResourcePath(URL url) { + if (url == null) { + return "/"; //$NON-NLS-1$ + } + String path = url.getPath(); + if (path == null || path.isEmpty()) { + return "/"; //$NON-NLS-1$ + } + String encodedPath = HttpSupport.urlEncode(path, true); + if (encodedPath.startsWith("/")) { //$NON-NLS-1$ + return encodedPath; + } + return "/".concat(encodedPath); //$NON-NLS-1$ + } + + /** + * Constructs the canonicalized query string for a request. + * + * @param queryParameters + * The query parameters in the request. + * + * @return The canonicalized query string for the request. + */ + public static String getCanonicalizedQueryString( + Map<String, String> queryParameters) { + if (queryParameters == null || queryParameters.isEmpty()) { + return ""; //$NON-NLS-1$ + } + return queryParameters + .keySet().stream().sorted().map( + key -> HttpSupport.urlEncode(key, false) + '=' + + HttpSupport.urlEncode( + queryParameters.get(key), false)) + .collect(Collectors.joining("&")); //$NON-NLS-1$ + } + + /** + * Hashes the provided byte array using the SHA-256 algorithm. + * + * @param data + * The byte array to hash. + * + * @return Hashed string contents of the provided byte array. + */ + public static byte[] hash(byte[] data) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-256"); //$NON-NLS-1$ + md.update(data); + return md.digest(); + } catch (Exception e) { + throw new RuntimeException( + JGitText.get().couldNotHashByteArrayWithSha256, e); + } + } + + /** + * Signs the provided string data using the specified key. + * + * @param stringToSign + * The string data to sign. + * @param key + * The key material of the secret key. + * + * @return Signed string data. + */ + private static byte[] signStringWithKey(String stringToSign, byte[] key) { + try { + byte[] data = stringToSign.getBytes(StandardCharsets.UTF_8); + Mac mac = Mac.getInstance(MAC_ALGORITHM); + mac.init(new SecretKeySpec(key, MAC_ALGORITHM)); + return mac.doFinal(data); + } catch (Exception e) { + throw new RuntimeException(JGitText.get().couldNotSignStringWithKey, + e); + } + } + +} 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 3826bf7401..09c559d7b5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java @@ -210,9 +210,7 @@ abstract class BasePackConnection extends BaseConnection { try { line = readLine(); } catch (EOFException e) { - TransportException noRepo = noRepository(); - noRepo.initCause(e); - throw noRepo; + throw noRepository(e); } if (line != null && VERSION_1.equals(line)) { // Same as V0, except for this extra line. We shouldn't get @@ -567,11 +565,14 @@ abstract class BasePackConnection extends BaseConnection { * * Subclasses may override this method to provide better diagnostics. * + * @param cause + * root cause exception * @return a TransportException saying a repository cannot be found and * possibly why. */ - protected TransportException noRepository() { - return new NoRemoteRepositoryException(uri, JGitText.get().notFound); + protected TransportException noRepository(Throwable cause) { + return new NoRemoteRepositoryException(uri, JGitText.get().notFound, + cause); } /** 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 f48e1e68cc..3f167ccce2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java @@ -1,7 +1,7 @@ /* * Copyright (C) 2008, 2010 Google Inc. * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> - * Copyright (C) 2008, 2020 Shawn O. Pearce <spearce@spearce.org> and others + * Copyright (C) 2008, 2022 Shawn O. Pearce <spearce@spearce.org> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -1004,9 +1004,12 @@ public abstract class BasePackFetchConnection extends BasePackConnection OutputStream outputStream) throws IOException { onReceivePack(); InputStream input = in; - if (sideband) - input = new SideBandInputStream(input, monitor, getMessageWriter(), - outputStream); + SideBandInputStream sidebandIn = null; + if (sideband) { + sidebandIn = new SideBandInputStream(input, monitor, + getMessageWriter(), outputStream); + input = sidebandIn; + } try (ObjectInserter ins = local.newObjectInserter()) { PackParser parser = ins.newPackParser(input); @@ -1015,6 +1018,10 @@ public abstract class BasePackFetchConnection extends BasePackConnection parser.setLockMessage(lockMessage); packLock = parser.parse(monitor); ins.flush(); + } finally { + if (sidebandIn != null) { + sidebandIn.drainMessages(); + } } } 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 eb1d2ac0a9..b7be59d6f8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java @@ -1,6 +1,6 @@ /* * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> - * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others + * Copyright (C) 2008, 2022 Shawn O. Pearce <spearce@spearce.org> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -139,7 +139,7 @@ public abstract class BasePackPushConnection extends BasePackConnection implemen /** {@inheritDoc} */ @Override - protected TransportException noRepository() { + protected TransportException noRepository(Throwable cause) { // Sadly we cannot tell the "invalid URI" case from "push not allowed". // Opening a fetch connection can help us tell the difference, as any // useful repository is going to support fetch if it also would allow @@ -147,18 +147,18 @@ public abstract class BasePackPushConnection extends BasePackConnection implemen // URI is wrong. Otherwise we can correctly state push isn't allowed // as the fetch connection opened successfully. // + TransportException te; try { transport.openFetch().close(); - } catch (NotSupportedException e) { - // Fall through. + te = new TransportException(uri, JGitText.get().pushNotPermitted); } catch (NoRemoteRepositoryException e) { // Fetch concluded the repository doesn't exist. - // - return e; - } catch (TransportException e) { - // Fall through. + te = e; + } catch (NotSupportedException | TransportException e) { + te = new TransportException(uri, JGitText.get().pushNotPermitted, e); } - return new TransportException(uri, JGitText.get().pushNotPermitted); + te.addSuppressed(cause); + return te; } /** @@ -194,10 +194,11 @@ public abstract class BasePackPushConnection extends BasePackConnection implemen // the other data channels. // int b = in.read(); - if (0 <= b) + if (0 <= b) { throw new TransportException(uri, MessageFormat.format( JGitText.get().expectedEOFReceived, Character.valueOf((char) b))); + } } } } catch (TransportException e) { @@ -205,6 +206,9 @@ public abstract class BasePackPushConnection extends BasePackConnection implemen } catch (Exception e) { throw new TransportException(uri, e.getMessage(), e); } finally { + if (in instanceof SideBandInputStream) { + ((SideBandInputStream) in).drainMessages(); + } close(); } } 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 7d7b3ee0a0..7bface49d9 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java @@ -31,6 +31,7 @@ import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.errors.NotSupportedException; @@ -56,6 +57,12 @@ class FetchProcess { /** List of things we want to fetch from the remote repository. */ private final Collection<RefSpec> toFetch; + /** + * List of things we don't want to fetch from the remote repository or to + * the local repository. + */ + private final Collection<RefSpec> negativeRefSpecs; + /** Set of refs we will actually wind up asking to obtain. */ private final HashMap<ObjectId, Ref> askFor = new HashMap<>(); @@ -74,9 +81,12 @@ class FetchProcess { private Map<String, Ref> localRefs; - FetchProcess(Transport t, Collection<RefSpec> f) { + FetchProcess(Transport t, Collection<RefSpec> refSpecs) { transport = t; - toFetch = f; + toFetch = refSpecs.stream().filter(refSpec -> !refSpec.isNegative()) + .collect(Collectors.toList()); + negativeRefSpecs = refSpecs.stream().filter(RefSpec::isNegative) + .collect(Collectors.toList()); } void execute(ProgressMonitor monitor, FetchResult result, @@ -204,8 +214,13 @@ class FetchProcess { BatchRefUpdate batch = transport.local.getRefDatabase() .newBatchUpdate() - .setAllowNonFastForwards(true) - .setRefLogMessage("fetch", true); //$NON-NLS-1$ + .setAllowNonFastForwards(true); + + // Generate reflog only when fetching updates and not at the first clone + if (initialBranch == null) { + batch.setRefLogMessage("fetch", true); //$NON-NLS-1$ + } + try (RevWalk walk = new RevWalk(transport.local)) { walk.setRetainBody(false); if (monitor instanceof BatchingProgressMonitor) { @@ -389,8 +404,13 @@ class FetchProcess { private void expandWildcard(RefSpec spec, Set<Ref> matched) throws TransportException { for (Ref src : conn.getRefs()) { - if (spec.matchSource(src) && matched.add(src)) - want(src, spec.expandFromSource(src)); + if (spec.matchSource(src)) { + RefSpec expandedRefSpec = spec.expandFromSource(src); + if (!matchNegativeRefSpec(expandedRefSpec) + && matched.add(src)) { + want(src, expandedRefSpec); + } + } } } @@ -406,11 +426,27 @@ class FetchProcess { if (src == null) { throw new TransportException(MessageFormat.format(JGitText.get().remoteDoesNotHaveSpec, want)); } - if (matched.add(src)) { + if (!matchNegativeRefSpec(spec) && matched.add(src)) { want(src, spec); } } + private boolean matchNegativeRefSpec(RefSpec spec) { + for (RefSpec negativeRefSpec : negativeRefSpecs) { + if (negativeRefSpec.getSource() != null && spec.getSource() != null + && negativeRefSpec.matchSource(spec.getSource())) { + return true; + } + + if (negativeRefSpec.getDestination() != null + && spec.getDestination() != null && negativeRefSpec + .matchDestination(spec.getDestination())) { + return true; + } + } + return false; + } + private boolean localHasObject(ObjectId id) throws TransportException { try { return transport.local.getObjectDatabase().has(id); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushConfig.java index fda7a8152a..c8774d546a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017, David Pursehouse <david.pursehouse@gmail.com> and others + * Copyright (C) 2017, 2022 David Pursehouse <david.pursehouse@gmail.com> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -10,7 +10,10 @@ package org.eclipse.jgit.transport; +import java.util.Locale; + import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.util.StringUtils; /** @@ -19,8 +22,9 @@ import org.eclipse.jgit.util.StringUtils; * @since 4.9 */ public class PushConfig { + /** - * Config values for push.recurseSubmodules. + * Git config values for {@code push.recurseSubmodules}. */ public enum PushRecurseSubmodulesMode implements Config.ConfigEnum { /** @@ -59,4 +63,100 @@ public class PushConfig { || configValue.equalsIgnoreCase(s); } } + + /** + * Git config values for {@code push.default}. + * + * @since 6.1 + */ + public enum PushDefault implements Config.ConfigEnum { + + /** + * Do not push if there are no explicit refspecs. + */ + NOTHING, + + /** + * Push the current branch to an upstream branch of the same name. + */ + CURRENT, + + /** + * Push the current branch to an upstream branch determined by git + * config {@code branch.<currentBranch>.merge}. + */ + UPSTREAM("tracking"), //$NON-NLS-1$ + + /** + * Like {@link #UPSTREAM}, but only if the upstream name is the same as + * the name of the current local branch. + */ + SIMPLE, + + /** + * Push all current local branches that match a configured push refspec + * of the remote configuration. + */ + MATCHING; + + private final String alias; + + private PushDefault() { + alias = null; + } + + private PushDefault(String alias) { + this.alias = alias; + } + + @Override + public String toConfigValue() { + return name().toLowerCase(Locale.ROOT); + } + + @Override + public boolean matchConfigValue(String in) { + return toConfigValue().equalsIgnoreCase(in) + || (alias != null && alias.equalsIgnoreCase(in)); + } + } + + private final PushRecurseSubmodulesMode recurseSubmodules; + + private final PushDefault pushDefault; + + /** + * Creates a new instance. + * + * @param config + * {@link Config} to fill the {@link PushConfig} from + * @since 6.1 + */ + public PushConfig(Config config) { + recurseSubmodules = config.getEnum(ConfigConstants.CONFIG_PUSH_SECTION, + null, ConfigConstants.CONFIG_KEY_RECURSE_SUBMODULES, + PushRecurseSubmodulesMode.NO); + pushDefault = config.getEnum(ConfigConstants.CONFIG_PUSH_SECTION, null, + ConfigConstants.CONFIG_KEY_DEFAULT, PushDefault.SIMPLE); + } + + /** + * Retrieves the value of git config {@code push.recurseSubmodules}. + * + * @return the value + * @since 6.1 + */ + public PushRecurseSubmodulesMode getRecurseSubmodules() { + return recurseSubmodules; + } + + /** + * Retrieves the value of git config {@code push.default}. + * + * @return the value + * @since 6.1 + */ + public PushDefault getPushDefault() { + return pushDefault; + } } 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 a244c55a38..b59ae0c450 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> and others + * Copyright (C) 2008, 2022 Marek Zawirski <marek.zawirski@gmail.com> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -18,11 +18,15 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; +import org.eclipse.jgit.api.errors.AbortedByHookException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.errors.NotSupportedException; import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.hooks.PrePushHook; import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; @@ -58,6 +62,8 @@ class PushProcess { /** A list of option strings associated with this push */ private List<String> pushOptions; + private final PrePushHook prePush; + /** * Create process for specified transport and refs updates specification. * @@ -66,12 +72,14 @@ class PushProcess { * connection. * @param toPush * specification of refs updates (and local tracking branches). - * + * @param prePush + * {@link PrePushHook} to run after the remote advertisement has + * been gotten * @throws TransportException */ - PushProcess(final Transport transport, - final Collection<RemoteRefUpdate> toPush) throws TransportException { - this(transport, toPush, null); + PushProcess(Transport transport, Collection<RemoteRefUpdate> toPush, + PrePushHook prePush) throws TransportException { + this(transport, toPush, prePush, null); } /** @@ -82,16 +90,19 @@ class PushProcess { * connection. * @param toPush * specification of refs updates (and local tracking branches). + * @param prePush + * {@link PrePushHook} to run after the remote advertisement has + * been gotten * @param out * OutputStream to write messages to * @throws TransportException */ - PushProcess(final Transport transport, - final Collection<RemoteRefUpdate> toPush, OutputStream out) - throws TransportException { + PushProcess(Transport transport, Collection<RemoteRefUpdate> toPush, + PrePushHook prePush, OutputStream out) throws TransportException { this.walker = new RevWalk(transport.local); this.transport = transport; this.toPush = new LinkedHashMap<>(); + this.prePush = prePush; this.out = out; this.pushOptions = transport.getPushOptions(); for (RemoteRefUpdate rru : toPush) { @@ -129,10 +140,39 @@ class PushProcess { res.setAdvertisedRefs(transport.getURI(), connection .getRefsMap()); res.peerUserAgent = connection.getPeerUserAgent(); - res.setRemoteUpdates(toPush); monitor.endTask(); + Map<String, RemoteRefUpdate> expanded = expandMatching(); + toPush.clear(); + toPush.putAll(expanded); + + res.setRemoteUpdates(toPush); final Map<String, RemoteRefUpdate> preprocessed = prepareRemoteUpdates(); + List<RemoteRefUpdate> willBeAttempted = preprocessed.values() + .stream().filter(u -> { + switch (u.getStatus()) { + case NON_EXISTING: + case REJECTED_NODELETE: + case REJECTED_NONFASTFORWARD: + case REJECTED_OTHER_REASON: + case REJECTED_REMOTE_CHANGED: + case UP_TO_DATE: + return false; + default: + return true; + } + }).collect(Collectors.toList()); + if (!willBeAttempted.isEmpty()) { + if (prePush != null) { + try { + prePush.setRefs(willBeAttempted); + prePush.setDryRun(transport.isDryRun()); + prePush.call(); + } catch (AbortedByHookException | IOException e) { + throw new TransportException(e.getMessage(), e); + } + } + } if (transport.isDryRun()) modifyUpdatesForDryRun(); else if (!preprocessed.isEmpty()) @@ -201,25 +241,8 @@ class PushProcess { continue; } - // check for fast-forward: - // - both old and new ref must point to commits, AND - // - both of them must be known for us, exist in repository, AND - // - old commit must be ancestor of new commit - boolean fastForward = true; - try { - RevObject oldRev = walker.parseAny(advertisedOld); - final RevObject newRev = walker.parseAny(rru.getNewObjectId()); - if (!(oldRev instanceof RevCommit) - || !(newRev instanceof RevCommit) - || !walker.isMergedInto((RevCommit) oldRev, - (RevCommit) newRev)) - fastForward = false; - } catch (MissingObjectException x) { - fastForward = false; - } catch (Exception x) { - throw new TransportException(transport.getURI(), MessageFormat.format( - JGitText.get().readingObjectsFromLocalRepositoryFailed, x.getMessage()), x); - } + boolean fastForward = isFastForward(advertisedOld, + rru.getNewObjectId()); rru.setFastForward(fastForward); if (!fastForward && !rru.isForceUpdate()) { rru.setStatus(Status.REJECTED_NONFASTFORWARD); @@ -233,6 +256,134 @@ class PushProcess { return result; } + /** + * Determines whether an update from {@code oldOid} to {@code newOid} is a + * fast-forward update: + * <ul> + * <li>both old and new must be commits, AND</li> + * <li>both of them must be known to us and exist in the repository, + * AND</li> + * <li>the old commit must be an ancestor of the new commit.</li> + * </ul> + * + * @param oldOid + * {@link ObjectId}Â of the old commit + * @param newOid + * {@link ObjectId}Â of the new commit + * @return {@code true} if the update fast-forwards, {@code false} otherwise + * @throws TransportException + */ + private boolean isFastForward(ObjectId oldOid, ObjectId newOid) + throws TransportException { + try { + RevObject oldRev = walker.parseAny(oldOid); + RevObject newRev = walker.parseAny(newOid); + if (!(oldRev instanceof RevCommit) || !(newRev instanceof RevCommit) + || !walker.isMergedInto((RevCommit) oldRev, + (RevCommit) newRev)) { + return false; + } + } catch (MissingObjectException x) { + return false; + } catch (Exception x) { + throw new TransportException(transport.getURI(), + MessageFormat.format(JGitText + .get().readingObjectsFromLocalRepositoryFailed, + x.getMessage()), + x); + } + return true; + } + + /** + * Expands all placeholder {@link RemoteRefUpdate}s for "matching" + * {@link RefSpec}s ":" in {@link #toPush} and returns the resulting map in + * which the placeholders have been replaced by their expansion. + * + * @return a new map of {@link RemoteRefUpdate}s keyed by remote name + * @throws TransportException + * if the expansion results in duplicate updates + */ + private Map<String, RemoteRefUpdate> expandMatching() + throws TransportException { + Map<String, RemoteRefUpdate> result = new LinkedHashMap<>(); + boolean hadMatch = false; + for (RemoteRefUpdate update : toPush.values()) { + if (update.isMatching()) { + if (hadMatch) { + throw new TransportException(MessageFormat.format( + JGitText.get().duplicateRemoteRefUpdateIsIllegal, + ":")); //$NON-NLS-1$ + } + expandMatching(result, update); + hadMatch = true; + } else if (result.put(update.getRemoteName(), update) != null) { + throw new TransportException(MessageFormat.format( + JGitText.get().duplicateRemoteRefUpdateIsIllegal, + update.getRemoteName())); + } + } + return result; + } + + /** + * Expands the placeholder {@link RemoteRefUpdate} {@code match} for a + * "matching" {@link RefSpec} ":" or "+:" and puts the expansion into the + * given map {@code updates}. + * + * @param updates + * map to put the expansion in + * @param match + * the placeholder {@link RemoteRefUpdate} to expand + * + * @throws TransportException + * if the expansion results in duplicate updates, or the local + * branches cannot be determined + */ + private void expandMatching(Map<String, RemoteRefUpdate> updates, + RemoteRefUpdate match) throws TransportException { + try { + Map<String, Ref> advertisement = connection.getRefsMap(); + Collection<RefSpec> fetchSpecs = match.getFetchSpecs(); + boolean forceUpdate = match.isForceUpdate(); + for (Ref local : transport.local.getRefDatabase() + .getRefsByPrefix(Constants.R_HEADS)) { + if (local.isSymbolic()) { + continue; + } + String name = local.getName(); + Ref advertised = advertisement.get(name); + if (advertised == null || advertised.isSymbolic()) { + continue; + } + ObjectId oldOid = advertised.getObjectId(); + if (oldOid == null || ObjectId.zeroId().equals(oldOid)) { + continue; + } + ObjectId newOid = local.getObjectId(); + if (newOid == null || ObjectId.zeroId().equals(newOid)) { + continue; + } + + RemoteRefUpdate rru = new RemoteRefUpdate(transport.local, name, + newOid, name, forceUpdate, + Transport.findTrackingRefName(name, fetchSpecs), + oldOid); + if (updates.put(rru.getRemoteName(), rru) != null) { + throw new TransportException(MessageFormat.format( + JGitText.get().duplicateRemoteRefUpdateIsIllegal, + rru.getRemoteName())); + } + } + } catch (IOException x) { + throw new TransportException(transport.getURI(), + MessageFormat.format(JGitText + .get().readingObjectsFromLocalRepositoryFailed, + x.getMessage()), + x); + } + } + private Map<String, RemoteRefUpdate> rejectAll() { for (RemoteRefUpdate rru : toPush.values()) { if (rru.getStatus() == Status.NOT_ATTEMPTED) { 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 ac357afdae..61d193593a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008, 2013 Shawn O. Pearce <spearce@spearce.org> and others + * Copyright (C) 2008, 2022 Shawn O. Pearce <spearce@spearce.org> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -12,11 +12,11 @@ package org.eclipse.jgit.transport; import java.io.Serializable; import java.text.MessageFormat; +import java.util.Objects; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Ref; -import org.eclipse.jgit.util.References; /** * Describes how refs in one repository copy into another repository. @@ -50,6 +50,12 @@ public class RefSpec implements Serializable { /** Is this specification actually a wildcard match? */ private boolean wildcard; + /** Is this the special ":" RefSpec? */ + private boolean matching; + + /** Is this a negative refspec. */ + private boolean negative; + /** * How strict to be about wildcards. * @@ -71,6 +77,7 @@ public class RefSpec implements Serializable { */ ALLOW_MISMATCH } + /** Whether a wildcard is allowed on one side but not the other. */ private WildcardMode allowMismatchedWildcards; @@ -87,16 +94,28 @@ public class RefSpec implements Serializable { * applications, as at least one field must be set to match a source name. */ public RefSpec() { + matching = false; force = false; wildcard = false; srcName = Constants.HEAD; dstName = null; + negative =false; allowMismatchedWildcards = WildcardMode.REQUIRE_MATCH; } /** * Parse a ref specification for use during transport operations. * <p> + * {@link RefSpec}s can be regular or negative, regular RefSpecs indicate + * what to include in transport operations while negative RefSpecs indicate + * what to exclude in fetch. + * <p> + * Negative {@link RefSpec}s can't be force, must have only source or + * destination. Wildcard patterns are also supported in negative RefSpecs + * but they can not go with {@code WildcardMode.REQUIRE_MATCH} because they + * are natually one to many mappings. + * + * <p> * Specifications are typically one of the following forms: * <ul> * <li><code>refs/heads/master</code></li> @@ -116,6 +135,12 @@ public class RefSpec implements Serializable { * <li><code>refs/heads/*:refs/heads/master</code></li> * </ul> * + * Negative specifications are usually like: + * <ul> + * <li><code>^:refs/heads/master</code></li> + * <li><code>^refs/heads/*</code></li> + * </ul> + * * @param spec * string describing the specification. * @param mode @@ -128,22 +153,41 @@ public class RefSpec implements Serializable { public RefSpec(String spec, WildcardMode mode) { this.allowMismatchedWildcards = mode; String s = spec; + + if (s.startsWith("^+") || s.startsWith("+^")) { //$NON-NLS-1$ //$NON-NLS-2$ + throw new IllegalArgumentException( + JGitText.get().invalidNegativeAndForce); + } + if (s.startsWith("+")) { //$NON-NLS-1$ force = true; s = s.substring(1); } + if (s.startsWith("^")) { //$NON-NLS-1$ + negative = true; + s = s.substring(1); + } + + boolean matchPushSpec = false; final int c = s.lastIndexOf(':'); if (c == 0) { s = s.substring(1); - if (isWildcard(s)) { + if (s.isEmpty()) { + matchPushSpec = true; wildcard = true; - if (mode == WildcardMode.REQUIRE_MATCH) { - throw new IllegalArgumentException(MessageFormat - .format(JGitText.get().invalidWildcards, spec)); + srcName = Constants.R_HEADS + '*'; + dstName = srcName; + } else { + if (isWildcard(s)) { + wildcard = true; + if (mode == WildcardMode.REQUIRE_MATCH) { + throw new IllegalArgumentException(MessageFormat + .format(JGitText.get().invalidWildcards, spec)); + } } + dstName = checkValid(s); } - dstName = checkValid(s); } else if (c > 0) { String src = s.substring(0, c); String dst = s.substring(c + 1); @@ -168,6 +212,22 @@ public class RefSpec implements Serializable { } srcName = checkValid(s); } + + // Negative refspecs must only have dstName or srcName. + if (isNegative()) { + if (isNullOrEmpty(srcName) && isNullOrEmpty(dstName)) { + throw new IllegalArgumentException(MessageFormat + .format(JGitText.get().invalidRefSpec, spec)); + } + if (!isNullOrEmpty(srcName) && !isNullOrEmpty(dstName)) { + throw new IllegalArgumentException(MessageFormat + .format(JGitText.get().invalidRefSpec, spec)); + } + if(wildcard && mode == WildcardMode.REQUIRE_MATCH) { + throw new IllegalArgumentException(MessageFormat + .format(JGitText.get().invalidRefSpec, spec));} + } + matching = matchPushSpec; } /** @@ -191,18 +251,32 @@ public class RefSpec implements Serializable { * the specification is invalid. */ public RefSpec(String spec) { - this(spec, WildcardMode.REQUIRE_MATCH); + this(spec, spec.startsWith("^") ? WildcardMode.ALLOW_MISMATCH //$NON-NLS-1$ + : WildcardMode.REQUIRE_MATCH); } private RefSpec(RefSpec p) { + matching = false; force = p.isForceUpdate(); wildcard = p.isWildcard(); + negative = p.isNegative(); srcName = p.getSource(); dstName = p.getDestination(); allowMismatchedWildcards = p.allowMismatchedWildcards; } /** + * Tells whether this {@link RefSpec} is the special "matching" RefSpec ":" + * for pushing. + * + * @return whether this is a "matching" RefSpec + * @since 6.1 + */ + public boolean isMatching() { + return matching; + } + + /** * Check if this specification wants to forcefully update the destination. * * @return true if this specification asks for updates without merge tests. @@ -220,6 +294,11 @@ public class RefSpec implements Serializable { */ public RefSpec setForceUpdate(boolean forceUpdate) { final RefSpec r = new RefSpec(this); + if (forceUpdate && isNegative()) { + throw new IllegalArgumentException( + JGitText.get().invalidNegativeAndForce); + } + r.matching = matching; r.force = forceUpdate; return r; } @@ -238,6 +317,16 @@ public class RefSpec implements Serializable { } /** + * Check if this specification is a negative one. + * + * @return true if this specification is negative. + * @since 6.2 + */ + public boolean isNegative() { + return negative; + } + + /** * Get the source ref description. * <p> * During a fetch this is the name of the ref on the remote repository we @@ -322,8 +411,7 @@ public class RefSpec implements Serializable { * The wildcard status of the new source disagrees with the * wildcard status of the new destination. */ - public RefSpec setSourceDestination(final String source, - final String destination) { + public RefSpec setSourceDestination(String source, String destination) { if (isWildcard(source) != isWildcard(destination)) throw new IllegalStateException(JGitText.get().sourceDestinationMustMatch); final RefSpec r = new RefSpec(this); @@ -409,6 +497,10 @@ public class RefSpec implements Serializable { return this; } + private static boolean isNullOrEmpty(String refName) { + return refName == null || refName.isEmpty(); + } + /** * Expand this specification to exactly match a ref. * <p> @@ -541,37 +633,42 @@ public class RefSpec implements Serializable { if (!(obj instanceof RefSpec)) return false; final RefSpec b = (RefSpec) obj; - if (isForceUpdate() != b.isForceUpdate()) - return false; - if (isWildcard() != b.isWildcard()) + if (isForceUpdate() != b.isForceUpdate()) { return false; - if (!eq(getSource(), b.getSource())) - return false; - if (!eq(getDestination(), b.getDestination())) + } + if(isNegative() != b.isNegative()) { return false; - return true; - } - - private static boolean eq(String a, String b) { - if (References.isSameObject(a, b)) { - return true; } - if (a == null || b == null) + if (isMatching()) { + return b.isMatching(); + } else if (b.isMatching()) { return false; - return a.equals(b); + } + return isWildcard() == b.isWildcard() + && Objects.equals(getSource(), b.getSource()) + && Objects.equals(getDestination(), b.getDestination()); } /** {@inheritDoc} */ @Override public String toString() { final StringBuilder r = new StringBuilder(); - if (isForceUpdate()) + if (isForceUpdate()) { r.append('+'); - if (getSource() != null) - r.append(getSource()); - if (getDestination() != null) { + } + if(isNegative()) { + r.append('^'); + } + if (isMatching()) { r.append(':'); - r.append(getDestination()); + } else { + if (getSource() != null) { + r.append(getSource()); + } + if (getDestination() != null) { + r.append(':'); + r.append(getDestination()); + } } return r.toString(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteConfig.java index 2f3160bb8e..c4e105ec4a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteConfig.java @@ -16,10 +16,7 @@ import java.io.Serializable; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.Map.Entry; import org.eclipse.jgit.lib.Config; @@ -54,10 +51,6 @@ public class RemoteConfig implements Serializable { private static final String KEY_TIMEOUT = "timeout"; //$NON-NLS-1$ - private static final String KEY_INSTEADOF = "insteadof"; //$NON-NLS-1$ - - private static final String KEY_PUSHINSTEADOF = "pushinsteadof"; //$NON-NLS-1$ - private static final boolean DEFAULT_MIRROR = false; /** Default value for {@link #getUploadPack()} if not specified. */ @@ -135,10 +128,10 @@ public class RemoteConfig implements Serializable { String val; vlst = rc.getStringList(SECTION, name, KEY_URL); - Map<String, String> insteadOf = getReplacements(rc, KEY_INSTEADOF); + UrlConfig urls = new UrlConfig(rc); uris = new ArrayList<>(vlst.length); for (String s : vlst) { - uris.add(new URIish(replaceUri(s, insteadOf))); + uris.add(new URIish(urls.replace(s))); } String[] plst = rc.getStringList(SECTION, name, KEY_PUSHURL); pushURIs = new ArrayList<>(plst.length); @@ -148,11 +141,9 @@ public class RemoteConfig implements Serializable { if (pushURIs.isEmpty()) { // Would default to the uris. If we have pushinsteadof, we must // supply rewritten push uris. - Map<String, String> pushInsteadOf = getReplacements(rc, - KEY_PUSHINSTEADOF); - if (!pushInsteadOf.isEmpty()) { + if (urls.hasPushReplacements()) { for (String s : vlst) { - String replaced = replaceUri(s, pushInsteadOf); + String replaced = urls.replacePush(s); if (!s.equals(replaced)) { pushURIs.add(new URIish(replaced)); } @@ -248,39 +239,6 @@ public class RemoteConfig implements Serializable { rc.unset(SECTION, getName(), key); } - private Map<String, String> getReplacements(final Config config, - final String keyName) { - final Map<String, String> replacements = new HashMap<>(); - for (String url : config.getSubsections(KEY_URL)) - for (String insteadOf : config.getStringList(KEY_URL, url, keyName)) - replacements.put(insteadOf, url); - return replacements; - } - - private String replaceUri(final String uri, - final Map<String, String> replacements) { - if (replacements.isEmpty()) { - return uri; - } - Entry<String, String> match = null; - for (Entry<String, String> replacement : replacements.entrySet()) { - // Ignore current entry if not longer than previous match - if (match != null - && match.getKey().length() > replacement.getKey() - .length()) { - continue; - } - if (!uri.startsWith(replacement.getKey())) { - continue; - } - match = replacement; - } - if (match != null) { - return match.getValue() + uri.substring(match.getKey().length()); - } - return uri; - } - /** * Get the local name this remote configuration is recognized as. * diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java index 43eaac7927..218e62c10a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java @@ -12,7 +12,9 @@ package org.eclipse.jgit.transport; import java.io.IOException; import java.text.MessageFormat; +import java.util.Collection; +import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; @@ -116,6 +118,12 @@ public class RemoteRefUpdate { private RefUpdate localUpdate; /** + * If set, the RemoteRefUpdate is a placeholder for the "matching" RefSpec + * to be expanded after the advertisements have been received in a push. + */ + private Collection<RefSpec> fetchSpecs; + + /** * Construct remote ref update request by providing an update specification. * Object is created with default * {@link org.eclipse.jgit.transport.RemoteRefUpdate.Status#NOT_ATTEMPTED} @@ -157,9 +165,8 @@ public class RemoteRefUpdate { * @throws java.lang.IllegalArgumentException * if some required parameter was null */ - public RemoteRefUpdate(final Repository localDb, final String srcRef, - final String remoteName, final boolean forceUpdate, - final String localName, final ObjectId expectedOldObjectId) + public RemoteRefUpdate(Repository localDb, String srcRef, String remoteName, + boolean forceUpdate, String localName, ObjectId expectedOldObjectId) throws IOException { this(localDb, srcRef, srcRef != null ? localDb.resolve(srcRef) : ObjectId.zeroId(), remoteName, forceUpdate, localName, @@ -203,9 +210,8 @@ public class RemoteRefUpdate { * @throws java.lang.IllegalArgumentException * if some required parameter was null */ - public RemoteRefUpdate(final Repository localDb, final Ref srcRef, - final String remoteName, final boolean forceUpdate, - final String localName, final ObjectId expectedOldObjectId) + public RemoteRefUpdate(Repository localDb, Ref srcRef, String remoteName, + boolean forceUpdate, String localName, ObjectId expectedOldObjectId) throws IOException { this(localDb, srcRef != null ? srcRef.getName() : null, srcRef != null ? srcRef.getObjectId() : null, remoteName, @@ -255,28 +261,41 @@ public class RemoteRefUpdate { * @throws java.lang.IllegalArgumentException * if some required parameter was null */ - public RemoteRefUpdate(final Repository localDb, final String srcRef, - final ObjectId srcId, final String remoteName, - final boolean forceUpdate, final String localName, - final ObjectId expectedOldObjectId) throws IOException { - if (remoteName == null) - throw new IllegalArgumentException(JGitText.get().remoteNameCannotBeNull); - if (srcId == null && srcRef != null) - throw new IOException(MessageFormat.format( - JGitText.get().sourceRefDoesntResolveToAnyObject, srcRef)); - - if (srcRef != null) + public RemoteRefUpdate(Repository localDb, String srcRef, ObjectId srcId, + String remoteName, boolean forceUpdate, String localName, + ObjectId expectedOldObjectId) throws IOException { + this(localDb, srcRef, srcId, remoteName, forceUpdate, localName, null, + expectedOldObjectId); + } + + private RemoteRefUpdate(Repository localDb, String srcRef, ObjectId srcId, + String remoteName, boolean forceUpdate, String localName, + Collection<RefSpec> fetchSpecs, ObjectId expectedOldObjectId) + throws IOException { + if (fetchSpecs == null) { + if (remoteName == null) { + throw new IllegalArgumentException( + JGitText.get().remoteNameCannotBeNull); + } + if (srcId == null && srcRef != null) { + throw new IOException(MessageFormat.format( + JGitText.get().sourceRefDoesntResolveToAnyObject, + srcRef)); + } + } + if (srcRef != null) { this.srcRef = srcRef; - else if (srcId != null && !srcId.equals(ObjectId.zeroId())) + } else if (srcId != null && !srcId.equals(ObjectId.zeroId())) { this.srcRef = srcId.name(); - else + } else { this.srcRef = null; - - if (srcId != null) + } + if (srcId != null) { this.newObjectId = srcId; - else + } else { this.newObjectId = ObjectId.zeroId(); - + } + this.fetchSpecs = fetchSpecs; this.remoteName = remoteName; this.forceUpdate = forceUpdate; if (localName != null && localDb != null) { @@ -292,8 +311,9 @@ public class RemoteRefUpdate { ? localUpdate.getOldObjectId() : ObjectId.zeroId(), newObjectId); - } else + } else { trackingRefUpdate = null; + } this.localDb = localDb; this.expectedOldObjectId = expectedOldObjectId; this.status = Status.NOT_ATTEMPTED; @@ -316,11 +336,57 @@ public class RemoteRefUpdate { * local tracking branch or srcRef of base object no longer can * be resolved to any object. */ - public RemoteRefUpdate(final RemoteRefUpdate base, - final ObjectId newExpectedOldObjectId) throws IOException { - this(base.localDb, base.srcRef, base.remoteName, base.forceUpdate, + public RemoteRefUpdate(RemoteRefUpdate base, + ObjectId newExpectedOldObjectId) throws IOException { + this(base.localDb, base.srcRef, base.newObjectId, base.remoteName, + base.forceUpdate, (base.trackingRefUpdate == null ? null : base.trackingRefUpdate - .getLocalName()), newExpectedOldObjectId); + .getLocalName()), + base.fetchSpecs, newExpectedOldObjectId); + } + + /** + * Creates a "placeholder" update for the "matching" RefSpec ":". + * + * @param localDb + * local repository to push from + * @param forceUpdate + * whether non-fast-forward updates shall be allowed + * @param fetchSpecs + * The fetch {@link RefSpec}s to use when this placeholder is + * expanded to determine remote tracking branch updates + */ + RemoteRefUpdate(Repository localDb, boolean forceUpdate, + @NonNull Collection<RefSpec> fetchSpecs) { + this.localDb = localDb; + this.forceUpdate = forceUpdate; + this.fetchSpecs = fetchSpecs; + this.trackingRefUpdate = null; + this.srcRef = null; + this.remoteName = null; + this.newObjectId = null; + this.status = Status.NOT_ATTEMPTED; + } + + /** + * Tells whether this {@link RemoteRefUpdate} is a placeholder for a + * "matching" {@link RefSpec}. + * + * @return {@code true} if this is a placeholder, {@code false} otherwise + * @since 6.1 + */ + public boolean isMatching() { + return fetchSpecs != null; + } + + /** + * Retrieves the fetch {@link RefSpec}s of this {@link RemoteRefUpdate}. + * + * @return the fetch {@link RefSpec}s, or {@code null} if + * {@code this.}{@link #isMatching()} {@code == false} + */ + Collection<RefSpec> getFetchSpecs() { + return fetchSpecs; } /** 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 8a8d977ed3..96c7be5b97 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandInputStream.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandInputStream.java @@ -1,6 +1,6 @@ /* * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> - * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others + * Copyright (C) 2008, 2022 Shawn O. Pearce <spearce@spearce.org> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -28,6 +28,8 @@ import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.util.IO; import org.eclipse.jgit.util.RawParseUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Unmultiplexes the data portion of a side-band channel. @@ -46,6 +48,10 @@ import org.eclipse.jgit.util.RawParseUtils; * @since 4.11 */ public class SideBandInputStream extends InputStream { + + private static final Logger LOG = LoggerFactory + .getLogger(SideBandInputStream.class); + static final int CH_DATA = 1; static final int CH_PROGRESS = 2; static final int CH_ERROR = 3; @@ -210,6 +216,21 @@ public class SideBandInputStream extends InputStream { monitor.beginTask(remote(currentTask), totalWorkUnits); } + /** + * Forces any buffered progress messages to be written. + */ + void drainMessages() { + if (!progressBuffer.isEmpty()) { + try { + progress("\n"); //$NON-NLS-1$ + } catch (IOException e) { + // Just log; otherwise this IOException might hide a real + // TransportException + LOG.error(e.getMessage(), e); + } + } + } + private static String remote(String msg) { String prefix = JGitText.get().prefixRemote; StringBuilder r = new StringBuilder(prefix.length() + msg.length() + 1); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java index 212a4e46c1..48cacf0964 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018, 2020 Thomas Wolf <thomas.wolf@paranor.ch> and others + * Copyright (C) 2018, 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -44,6 +44,14 @@ public final class SshConstants { // Config file keys + /** + * Property to control whether private keys are added to an SSH agent, if + * one is running, after having been loaded. + * + * @since 6.1 + */ + public static final String ADD_KEYS_TO_AGENT = "AddKeysToAgent"; + /** Key in an ssh config file. */ public static final String BATCH_MODE = "BatchMode"; @@ -62,6 +70,15 @@ public final class SshConstants { /** Key in an ssh config file. */ public static final String CONNECTION_ATTEMPTS = "ConnectionAttempts"; + /** + * An OpenSSH time value for the connection timeout. In OpenSSH, this + * includes everything until the end of the initial key exchange; in JGit it + * covers only the underlying TCP connect. + * + * @since 6.1 + */ + public static final String CONNECT_TIMEOUT = "ConnectTimeout"; + /** Key in an ssh config file. */ public static final String CONTROL_PATH = "ControlPath"; @@ -159,6 +176,14 @@ public final class SshConstants { /** Key in an ssh config file. */ public static final String REMOTE_FORWARD = "RemoteForward"; + /** + * (Absolute) path to a middleware library the SSH agent shall use to load + * SK (U2F) keys. + * + * @since 6.1 + */ + public static final String SECURITY_KEY_PROVIDER = "SecurityKeyProvider"; + /** Key in an ssh config file. */ public static final String SEND_ENV = "SendEnv"; @@ -229,4 +254,12 @@ public final class SshConstants { public static final String[] DEFAULT_IDENTITIES = { // ID_RSA, ID_DSA, ID_ECDSA, ID_ED25519 }; + + /** + * Name of the environment variable holding the Unix domain socket for + * communication with an SSH agent. + * + * @since 6.1 + */ + public static final String ENV_SSH_AUTH_SOCKET = "SSH_AUTH_SOCK"; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshSessionFactory.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshSessionFactory.java index 1e98a56f79..a0194ea8b1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshSessionFactory.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshSessionFactory.java @@ -1,6 +1,6 @@ /* * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> - * Copyright (C) 2008, 2020 Shawn O. Pearce <spearce@spearce.org> and others + * Copyright (C) 2008, 2022 Shawn O. Pearce <spearce@spearce.org> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -36,15 +36,35 @@ import org.eclipse.jgit.util.SystemReader; */ public abstract class SshSessionFactory { - private static volatile SshSessionFactory INSTANCE = loadSshSessionFactory(); + private static class DefaultFactory { - private static SshSessionFactory loadSshSessionFactory() { - ServiceLoader<SshSessionFactory> loader = ServiceLoader.load(SshSessionFactory.class); - Iterator<SshSessionFactory> iter = loader.iterator(); - if(iter.hasNext()) { - return iter.next(); + private static volatile SshSessionFactory INSTANCE = loadSshSessionFactory(); + + private static SshSessionFactory loadSshSessionFactory() { + ServiceLoader<SshSessionFactory> loader = ServiceLoader + .load(SshSessionFactory.class); + Iterator<SshSessionFactory> iter = loader.iterator(); + if (iter.hasNext()) { + return iter.next(); + } + return null; + } + + private DefaultFactory() { + // No instantiation + } + + public static SshSessionFactory getInstance() { + return INSTANCE; + } + + public static void setInstance(SshSessionFactory newFactory) { + if (newFactory != null) { + INSTANCE = newFactory; + } else { + INSTANCE = loadSshSessionFactory(); + } } - return null; } /** @@ -57,7 +77,7 @@ public abstract class SshSessionFactory { * @return factory the current factory for this JVM. */ public static SshSessionFactory getInstance() { - return INSTANCE; + return DefaultFactory.getInstance(); } /** @@ -68,11 +88,7 @@ public abstract class SshSessionFactory { * {@code null} the default factory will be restored. */ public static void setInstance(SshSessionFactory newFactory) { - if (newFactory != null) { - INSTANCE = newFactory; - } else { - INSTANCE = loadSshSessionFactory(); - } + DefaultFactory.setInstance(newFactory); } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TrackingRefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TrackingRefUpdate.java index 696ca7cf46..51bc07cb94 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TrackingRefUpdate.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TrackingRefUpdate.java @@ -12,6 +12,8 @@ package org.eclipse.jgit.transport; +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_ABBREV_STRING_LENGTH; + import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.RefUpdate; @@ -184,9 +186,13 @@ public class TrackingRefUpdate { if (forceUpdate) sb.append(" (forced)"); sb.append(" "); - sb.append(oldObjectId == null ? "" : oldObjectId.abbreviate(7).name()); + sb.append(oldObjectId == null ? "" + : oldObjectId.abbreviate(OBJECT_ID_ABBREV_STRING_LENGTH) + .name()); sb.append(".."); - sb.append(newObjectId == null ? "" : newObjectId.abbreviate(7).name()); + sb.append(newObjectId == null ? "" + : newObjectId.abbreviate(OBJECT_ID_ABBREV_STRING_LENGTH) + .name()); sb.append("]"); return sb.toString(); } 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 5b781ac25f..3222d6330c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java @@ -2,7 +2,7 @@ * Copyright (C) 2008, 2009 Google Inc. * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> - * Copyright (C) 2008, 2020 Shawn O. Pearce <spearce@spearce.org> and others + * Copyright (C) 2008, 2022 Shawn O. Pearce <spearce@spearce.org> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -40,7 +40,6 @@ import java.util.concurrent.CopyOnWriteArrayList; import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.annotations.Nullable; -import org.eclipse.jgit.api.errors.AbortedByHookException; import org.eclipse.jgit.errors.NotSupportedException; import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.hooks.Hooks; @@ -590,6 +589,11 @@ public abstract class Transport implements AutoCloseable { final Collection<RefSpec> procRefs = expandPushWildcardsFor(db, specs); for (RefSpec spec : procRefs) { + if (spec.isMatching()) { + result.add(new RemoteRefUpdate(db, spec.isForceUpdate(), + fetchSpecs)); + continue; + } String srcSpec = spec.getSource(); final Ref srcRef = db.findRef(srcSpec); if (srcRef != null) @@ -656,14 +660,18 @@ public abstract class Transport implements AutoCloseable { private static Collection<RefSpec> expandPushWildcardsFor( final Repository db, final Collection<RefSpec> specs) throws IOException { - final List<Ref> localRefs = db.getRefDatabase().getRefs(); final Collection<RefSpec> procRefs = new LinkedHashSet<>(); + List<Ref> localRefs = null; for (RefSpec spec : specs) { - if (spec.isWildcard()) { + if (!spec.isMatching() && spec.isWildcard()) { + if (localRefs == null) { + localRefs = db.getRefDatabase().getRefs(); + } for (Ref localRef : localRefs) { - if (spec.matchSource(localRef)) + if (spec.matchSource(localRef)) { procRefs.add(spec.expandFromSource(localRef)); + } } } else { procRefs.add(spec); @@ -672,7 +680,7 @@ public abstract class Transport implements AutoCloseable { return procRefs; } - private static String findTrackingRefName(final String remoteName, + static String findTrackingRefName(final String remoteName, final Collection<RefSpec> fetchSpecs) { // try to find matching tracking refs for (RefSpec fetchSpec : fetchSpecs) { @@ -1222,7 +1230,9 @@ public abstract class Transport implements AutoCloseable { * @param toFetch * specification of refs to fetch locally. May be null or the * empty collection to use the specifications from the - * RemoteConfig. Source for each RefSpec can't be null. + * RemoteConfig. May contains regular and negative + * {@link RefSpec}s. Source for each regular RefSpec can't + * be null. * @return information describing the tracking refs updated. * @throws org.eclipse.jgit.errors.NotSupportedException * this transport implementation does not support fetching @@ -1256,7 +1266,9 @@ public abstract class Transport implements AutoCloseable { * @param toFetch * specification of refs to fetch locally. May be null or the * empty collection to use the specifications from the - * RemoteConfig. Source for each RefSpec can't be null. + * RemoteConfig. May contains regular and negative + * {@link RefSpec}s. Source for each regular RefSpec can't + * be null. * @param branch * the initial branch to check out when cloning the repository. * Can be specified as ref name (<code>refs/heads/master</code>), @@ -1371,16 +1383,9 @@ public abstract class Transport implements AutoCloseable { if (toPush.isEmpty()) throw new TransportException(JGitText.get().nothingToPush); } - if (prePush != null) { - try { - prePush.setRefs(toPush); - prePush.call(); - } catch (AbortedByHookException | IOException e) { - throw new TransportException(e.getMessage(), e); - } - } - final PushProcess pushProcess = new PushProcess(this, toPush, out); + final PushProcess pushProcess = new PushProcess(this, toPush, prePush, + out); return pushProcess.execute(monitor); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java index 0710d3fdfb..405373a0f9 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java @@ -1540,14 +1540,19 @@ public class TransportHttp extends HttpTransport implements WalkTransport, } @Override - protected void doFetch(final ProgressMonitor monitor, - final Collection<Ref> want, final Set<ObjectId> have, - final OutputStream outputStream) throws TransportException { - try { - svc = new MultiRequestService(SVC_UPLOAD_PACK, - getProtocolVersion()); - init(svc.getInputStream(), svc.getOutputStream()); + protected void doFetch(ProgressMonitor monitor, Collection<Ref> want, + Set<ObjectId> have, OutputStream outputStream) + throws TransportException { + svc = new MultiRequestService(SVC_UPLOAD_PACK, + getProtocolVersion()); + try (InputStream svcIn = svc.getInputStream(); + OutputStream svcOut = svc.getOutputStream()) { + init(svcIn, svcOut); super.doFetch(monitor, want, have, outputStream); + } catch (TransportException e) { + throw e; + } catch (IOException e) { + throw new TransportException(e.getMessage(), e); } finally { svc = null; } @@ -1571,13 +1576,20 @@ public class TransportHttp extends HttpTransport implements WalkTransport, } @Override - protected void doPush(final ProgressMonitor monitor, - final Map<String, RemoteRefUpdate> refUpdates, + protected void doPush(ProgressMonitor monitor, + Map<String, RemoteRefUpdate> refUpdates, OutputStream outputStream) throws TransportException { - final Service svc = new MultiRequestService(SVC_RECEIVE_PACK, + Service svc = new MultiRequestService(SVC_RECEIVE_PACK, getProtocolVersion()); - init(svc.getInputStream(), svc.getOutputStream()); - super.doPush(monitor, refUpdates, outputStream); + try (InputStream svcIn = svc.getInputStream(); + OutputStream svcOut = svc.getOutputStream()) { + init(svcIn, svcOut); + super.doPush(monitor, refUpdates, outputStream); + } catch (TransportException e) { + throw e; + } catch (IOException e) { + throw new TransportException(e.getMessage(), e); + } } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java index 6da6c13342..dcd806a3da 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java @@ -39,6 +39,7 @@ import static org.eclipse.jgit.transport.GitProtocolConstants.VERSION_2_REQUEST; import static org.eclipse.jgit.util.RefMap.toRefMap; import java.io.ByteArrayOutputStream; +import java.io.Closeable; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; @@ -105,7 +106,7 @@ import org.eclipse.jgit.util.io.TimeoutOutputStream; /** * Implements the server side of a fetch connection, transmitting objects. */ -public class UploadPack { +public class UploadPack implements Closeable { /** Policy the server uses to validate client requests */ public enum RequestPolicy { /** Client may only ask for objects the server advertised a reference for. */ @@ -733,6 +734,17 @@ public class UploadPack { && clientRequestedV2; } + @Override + public void close() { + if (timer != null) { + try { + timer.terminate(); + } finally { + timer = null; + } + } + } + /** * Execute the upload task on the socket. * @@ -780,6 +792,8 @@ public class UploadPack { throw new UploadPackInternalServerErrorException(err); } throw err; + } finally { + close(); } } @@ -789,6 +803,10 @@ public class UploadPack { * <p> * If the client passed extra parameters (e.g., "version=2") through a side * channel, the caller must call setExtraParameters first to supply them. + * Callers of this method should call {@link #close()} to terminate the + * internal interrupt timer thread. If the caller fails to terminate the + * thread, it will (eventually) terminate itself when the InterruptTimer + * instance is garbage collected. * * @param input * raw input to read client commands from. Caller must ensure the @@ -845,13 +863,6 @@ public class UploadPack { } finally { msgOut = NullOutputStream.INSTANCE; walk.close(); - if (timer != null) { - try { - timer.terminate(); - } finally { - timer = null; - } - } } } @@ -1998,12 +2009,16 @@ public class UploadPack { throws IOException { ObjectReader reader = up.getRevWalk().getObjectReader(); + Set<ObjectId> directlyVisibleObjects = refIdSet(visibleRefs); + List<ObjectId> nonTipWants = notAdvertisedWants.stream() + .filter(not(directlyVisibleObjects::contains)) + .collect(Collectors.toList()); try (RevWalk walk = new RevWalk(reader)) { walk.setRetainBody(false); // Missing "wants" throw exception here List<RevObject> wantsAsObjs = objectIdsToRevObjects(walk, - notAdvertisedWants); + nonTipWants); List<RevCommit> wantsAsCommits = wantsAsObjs.stream() .filter(obj -> obj instanceof RevCommit) .map(obj -> (RevCommit) obj) @@ -2023,7 +2038,8 @@ public class UploadPack { .filter(obj -> !(obj instanceof RevCommit)) .limit(1) .collect(Collectors.toList()).get(0); - throw new WantNotValidException(nonCommit); + throw new WantNotValidException(nonCommit, + new Exception("Cannot walk without bitmaps")); //$NON-NLS-1$ } try (ObjectWalk objWalk = walk.toObjectWalkWithSameObjects()) { @@ -2037,6 +2053,11 @@ public class UploadPack { Optional<RevObject> unreachable = reachabilityChecker .areAllReachable(wantsAsObjs, startersAsObjs); if (unreachable.isPresent()) { + if (!repoHasBitmaps) { + throw new WantNotValidException( + unreachable.get(), new Exception( + "Retry with bitmaps enabled")); //$NON-NLS-1$ + } throw new WantNotValidException(unreachable.get()); } } @@ -2063,6 +2084,10 @@ public class UploadPack { } } + private static <T> Predicate<T> not(Predicate<T> t) { + return t.negate(); + } + static Stream<Ref> importantRefsFirst( Collection<Ref> visibleRefs) { Predicate<Ref> startsWithRefsHeads = ref -> ref.getName() @@ -2175,6 +2200,11 @@ public class UploadPack { if (want.has(SATISFIED)) return true; + if (((RevCommit) want).getParentCount() == 0) { + want.add(SATISFIED); + return true; + } + walk.resetRetain(SAVE); walk.markStart((RevCommit) want); if (oldestTime != 0) @@ -2334,7 +2364,7 @@ public class UploadPack { : req.getDepth() - 1; pw.setShallowPack(req.getDepth(), unshallowCommits); - @SuppressWarnings("resource") // Ownership is transferred below + // Ownership is transferred below DepthWalk.RevWalk dw = new DepthWalk.RevWalk( walk.getObjectReader(), walkDepth); dw.setDeepenSince(req.getDeepenSince()); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UrlConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UrlConfig.java new file mode 100644 index 0000000000..574fcf806d --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UrlConfig.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.transport; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.eclipse.jgit.lib.Config; + +/** + * Support for URL translations via git configs {@code url.<base>.insteadOf} and + * {@code url.<base>.pushInsteadOf}. + * + * @since 6.2 + */ +public class UrlConfig { + + private static final String KEY_INSTEADOF = "insteadof"; //$NON-NLS-1$ + + private static final String KEY_PUSHINSTEADOF = "pushinsteadof"; //$NON-NLS-1$ + + private static final String SECTION_URL = "url"; //$NON-NLS-1$ + + private final Config config; + + private Map<String, String> insteadOf; + + private Map<String, String> pushInsteadOf; + + /** + * Creates a new {@link UrlConfig} instance. + * + * @param config + * {@link Config} to read values from + */ + public UrlConfig(Config config) { + this.config = config; + } + + /** + * Performs replacements as defined by git config + * {@code url.<base>.insteadOf}. If there is no match, the input is returned + * unchanged. + * + * @param url + * to substitute + * @return the {@code url} with substitution applied + */ + public String replace(String url) { + if (insteadOf == null) { + insteadOf = load(KEY_INSTEADOF); + } + return replace(url, insteadOf); + } + + /** + * Tells whether there are push replacements. + * + * @return {@code true} if there are push replacements, {@code false} + * otherwise + */ + public boolean hasPushReplacements() { + if (pushInsteadOf == null) { + pushInsteadOf = load(KEY_PUSHINSTEADOF); + } + return !pushInsteadOf.isEmpty(); + } + + /** + * Performs replacements as defined by git config + * {@code url.<base>.pushInsteadOf}. If there is no match, the input is + * returned unchanged. + * + * @param url + * to substitute + * @return the {@code url} with substitution applied + */ + public String replacePush(String url) { + if (pushInsteadOf == null) { + pushInsteadOf = load(KEY_PUSHINSTEADOF); + } + return replace(url, pushInsteadOf); + } + + private Map<String, String> load(String key) { + Map<String, String> replacements = new HashMap<>(); + for (String url : config.getSubsections(SECTION_URL)) { + for (String prefix : config.getStringList(SECTION_URL, url, key)) { + replacements.put(prefix, url); + } + } + return replacements; + } + + private String replace(String uri, Map<String, String> replacements) { + Entry<String, String> match = null; + for (Entry<String, String> replacement : replacements.entrySet()) { + // Ignore current entry if not longer than previous match + if (match != null && match.getKey().length() > replacement.getKey() + .length()) { + continue; + } + if (uri.startsWith(replacement.getKey())) { + match = replacement; + } + } + if (match != null) { + return match.getValue() + uri.substring(match.getKey().length()); + } + return uri; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/resolver/FileResolver.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/resolver/FileResolver.java index 3d15ef5e72..046f395049 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/resolver/FileResolver.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/resolver/FileResolver.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2010, Google Inc. and others + * Copyright (C) 2009-2022, Google Inc. and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -18,11 +18,11 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import org.eclipse.jgit.errors.RepositoryNotFoundException; -import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.RepositoryCache; import org.eclipse.jgit.lib.RepositoryCache.FileKey; import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.StringUtils; /** * Default resolver serving from the local filesystem. @@ -67,7 +67,7 @@ public class FileResolver<C> implements RepositoryResolver<C> { if (isUnreasonableName(name)) throw new RepositoryNotFoundException(name); - Repository db = exports.get(nameWithDotGit(name)); + Repository db = exports.get(StringUtils.nameWithDotGit(name)); if (db != null) { db.incrementOpen(); return db; @@ -154,7 +154,7 @@ public class FileResolver<C> implements RepositoryResolver<C> { * the repository instance. */ public void exportRepository(String name, Repository db) { - exports.put(nameWithDotGit(name), db); + exports.put(StringUtils.nameWithDotGit(name), db); } /** @@ -197,12 +197,6 @@ public class FileResolver<C> implements RepositoryResolver<C> { return false; } - private static String nameWithDotGit(String name) { - if (name.endsWith(Constants.DOT_GIT_EXT)) - return name; - return name + Constants.DOT_GIT_EXT; - } - private static boolean isUnreasonableName(String name) { if (name.length() == 0) return true; // no empty paths 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 1f614e31f6..8269666d26 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java @@ -1,6 +1,6 @@ /* - * Copyright (C) 2008-2009, Google Inc. - * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others + * Copyright (C) 2008, 2009 Google Inc. + * Copyright (C) 2008, 2022 Shawn O. Pearce <spearce@spearce.org> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -14,6 +14,7 @@ package org.eclipse.jgit.treewalk; import static java.nio.charset.StandardCharsets.UTF_8; import java.io.IOException; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -73,6 +74,7 @@ import org.eclipse.jgit.util.io.EolStreamTypeUtil; * threads. */ public class TreeWalk implements AutoCloseable, AttributesProvider { + private static final AbstractTreeIterator[] NO_TREES = {}; /** @@ -92,7 +94,7 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { } /** - * Type of operation you want to retrieve the git attributes for. + * Type of operation you want to retrieve the git attributes for. */ private OperationType operationType = OperationType.CHECKOUT_OP; @@ -284,11 +286,20 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { AbstractTreeIterator currentHead; - /** Cached attribute for the current entry */ - private Attributes attrs = null; + /** + * Cached attributes for the current entry; per tree. Index i+1 is for tree + * i; index 0 is for the deprecated legacy behavior. + */ + private Attributes[] attrs; + + /** + * Cached attributes handler; per tree. Index i+1 is for tree i; index 0 is + * for the deprecated legacy behavior. + */ + private AttributesHandler[] attributesHandlers; - /** Cached attributes handler */ - private AttributesHandler attributesHandler; + /** Can be set to identify the tree to use for {@link #getAttributes()}. */ + private int headIndex = -1; private Config config; @@ -515,6 +526,24 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { } /** + * Identifies the tree at the given index as the head tree. This is the tree + * use by default to determine attributes and EOL modes. + * + * @param index + * of the tree to use as head + * @throws IllegalArgumentException + * if the index is out of range + * @since 6.1 + */ + public void setHead(int index) { + if (index < 0 || index >= trees.length) { + throw new IllegalArgumentException("Head index " + index //$NON-NLS-1$ + + " out of range [0," + trees.length + ')'); //$NON-NLS-1$ + } + headIndex = index; + } + + /** * {@inheritDoc} * <p> * Retrieve the git attributes for the current entry. @@ -556,25 +585,51 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { */ @Override public Attributes getAttributes() { - if (attrs != null) - return attrs; + return getAttributes(headIndex); + } + /** + * Retrieves the git attributes based on the given tree. + * + * @param index + * of the tree to use as base for the attributes + * @return the attributes + * @since 6.1 + */ + public Attributes getAttributes(int index) { + int attrIndex = index + 1; + Attributes result = attrs[attrIndex]; + if (result != null) { + return result; + } if (attributesNodeProvider == null) { - // The work tree should have a AttributesNodeProvider to be able to - // retrieve the info and global attributes node throw new IllegalStateException( "The tree walk should have one AttributesNodeProvider set in order to compute the git attributes."); //$NON-NLS-1$ } try { - // Lazy create the attributesHandler on the first access of - // attributes. This requires the info, global and root - // attributes nodes - if (attributesHandler == null) { - attributesHandler = new AttributesHandler(this); + AttributesHandler handler = attributesHandlers[attrIndex]; + if (handler == null) { + if (index < 0) { + // Legacy behavior (headIndex not set, getAttributes() above + // called) + handler = new AttributesHandler(this, () -> { + return getTree(CanonicalTreeParser.class); + }); + } else { + handler = new AttributesHandler(this, () -> { + AbstractTreeIterator tree = trees[index]; + if (tree instanceof CanonicalTreeParser) { + return (CanonicalTreeParser) tree; + } + return null; + }); + } + attributesHandlers[attrIndex] = handler; } - attrs = attributesHandler.getAttributes(); - return attrs; + result = handler.getAttributes(); + attrs[attrIndex] = result; + return result; } catch (IOException e) { throw new JGitInternalException("Error while parsing attributes", //$NON-NLS-1$ e); @@ -595,11 +650,34 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { */ @Nullable public EolStreamType getEolStreamType(OperationType opType) { - if (attributesNodeProvider == null || config == null) + if (attributesNodeProvider == null || config == null) { return null; - return EolStreamTypeUtil.detectStreamType( - opType != null ? opType : operationType, - config.get(WorkingTreeOptions.KEY), getAttributes()); + } + OperationType op = opType != null ? opType : operationType; + return EolStreamTypeUtil.detectStreamType(op, + config.get(WorkingTreeOptions.KEY), getAttributes()); + } + + /** + * Get the EOL stream type of the current entry for checking out using the + * config and {@link #getAttributes()}. + * + * @param tree + * index of the tree the check-out is to be from + * @return the EOL stream type of the current entry using the config and + * {@link #getAttributes()}. Note that this method may return null + * if the {@link org.eclipse.jgit.treewalk.TreeWalk} is not based on + * a working tree + * @since 6.1 + */ + @Nullable + public EolStreamType getCheckoutEolStreamType(int tree) { + if (attributesNodeProvider == null || config == null) { + return null; + } + Attributes attr = getAttributes(tree); + return EolStreamTypeUtil.detectStreamType(OperationType.CHECKOUT_OP, + config.get(WorkingTreeOptions.KEY), attr); } /** @@ -607,7 +685,8 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { */ public void reset() { attrs = null; - attributesHandler = null; + attributesHandlers = null; + headIndex = -1; trees = NO_TREES; advance = false; depth = 0; @@ -651,7 +730,9 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { advance = false; depth = 0; - attrs = null; + attrs = new Attributes[2]; + attributesHandlers = new AttributesHandler[2]; + headIndex = -1; } /** @@ -701,7 +782,14 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { trees = r; advance = false; depth = 0; - attrs = null; + if (oldLen == newLen) { + Arrays.fill(attrs, null); + Arrays.fill(attributesHandlers, null); + } else { + attrs = new Attributes[newLen + 1]; + attributesHandlers = new AttributesHandler[newLen + 1]; + } + headIndex = -1; } /** @@ -758,6 +846,16 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { p.matchShift = 0; trees = newTrees; + if (attrs == null) { + attrs = new Attributes[n + 2]; + } else { + attrs = Arrays.copyOf(attrs, n + 2); + } + if (attributesHandlers == null) { + attributesHandlers = new AttributesHandler[n + 2]; + } else { + attributesHandlers = Arrays.copyOf(attributesHandlers, n + 2); + } return n; } @@ -800,7 +898,7 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { } for (;;) { - attrs = null; + Arrays.fill(attrs, null); final AbstractTreeIterator t = min(); if (t.eof()) { if (depth > 0) { @@ -1255,7 +1353,7 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { */ public void enterSubtree() throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, IOException { - attrs = null; + Arrays.fill(attrs, null); final AbstractTreeIterator ch = currentHead; final AbstractTreeIterator[] tmp = new AbstractTreeIterator[trees.length]; for (int i = 0; i < trees.length; i++) { @@ -1374,11 +1472,12 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { /** * Inspect config and attributes to return a filtercommand applicable for - * the current path, but without expanding %f occurences + * the current path. * * @param filterCommandType * which type of filterCommand should be executed. E.g. "clean", - * "smudge" + * "smudge". For "smudge" consider using + * {{@link #getSmudgeCommand(int)} instead. * @return a filter command * @throws java.io.IOException * @since 4.2 @@ -1407,6 +1506,54 @@ public class TreeWalk implements AutoCloseable, AttributesProvider { } /** + * Inspect config and attributes to return a filtercommand applicable for + * the current path. + * + * @param index + * of the tree the item to be smudged is in + * @return a filter command + * @throws java.io.IOException + * @since 6.1 + */ + public String getSmudgeCommand(int index) + throws IOException { + return getSmudgeCommand(getAttributes(index)); + } + + /** + * Inspect config and attributes to return a filtercommand applicable for + * the current path. + * + * @param attributes + * to use + * @return a filter command + * @throws java.io.IOException + * @since 6.1 + */ + public String getSmudgeCommand(Attributes attributes) throws IOException { + if (attributes == null) { + return null; + } + Attribute f = attributes.get(Constants.ATTR_FILTER); + if (f == null) { + return null; + } + String filterValue = f.getValue(); + if (filterValue == null) { + return null; + } + + String filterCommand = getFilterCommandDefinition(filterValue, + Constants.ATTR_FILTER_TYPE_SMUDGE); + if (filterCommand == null) { + return null; + } + return filterCommand.replaceAll("%f", //$NON-NLS-1$ + Matcher.quoteReplacement( + QuotedString.BOURNE.quote((getPathString())))); + } + + /** * Get the filter command how it is defined in gitconfig. The returned * string may contain "%f" which needs to be replaced by the current path * before executing the filter command. These filter definitions are cached 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 50ce15ebc9..427eac5b53 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java @@ -2,7 +2,7 @@ * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com> * Copyright (C) 2010, Matthias Sohn <matthias.sohn@sap.com> - * Copyright (C) 2012-2021, Robin Rosenberg and others + * Copyright (C) 2012, 2022, Robin Rosenberg and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -387,8 +387,8 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { state.initializeReadBuffer(); final long len = e.getLength(); - InputStream filteredIs = possiblyFilteredInputStream(e, is, len, - OperationType.CHECKIN_OP); + InputStream filteredIs = possiblyFilteredInputStream(e, is, + len); return computeHash(filteredIs, canonLen); } finally { safeClose(is); @@ -400,23 +400,18 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { } private InputStream possiblyFilteredInputStream(final Entry e, - final InputStream is, final long len) throws IOException { - return possiblyFilteredInputStream(e, is, len, null); - - } - - private InputStream possiblyFilteredInputStream(final Entry e, - final InputStream is, final long len, OperationType opType) + final InputStream is, final long len) throws IOException { if (getCleanFilterCommand() == null - && getEolStreamType(opType) == EolStreamType.DIRECT) { + && getEolStreamType( + OperationType.CHECKIN_OP) == EolStreamType.DIRECT) { canonLen = len; return is; } if (len <= MAXIMUM_FILE_SIZE_TO_READ_FULLY) { ByteBuffer rawbuf = IO.readWholeStream(is, (int) len); - rawbuf = filterClean(rawbuf.array(), rawbuf.limit(), opType); + rawbuf = filterClean(rawbuf.array(), rawbuf.limit()); canonLen = rawbuf.limit(); return new ByteArrayInputStream(rawbuf.array(), 0, (int) canonLen); } @@ -426,14 +421,13 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { return is; } - final InputStream lenIs = filterClean(e.openInputStream(), - opType); + final InputStream lenIs = filterClean(e.openInputStream()); try { canonLen = computeLength(lenIs); } finally { safeClose(lenIs); } - return filterClean(is, opType); + return filterClean(is); } private static void safeClose(InputStream in) { @@ -455,23 +449,20 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { } } - private ByteBuffer filterClean(byte[] src, int n, OperationType opType) + private ByteBuffer filterClean(byte[] src, int n) throws IOException { InputStream in = new ByteArrayInputStream(src); try { - return IO.readWholeStream(filterClean(in, opType), n); + return IO.readWholeStream(filterClean(in), n); } finally { safeClose(in); } } - private InputStream filterClean(InputStream in) throws IOException { - return filterClean(in, null); - } - - private InputStream filterClean(InputStream in, OperationType opType) + private InputStream filterClean(InputStream in) throws IOException { - in = handleAutoCRLF(in, opType); + in = EolStreamTypeUtil.wrapInputStream(in, + getEolStreamType(OperationType.CHECKIN_OP)); String filterCommand = getCleanFilterCommand(); if (filterCommand != null) { if (FilterCommandRegistry.isRegistered(filterCommand)) { @@ -509,11 +500,6 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { return in; } - private InputStream handleAutoCRLF(InputStream in, OperationType opType) - throws IOException { - return EolStreamTypeUtil.wrapInputStream(in, getEolStreamType(opType)); - } - /** * Returns the working tree options used by this iterator. * @@ -664,7 +650,8 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator { public InputStream openEntryStream() throws IOException { InputStream rawis = current().openInputStream(); if (getCleanFilterCommand() == null - && getEolStreamType() == EolStreamType.DIRECT) { + && getEolStreamType( + OperationType.CHECKIN_OP) == EolStreamType.DIRECT) { return rawis; } return filterClean(rawis); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/Equality.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/Equality.java new file mode 100644 index 0000000000..da1684630b --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/Equality.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2022, Fabio Ponciroli <ponch78@gmail.com> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.util; + +/** + * Equality utilities. + * + * @since: 6.2 + */ +public class Equality { + + /** + * Compare by reference + * + * @param a + * First object to compare + * @param b + * Second object to compare + * @return {@code true} if the objects are identical, {@code false} + * otherwise + * + * @since 6.2 + */ + @SuppressWarnings("ReferenceEquality") + public static <T> boolean isSameInstance(T a, T b) { + return a == b; + } +}
\ No newline at end of file diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java index 9237c0a9b2..e8f38d8fd9 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java @@ -47,7 +47,6 @@ import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; @@ -262,8 +261,9 @@ public abstract class FS { * * @see java.util.concurrent.Executors#newCachedThreadPool() */ - private static final Executor FUTURE_RUNNER = new ThreadPoolExecutor(0, - 5, 30L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), + private static final ExecutorService FUTURE_RUNNER = new ThreadPoolExecutor( + 0, 5, 30L, TimeUnit.SECONDS, + new LinkedBlockingQueue<Runnable>(), runnable -> { Thread t = new Thread(runnable, "JGit-FileStoreAttributeReader-" //$NON-NLS-1$ @@ -285,8 +285,9 @@ public abstract class FS { * small keep-alive time to avoid delays on shut-down. * </p> */ - private static final Executor SAVE_RUNNER = new ThreadPoolExecutor(0, 1, - 1L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), + private static final ExecutorService SAVE_RUNNER = new ThreadPoolExecutor( + 0, 1, 1L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<Runnable>(), runnable -> { Thread t = new Thread(runnable, "JGit-FileStoreAttributeWriter-" //$NON-NLS-1$ @@ -296,6 +297,18 @@ public abstract class FS { return t; }); + static { + // Shut down the SAVE_RUNNER on System.exit() + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + try { + SAVE_RUNNER.shutdownNow(); + SAVE_RUNNER.awaitTermination(100, TimeUnit.MILLISECONDS); + } catch (Exception e) { + // Ignore; we're shutting down + } + })); + } + /** * Whether FileStore attributes should be determined asynchronously * @@ -452,11 +465,13 @@ public abstract class FS { return null; } // fall through and return fallback - } catch (IOException | InterruptedException - | ExecutionException | CancellationException e) { + } catch (IOException | ExecutionException | CancellationException e) { LOG.error(e.getMessage(), e); } catch (TimeoutException | SecurityException e) { // use fallback + } catch (InterruptedException e) { + LOG.error(e.getMessage(), e); + Thread.currentThread().interrupt(); } LOG.debug("{}: use fallback timestamp resolution for directory {}", //$NON-NLS-1$ Thread.currentThread(), dir); @@ -474,6 +489,7 @@ public abstract class FS { Path probe = dir.resolve(".probe-" + UUID.randomUUID()); //$NON-NLS-1$ Instant end = Instant.now().plusSeconds(3); try { + probe.toFile().deleteOnExit(); Files.createFile(probe); do { n++; @@ -540,6 +556,7 @@ public abstract class FS { } Path probe = dir.resolve(".probe-" + UUID.randomUUID()); //$NON-NLS-1$ try { + probe.toFile().deleteOnExit(); Files.createFile(probe); Duration fsResolution = getFsResolution(s, dir, probe); Duration clockResolution = measureClockResolution(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32.java index ff094f6975..ae73d3feb8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32.java @@ -16,6 +16,7 @@ import java.io.IOException; import java.nio.file.FileVisitOption; import java.nio.file.FileVisitResult; import java.nio.file.Files; +import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; @@ -96,6 +97,9 @@ public class FS_Win32 extends FS { /** {@inheritDoc} */ @Override public Entry[] list(File directory, FileModeStrategy fileModeStrategy) { + if (!Files.isDirectory(directory.toPath(), LinkOption.NOFOLLOW_LINKS)) { + return NO_ENTRIES; + } List<Entry> result = new ArrayList<>(); FS fs = this; boolean checkExecutable = fs.supportsExecute(); 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 b9dd9baa61..f013e7e095 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java @@ -17,6 +17,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InterruptedIOException; import java.nio.channels.FileChannel; import java.nio.file.AtomicMoveNotSupportedException; import java.nio.file.CopyOption; @@ -655,6 +656,99 @@ public class FileUtils { } /** + * Like a {@link java.util.function.Function} but throwing an + * {@link Exception}. + * + * @param <A> + * input type + * @param <B> + * output type + * @since 6.2 + */ + @FunctionalInterface + public interface IOFunction<A, B> { + + /** + * Performs the function. + * + * @param t + * input to operate on + * @return the output + * @throws Exception + * if a problem occurs + */ + B apply(A t) throws Exception; + } + + private static void backOff(long delay, IOException cause) + throws IOException { + try { + Thread.sleep(delay); + } catch (InterruptedException e) { + IOException interruption = new InterruptedIOException(); + interruption.initCause(e); + interruption.addSuppressed(cause); + Thread.currentThread().interrupt(); // Re-set flag + throw interruption; + } + } + + /** + * Invokes the given {@link IOFunction}, performing a limited number of + * re-tries if exceptions occur that indicate either a stale NFS file handle + * or that indicate that the file may be written concurrently. + * + * @param <T> + * result type + * @param file + * to read + * @param reader + * for reading the file and creating an instance of {@code T} + * @return the result of the {@code reader}, or {@code null} if the file + * does not exist + * @throws Exception + * if a problem occurs + * @since 6.2 + */ + public static <T> T readWithRetries(File file, + IOFunction<File, ? extends T> reader) + throws Exception { + int maxStaleRetries = 5; + int retries = 0; + long backoff = 50; + while (true) { + try { + try { + return reader.apply(file); + } catch (IOException e) { + if (FileUtils.isStaleFileHandleInCausalChain(e) + && retries < maxStaleRetries) { + if (LOG.isDebugEnabled()) { + LOG.debug(MessageFormat.format( + JGitText.get().packedRefsHandleIsStale, + Integer.valueOf(retries)), e); + } + retries++; + continue; + } + throw e; + } + } catch (FileNotFoundException noFile) { + if (!file.isFile()) { + return null; + } + // Probably Windows and some other thread is writing the file + // concurrently. + if (backoff > 1000) { + throw noFile; + } + backOff(backoff, noFile); + backoff *= 2; // 50, 100, 200, 400, 800 ms + } + } + } + + /** * @param file * @return {@code true} if the passed file is a symbolic link */ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java index 23a73faf8c..663a3449e1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java @@ -186,6 +186,33 @@ public class HttpSupport { } /** + * Translates the provided URL into application/x-www-form-urlencoded + * format. + * + * @param url + * The URL to translate. + * @param keepPathSlash + * Whether or not to keep "/" in the URL (i.e. don't translate + * them to "%2F"). + * + * @return The translated URL. + * @since 5.13 + */ + public static String urlEncode(String url, boolean keepPathSlash) { + String encoded; + try { + encoded = URLEncoder.encode(url, UTF_8.name()); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(JGitText.get().couldNotURLEncodeToUTF8, + e); + } + if (keepPathSlash) { + encoded = encoded.replace("%2F", "/"); //$NON-NLS-1$ //$NON-NLS-2$ + } + return encoded; + } + + /** * Get the HTTP response code from the request. * <p> * Roughly the same as <code>c.getResponseCode()</code> but the diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/StringUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/StringUtils.java index 8ab13385e0..917add3609 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/StringUtils.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/StringUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2010, Google Inc. and others + * Copyright (C) 2009-2022, Google Inc. and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -15,6 +15,7 @@ import java.util.Collection; import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.Constants; /** * Miscellaneous string comparison utility methods. @@ -37,6 +38,10 @@ public final class StringUtils { LC[c] = (char) ('a' + (c - 'A')); } + private StringUtils() { + // Do not create instances + } + /** * Convert the input to lowercase. * <p> @@ -269,8 +274,20 @@ public final class StringUtils { return sb.toString(); } - private StringUtils() { - // Do not create instances + /** + * Appends {@link Constants#DOT_GIT_EXT} unless the given name already ends + * with that suffix. + * + * @param name + * to complete + * @return the name ending with {@link Constants#DOT_GIT_EXT} + * @since 6.1 + */ + public static String nameWithDotGit(String name) { + if (name.endsWith(Constants.DOT_GIT_EXT)) { + return name; + } + return name + Constants.DOT_GIT_EXT; } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java index 16e2577911..5ced0713e0 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java @@ -63,6 +63,8 @@ public abstract class SystemReader { private static volatile Boolean isWindows; + private static volatile Boolean isLinux; + static { SystemReader r = new Default(); r.init(); @@ -185,6 +187,7 @@ public abstract class SystemReader { public static void setInstance(SystemReader newReader) { isMacOS = null; isWindows = null; + isLinux = null; if (newReader == null) INSTANCE = DEFAULT; else { @@ -543,6 +546,20 @@ public abstract class SystemReader { return isMacOS.booleanValue(); } + /** + * Whether we are running on Linux. + * + * @return true if we are running on Linux. + * @since 6.3 + */ + public boolean isLinux() { + if (isLinux == null) { + String osname = getOsName(); + isLinux = Boolean.valueOf(osname.toLowerCase().startsWith("linux")); //$NON-NLS-1$ + } + return isLinux.booleanValue(); + } + private String getOsName() { return AccessController.doPrivileged( (PrivilegedAction<String>) () -> getProperty("os.name") //$NON-NLS-1$ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/BinaryHunkInputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/BinaryHunkInputStream.java index 4f940d77a0..2c972b578d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/BinaryHunkInputStream.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/BinaryHunkInputStream.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others + * Copyright (C) 2021, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -90,7 +90,7 @@ public class BinaryHunkInputStream extends InputStream { byte[] encoded = new byte[Base85.encodedLength(length)]; for (int i = 0; i < encoded.length; i++) { int b = in.read(); - if (b < 0 || b == '\n') { + if (b < 0 || b == '\r' || b == '\n') { throw new EOFException(MessageFormat.format( JGitText.get().binaryHunkInvalidLength, Integer.valueOf(lineNumber))); @@ -99,6 +99,10 @@ public class BinaryHunkInputStream extends InputStream { } // Must be followed by a newline; tolerate EOF. int b = in.read(); + if (b == '\r') { + // Be lenient and accept CR-LF, too. + b = in.read(); + } if (b >= 0 && b != '\n') { throw new StreamCorruptedException(MessageFormat.format( JGitText.get().binaryHunkMissingNewline, @@ -150,28 +150,27 @@ <java.version>11</java.version> <bundle-manifest>${project.build.directory}/META-INF/MANIFEST.MF</bundle-manifest> - <jgit-last-release-version>5.13.0.202109080827-r</jgit-last-release-version> + <jgit-last-release-version>6.2.0.202206071550-r</jgit-last-release-version> <ant-version>1.10.12</ant-version> - <apache-sshd-version>2.7.0</apache-sshd-version> + <apache-sshd-version>2.8.0</apache-sshd-version> <jsch-version>0.1.55</jsch-version> - <jzlib-version>1.1.1</jzlib-version> + <jzlib-version>1.1.3</jzlib-version> <javaewah-version>1.1.13</javaewah-version> <junit-version>4.13.2</junit-version> <test-fork-count>1C</test-fork-count> <args4j-version>2.33</args4j-version> <commons-compress-version>1.21</commons-compress-version> - <osgi-core-version>4.3.1</osgi-core-version> - <servlet-api-version>3.1.0</servlet-api-version> + <osgi-core-version>6.0.0</osgi-core-version> + <servlet-api-version>4.0.0</servlet-api-version> <jetty-version>10.0.6</jetty-version> <japicmp-version>0.15.3</japicmp-version> <httpclient-version>4.5.13</httpclient-version> <httpcore-version>4.4.14</httpcore-version> <slf4j-version>1.7.30</slf4j-version> - <log4j-version>1.2.15</log4j-version> <maven-javadoc-plugin-version>3.3.1</maven-javadoc-plugin-version> - <tycho-extras-version>2.5.0</tycho-extras-version> - <gson-version>2.8.8</gson-version> - <bouncycastle-version>1.69</bouncycastle-version> + <tycho-extras-version>2.6.0</tycho-extras-version> + <gson-version>2.8.9</gson-version> + <bouncycastle-version>1.70</bouncycastle-version> <spotbugs-maven-plugin-version>4.3.0</spotbugs-maven-plugin-version> <maven-project-info-reports-plugin-version>3.1.2</maven-project-info-reports-plugin-version> <maven-jxr-plugin-version>3.1.1</maven-jxr-plugin-version> @@ -204,6 +203,14 @@ <id>repo.eclipse.org.cbi-snapshots</id> <url>https://repo.eclipse.org/content/repositories/cbi-snapshots/</url> </pluginRepository> + <pluginRepository> + <id>repo.eclipse.org.dash-releases</id> + <url>https://repo.eclipse.org/content/repositories/dash-licenses-releases/</url> + </pluginRepository> + <pluginRepository> + <id>repo.eclipse.org.dash-snapshots</id> + <url>https://repo.eclipse.org/content/repositories/dash-licenses-snapshots/</url> + </pluginRepository> </pluginRepositories> <build> @@ -339,7 +346,7 @@ <dependency><!-- add support for ssh/scp --> <groupId>org.apache.maven.wagon</groupId> <artifactId>wagon-ssh</artifactId> - <version>3.4.3</version> + <version>3.5.1</version> </dependency> </dependencies> </plugin> @@ -383,6 +390,11 @@ <artifactId>spring-boot-maven-plugin</artifactId> <version>2.5.4</version> </plugin> + <plugin> + <groupId>org.eclipse.dash</groupId> + <artifactId>license-tool-plugin</artifactId> + <version>0.0.1-SNAPSHOT</version> + </plugin> </plugins> </pluginManagement> @@ -541,6 +553,10 @@ <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-report-plugin</artifactId> </plugin> + <plugin> + <groupId>org.eclipse.dash</groupId> + <artifactId>license-tool-plugin</artifactId> + </plugin> </plugins> </build> @@ -722,35 +738,11 @@ <dependency> <groupId>org.slf4j</groupId> - <artifactId>slf4j-log4j12</artifactId> + <artifactId>slf4j-simple</artifactId> <version>${slf4j-version}</version> </dependency> <dependency> - <groupId>log4j</groupId> - <artifactId>log4j</artifactId> - <version>${log4j-version}</version> - <exclusions> - <exclusion> - <groupId>javax.mail</groupId> - <artifactId>mail</artifactId> - </exclusion> - <exclusion> - <groupId>javax.jms</groupId> - <artifactId>jms</artifactId> - </exclusion> - <exclusion> - <groupId>com.sun.jdmk</groupId> - <artifactId>jmxtools</artifactId> - </exclusion> - <exclusion> - <groupId>com.sun.jmx</groupId> - <artifactId>jmxri</artifactId> - </exclusion> - </exclusions> - </dependency> - - <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>${gson-version}</version> @@ -878,7 +870,7 @@ <dependency> <groupId>org.eclipse.jdt</groupId> <artifactId>ecj</artifactId> - <version>3.27.0</version> + <version>3.28.0</version> </dependency> </dependencies> </plugin> diff --git a/tools/BUILD b/tools/BUILD index 0c6b8ece7c..a10901982f 100644 --- a/tools/BUILD +++ b/tools/BUILD @@ -1,34 +1,28 @@ load( "@bazel_tools//tools/jdk:default_java_toolchain.bzl", - "JDK9_JVM_OPTS", "default_java_toolchain", ) load("@rules_java//java:defs.bzl", "java_package_configuration") -JDK11_JVM_OPTS = [ - "--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED", - "--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED", - "--add-exports=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED", - "--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED", - "--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED", - "--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED", - "--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED", - "--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED", - "--patch-module=java.compiler=$(location @bazel_tools//tools/jdk:java_compiler_jar)", - "--patch-module=jdk.compiler=$(location @bazel_tools//tools/jdk:jdk_compiler_jar)", - "--add-opens=java.base/java.nio=ALL-UNNAMED", - "--add-opens=java.base/java.lang=ALL-UNNAMED", -] - default_java_toolchain( - name = "error_prone_warnings_toolchain", - bootclasspath = ["@bazel_tools//tools/jdk:platformclasspath.jar"], - jvm_opts = JDK11_JVM_OPTS, + name = "error_prone_warnings_toolchain_java11", + package_configuration = [ + ":error_prone", + ], source_version = "11", target_version = "11", + visibility = ["//visibility:public"], +) + +default_java_toolchain( + name = "error_prone_warnings_toolchain_java17", + configuration = dict(), + java_runtime = "@bazel_tools//tools/jdk:remotejdk_17", package_configuration = [ ":error_prone", ], + source_version = "17", + target_version = "17", visibility = ["//visibility:public"], ) @@ -108,20 +102,25 @@ package_group( "//org.eclipse.jgit.ant.test/...", "//org.eclipse.jgit.ant/...", "//org.eclipse.jgit.archive/...", + "//org.eclipse.jgit.gpg.bc.test/...", + "//org.eclipse.jgit.gpg.bc/...", "//org.eclipse.jgit.http.apache/...", "//org.eclipse.jgit.http.server/...", "//org.eclipse.jgit.http.test/...", - "//org.eclipse.jgit.junit.http/...", + "//org.eclipse.jgit.junit.ssh/...", "//org.eclipse.jgit.junit/...", + "//org.eclipse.jgit.junit/http/...", "//org.eclipse.jgit.lfs.server.test/...", "//org.eclipse.jgit.lfs.server/...", "//org.eclipse.jgit.lfs.test/...", "//org.eclipse.jgit.lfs/...", - "//org.eclipse.jgit.packaging/...", "//org.eclipse.jgit.pgm.test/...", "//org.eclipse.jgit.pgm/...", - "//org.eclipse.jgit.ssh.apache/...", "//org.eclipse.jgit.ssh.apache.agent/...", + "//org.eclipse.jgit.ssh.apache.test/...", + "//org.eclipse.jgit.ssh.apache/...", + "//org.eclipse.jgit.ssh.jsch.test/...", + "//org.eclipse.jgit.ssh.jsch/...", "//org.eclipse.jgit.test/...", "//org.eclipse.jgit.ui/...", "//org.eclipse.jgit/...", diff --git a/tools/bazelisk_version.bzl b/tools/bazelisk_version.bzl deleted file mode 100644 index d8b3d10982..0000000000 --- a/tools/bazelisk_version.bzl +++ /dev/null @@ -1,16 +0,0 @@ -_template = """ -load("@bazel_skylib//lib:versions.bzl", "versions") - -def check_bazel_version(): - versions.check(minimum_bazel_version = "{version}") -""".strip() - -def _impl(repository_ctx): - repository_ctx.symlink(Label("@//:.bazelversion"), ".bazelversion") - bazelversion = repository_ctx.read(".bazelversion").strip() - - repository_ctx.file("BUILD", executable = False) - - repository_ctx.file("check.bzl", executable = False, content = _template.format(version = bazelversion)) - -bazelisk_version = repository_rule(implementation = _impl) diff --git a/tools/remote-bazelrc b/tools/remote-bazelrc new file mode 100644 index 0000000000..58f794e18b --- /dev/null +++ b/tools/remote-bazelrc @@ -0,0 +1,67 @@ +# Copyright 2022 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This file is auto-generated from release/bazelrc.tpl and should not be +# modified directly. + +# This .bazelrc file contains all of the flags required for the provided +# toolchain with Remote Build Execution. +# +# This .bazelrc file also contains all of the flags required for the local +# docker sandboxing. + +# Depending on how many machines are in the remote execution instance, setting +# this higher can make builds faster by allowing more jobs to run in parallel. +# Setting it too high can result in jobs that timeout, however, while waiting +# for a remote machine to execute them. +build:remote --jobs=200 +build:remote --disk_cache= + +# Set several flags related to specifying the platform, toolchain and java +# properties. +build:remote --host_javabase=@rbe_jdk11//java:jdk +build:remote --javabase=@rbe_jdk11//java:jdk +build:remote --crosstool_top=@rbe_jdk11//cc:toolchain +build:remote --extra_toolchains=@rbe_jdk11//config:cc-toolchain +build:remote --extra_execution_platforms=@rbe_jdk11//config:platform +build:remote --host_platform=@rbe_jdk11//config:platform +build:remote --platforms=@rbe_jdk11//config:platform +build:remote --action_env=BAZEL_DO_NOT_DETECT_CPP_TOOLCHAIN=1 + +# Set various strategies so that all actions execute remotely. Mixing remote +# and local execution will lead to errors unless the toolchain and remote +# machine exactly match the host machine. +build:remote --define=EXECUTOR=remote + +# Enable the remote cache so action results can be shared across machines, +# developers, and workspaces. +build:remote --remote_cache=remotebuildexecution.googleapis.com + +# Enable remote execution so actions are performed on the remote systems. +build:remote --remote_executor=remotebuildexecution.googleapis.com + +# Set a higher timeout value, just in case. +build:remote --remote_timeout=3600 + +# Enable authentication. This will pick up application default credentials by +# default. You can use --auth_credentials=some_file.json to use a service +# account credential instead. +build:remote --google_default_credentials + +# The following flags enable the remote cache so action results can be shared +# across machines, developers, and workspaces. +build:remote-cache --remote_cache=remotebuildexecution.googleapis.com +build:remote-cache --tls_enabled=true +build:remote-cache --remote_timeout=3600 +build:remote-cache --auth_enabled=true |