aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.bazelrc27
-rw-r--r--.bazelversion2
-rw-r--r--.mailmap1
-rw-r--r--DEPENDENCIES109
-rw-r--r--WORKSPACE85
-rw-r--r--lib/BUILD5
-rw-r--r--org.eclipse.jgit.ant/pom.xml1
-rw-r--r--org.eclipse.jgit.benchmarks/src/org/eclipse/jgit/benchmarks/GetRefsBenchmark.java181
-rw-r--r--org.eclipse.jgit.gpg.bc.test/BUILD2
-rw-r--r--org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/GitSmartHttpTools.java25
-rw-r--r--org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/UploadPackServlet.java24
-rw-r--r--org.eclipse.jgit.http.test/build.properties3
-rw-r--r--org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/HookMessageTest.java1
-rw-r--r--org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/HttpClientTests.java6
-rw-r--r--org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/MeasurePackSizeTest.java1
-rw-r--r--org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java27
-rw-r--r--org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/HttpTestCase.java5
-rw-r--r--org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/RecordingLogger.java6
-rw-r--r--org.eclipse.jgit.junit.ssh/META-INF/MANIFEST.MF50
-rw-r--r--org.eclipse.jgit.junit.ssh/pom.xml10
-rw-r--r--org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java21
-rw-r--r--org.eclipse.jgit.lfs.server.test/tst/org/eclipse/jgit/lfs/server/fs/PushTest.java16
-rw-r--r--org.eclipse.jgit.lfs.server/src/org/eclipse/jgit/lfs/server/fs/ObjectDownloadListener.java1
-rw-r--r--org.eclipse.jgit.lfs.test/META-INF/MANIFEST.MF3
-rw-r--r--org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/LfsConfigGitTest.java301
-rw-r--r--org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/LfsGitTest.java23
-rw-r--r--org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/internal/LfsConnectionFactoryTest.java317
-rw-r--r--org.eclipse.jgit.lfs/META-INF/MANIFEST.MF1
-rw-r--r--org.eclipse.jgit.lfs/resources/org/eclipse/jgit/lfs/internal/LfsText.properties19
-rw-r--r--org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/LfsPrePushHook.java11
-rw-r--r--org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/SmudgeFilter.java2
-rw-r--r--org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/errors/LfsUnauthorized.java2
-rw-r--r--org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsConfig.java220
-rw-r--r--org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsConnectionFactory.java37
-rw-r--r--org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsText.java16
-rw-r--r--org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/lib/Constants.java7
-rw-r--r--org.eclipse.jgit.packaging/.settings/org.eclipse.core.resources.prefs2
-rw-r--r--org.eclipse.jgit.packaging/org.eclipse.jgit.gpg.bc.feature/.settings/org.eclipse.core.resources.prefs2
-rw-r--r--org.eclipse.jgit.packaging/org.eclipse.jgit.repository/category.xml10
-rw-r--r--org.eclipse.jgit.packaging/org.eclipse.jgit.ssh.jsch.feature/.settings/org.eclipse.core.resources.prefs2
-rw-r--r--org.eclipse.jgit.packaging/org.eclipse.jgit.target/.settings/org.eclipse.core.resources.prefs2
-rw-r--r--org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.17.target50
-rw-r--r--org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.17.tpd2
-rw-r--r--org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.18.target50
-rw-r--r--org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.18.tpd2
-rw-r--r--org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.19.target50
-rw-r--r--org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.19.tpd2
-rw-r--r--org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.20.target50
-rw-r--r--org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.20.tpd2
-rw-r--r--org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.21.target50
-rw-r--r--org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.21.tpd2
-rw-r--r--org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.22.target50
-rw-r--r--org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.22.tpd2
-rw-r--r--org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.23.target97
-rw-r--r--org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.23.tpd8
-rw-r--r--org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.24.target97
-rw-r--r--org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.24.tpd8
-rw-r--r--org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.25.target97
-rw-r--r--org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.25.tpd8
-rw-r--r--org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20211213173813-2021-12.tpd69
-rw-r--r--org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20220302172233-2022-03.tpd69
-rw-r--r--org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20220531185310-2022-06.tpd69
-rw-r--r--org.eclipse.jgit.packaging/org.eclipse.jgit.target/pom.xml27
-rw-r--r--org.eclipse.jgit.packaging/pom.xml14
-rw-r--r--org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF5
-rw-r--r--org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DescribeTest.java25
-rw-r--r--org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DiffToolTest.java363
-rw-r--r--org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/FetchTest.java1
-rw-r--r--org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeToolTest.java397
-rw-r--r--org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ToolTestCase.java250
-rw-r--r--org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin1
-rw-r--r--org.eclipse.jgit.pgm/build.properties2
-rw-r--r--org.eclipse.jgit.pgm/pom.xml7
-rw-r--r--org.eclipse.jgit.pgm/resources/log4j.properties6
-rw-r--r--org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties30
-rw-r--r--org.eclipse.jgit.pgm/resources/simplelogger.properties6
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Blame.java4
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Describe.java6
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTool.java263
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Merge.java8
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/MergeTool.java483
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Reflog.java5
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java20
-rw-r--r--org.eclipse.jgit.ssh.apache.agent/BUILD2
-rw-r--r--org.eclipse.jgit.ssh.apache.agent/README.md82
-rw-r--r--org.eclipse.jgit.ssh.apache.agent/pom.xml10
-rw-r--r--org.eclipse.jgit.ssh.apache.agent/resources/org/eclipse/jgit/internal/transport/sshd/agent/connector/Texts.properties3
-rw-r--r--org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/Factory.java25
-rw-r--r--org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/LibraryHolder.java4
-rw-r--r--org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/PageantConnector.java5
-rw-r--r--org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/Sockets.java5
-rw-r--r--org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/Texts.java3
-rw-r--r--org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/UnixDomainSocketConnector.java9
-rw-r--r--org.eclipse.jgit.ssh.apache.agent/src/org/eclipse/jgit/internal/transport/sshd/agent/connector/WinPipeConnector.java216
-rw-r--r--org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF28
-rw-r--r--org.eclipse.jgit.ssh.apache.test/build.properties3
-rw-r--r--org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshTest.java100
-rw-r--r--org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF99
-rw-r--r--org.eclipse.jgit.ssh.apache/README.md47
-rw-r--r--org.eclipse.jgit.ssh.apache/pom.xml6
-rw-r--r--org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties24
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/AuthenticationLogger.java238
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/CachingKeyPairProvider.java60
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthentication.java33
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthenticationReporter.java93
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java19
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPasswordAuthentication.java33
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPublicKeyAuthentication.java369
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java7
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitUserInteraction.java21
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java26
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/JGitSshAgentFactory.java26
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/SshAgentClient.java261
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSession.java40
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java19
-rw-r--r--org.eclipse.jgit.test/BUILD2
-rw-r--r--org.eclipse.jgit.test/META-INF/MANIFEST.MF1
-rw-r--r--org.eclipse.jgit.test/build.properties3
-rw-r--r--org.eclipse.jgit.test/tests.bzl3
-rw-r--r--org.eclipse.jgit.test/tst-rsrc/jgit-s3-config.disabled.properties9
-rw-r--r--org.eclipse.jgit.test/tst-rsrc/log4j.properties14
-rw-r--r--org.eclipse.jgit.test/tst-rsrc/simplelogger.properties12
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ApplyCommandTest.java14
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/BranchCommandTest.java2
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CheckoutCommandTest.java4
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java5
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CloneCommandTest.java44
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java57
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DescribeCommandTest.java30
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/EolRepositoryTest.java114
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/FetchAndPullCommandsRecurseSubmodulesTest.java3
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/FetchCommandTest.java67
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/LsRemoteCommandTest.java16
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java75
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java1
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandWithRebaseTest.java1
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java773
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java130
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RevertCommandTest.java7
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/SecurityManagerMissingPermissionsTest.java28
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StatusCommandTest.java29
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/AbstractRenameDetectionTestCase.java101
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/FilteredRenameDetectorTest.java154
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/RenameDetectorTest.java71
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/BareSuperprojectWriterTest.java106
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java21
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalDiffToolTest.java373
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalMergeToolTest.java433
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTestCase.java (renamed from org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/diffmergetool/ExternalToolTest.java)56
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTest.java118
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BatchRefUpdateTest.java6
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileReftableStackTest.java8
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcTestCase.java1
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/LockFileTest.java12
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java1
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/io/CancellableDigestOutputStreamTest.java105
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFileTest.java122
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/AbbrevConfigTest.java128
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitConfigTest.java258
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitTemplateConfigTest.java59
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java38
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutTest.java14
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/PersonIdentTest.java (renamed from org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/T0001_PersonIdentTest.java)32
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergeMessageFormatterTest.java26
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java1
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/storage/file/FileBasedConfigTest.java7
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleWalkTest.java1
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BasePackConnectionTest.java24
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BasePackPushConnectionTest.java96
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BundleWriterTest.java2
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PackParserTest.java9
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushConfigTest.java60
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushProcessTest.java71
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ReceivePackAdvertiseRefsHookTest.java18
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RefSpecTest.java62
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/SideBandInputStreamTest.java229
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackTest.java55
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/WalkEncryptionTest.java2
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FilterCommandsTest.java144
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/util/HookTest.java29
-rw-r--r--org.eclipse.jgit/.settings/.api_filters88
-rw-r--r--org.eclipse.jgit/META-INF/MANIFEST.MF2
-rw-r--r--org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties19
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/CherryPickCommand.java13
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java109
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java46
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/LsRemoteCommand.java20
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java6
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/PushCommand.java162
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java285
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/RevertCommand.java15
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/StashCreateCommand.java13
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesHandler.java80
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/blame/BlameGenerator.java8
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/diff/ContentSource.java77
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java3
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java67
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/errors/NoRemoteRepositoryException.java15
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/BareSuperprojectWriter.java319
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RegularSuperprojectWriter.java104
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java309
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/hooks/PrePushHook.java25
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java19
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diff/FilteredRenameDetector.java136
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandExecutor.java243
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineDiffTool.java4
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/CommandLineMergeTool.java327
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffToolConfig.java5
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/DiffTools.java323
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalDiffTool.java6
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalMergeTool.java33
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ExternalToolUtils.java242
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/FileElement.java262
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/InformNoToolHandler.java28
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeToolConfig.java147
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/MergeTools.java452
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedDiffTool.java13
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PreDefinedMergeTool.java91
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/PromptContinueHandler.java27
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/ToolException.java141
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedDiffTool.java19
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/diffmergetool/UserDefinedMergeTool.java69
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCache.java90
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfig.java76
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/InMemoryRepository.java9
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/LargePackedWholeObject.java1
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/CachedObjectDirectory.java13
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableStack.java13
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java7
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java14
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/Pack.java5
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFileSnapshot.java4
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java109
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/io/CancellableDigestOutputStream.java124
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/BaseSearch.java1
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackOutputStream.java77
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java96
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/AbbrevConfig.java150
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/BranchConfig.java12
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitConfig.java284
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java48
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java90
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java9
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/DefaultTypedConfigGetter.java20
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSignatureVerifierFactory.java49
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgSigner.java44
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java3
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectDatabase.java10
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectReader.java4
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/PersonIdent.java57
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/TypedConfigGetter.java33
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java46
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java99
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommit.java1
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/revplot/PlotCommitList.java4
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ObjectWalk.java10
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java15
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteGenerator.java69
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/revwalk/StartGenerator.java6
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TreeRevFilter.java3
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/storage/file/FileBasedConfig.java77
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/AmazonS3.java92
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/AwsRequestSignerV4.java313
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java11
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java15
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java24
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java50
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/PushConfig.java104
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java207
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java155
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteConfig.java50
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java122
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandInputStream.java23
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java35
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/SshSessionFactory.java44
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/TrackingRefUpdate.java10
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java39
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java36
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java52
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/UrlConfig.java120
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/resolver/FileResolver.java14
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java205
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java45
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/util/Equality.java36
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java31
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32.java4
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java94
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java27
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/util/StringUtils.java23
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/util/SystemReader.java17
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/util/io/BinaryHunkInputStream.java8
-rw-r--r--pom.xml64
-rw-r--r--tools/BUILD43
-rw-r--r--tools/bazelisk_version.bzl16
-rw-r--r--tools/remote-bazelrc67
295 files changed, 16694 insertions, 2412 deletions
diff --git a/.bazelrc b/.bazelrc
index e24be88639..8c32661215 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -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
diff --git a/.mailmap b/.mailmap
index f0bc990d8d..f0af49b4f4 100644
--- a/.mailmap
+++ b/.mailmap
@@ -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
diff --git a/WORKSPACE b/WORKSPACE
index f78a159fb2..cce13164ef 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -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",
)
diff --git a/lib/BUILD b/lib/BUILD
index 00c91e3bae..6be9e575fe 100644
--- a/lib/BUILD
+++ b/lib/BUILD
@@ -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/&lt;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.&lt;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: &lt;YourAWSAccessKey&gt;
* secretkey: &lt;YourAWSSecretKey&gt;
@@ -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,
diff --git a/pom.xml b/pom.xml
index d8ec00d22b..c71837c0e5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -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