summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.buckconfig15
-rw-r--r--.buckversion1
-rw-r--r--.gitignore2
-rw-r--r--BUCK45
-rw-r--r--lib/BUCK125
-rw-r--r--lib/jetty/BUCK56
-rw-r--r--org.eclipse.jgit.archive/BUCK13
-rw-r--r--org.eclipse.jgit.http.apache/BUCK12
-rw-r--r--org.eclipse.jgit.http.apache/META-INF/MANIFEST.MF3
-rw-r--r--org.eclipse.jgit.http.apache/src/org/eclipse/jgit/transport/http/apache/HttpClientConnection.java27
-rw-r--r--org.eclipse.jgit.http.server/BUCK10
-rw-r--r--org.eclipse.jgit.http.test/BUCK40
-rw-r--r--org.eclipse.jgit.http.test/pom.xml4
-rw-r--r--org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/DumbClientDumbServerTest.java25
-rw-r--r--org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/GitServletResponseTests.java6
-rw-r--r--org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/HttpClientTests.java125
-rw-r--r--org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java58
-rw-r--r--org.eclipse.jgit.junit.http/BUCK18
-rw-r--r--org.eclipse.jgit.junit/BUCK10
-rw-r--r--org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java9
-rw-r--r--org.eclipse.jgit.pgm.test/BUCK38
-rw-r--r--org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF1
-rw-r--r--org.eclipse.jgit.pgm.test/org.eclipse.jgit.pgm--All-Tests (Java8) (de).launch30
-rw-r--r--org.eclipse.jgit.pgm.test/src/org/eclipse/jgit/lib/CLIRepositoryTestCase.java83
-rw-r--r--org.eclipse.jgit.pgm.test/src/org/eclipse/jgit/pgm/CLIGitCommand.java177
-rw-r--r--org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/AddTest.java15
-rw-r--r--org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ArchiveTest.java95
-rw-r--r--org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/BranchTest.java196
-rw-r--r--org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CheckoutTest.java41
-rw-r--r--org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CommitTest.java100
-rw-r--r--org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DescribeTest.java34
-rw-r--r--org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeTest.java10
-rw-r--r--org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/RepoTest.java28
-rw-r--r--org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ResetTest.java39
-rw-r--r--org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/StatusTest.java8
-rw-r--r--org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/TagTest.java2
-rw-r--r--org.eclipse.jgit.pgm/BUCK70
-rw-r--r--org.eclipse.jgit.pgm/META-INF/MANIFEST.MF3
-rw-r--r--org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin1
-rw-r--r--org.eclipse.jgit.pgm/pom.xml11
-rw-r--r--org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties8
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Branch.java130
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Checkout.java7
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Clone.java21
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Daemon.java47
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Die.java15
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Main.java131
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Merge.java9
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Remote.java3
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Repo.java2
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Reset.java10
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/RevParse.java8
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Status.java6
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/TextBuiltin.java83
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/RebuildRefTree.java168
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java15
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/opt/CmdLineParser.java163
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/opt/OptionWithValuesListHandler.java52
-rw-r--r--org.eclipse.jgit.test/BUCK95
-rw-r--r--org.eclipse.jgit.test/META-INF/MANIFEST.MF1
-rw-r--r--org.eclipse.jgit.test/org.eclipse.jgit.core--All-Tests (Java 8) (de).launch31
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java98
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java31
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PathCheckoutCommandTest.java65
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java56
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeWorkingTreeIteratorTest.java3
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCachePathEditTest.java121
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java7
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreNodeTest.java3
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileRepositoryBuilderTest.java2
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java70
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java53
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/LocalDiskRefTreeDatabaseTest.java126
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/RefTreeDatabaseTest.java718
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/RefTreeTest.java303
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutTest.java6
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexDiffTest.java91
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectCheckerTest.java1465
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/T0002_TreeTest.java319
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/ObjectWalkTest.java45
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevCommitParseTest.java85
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevTagParseTest.java39
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/AtomicPushTest.java15
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BundleWriterTest.java6
-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.java50
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportTest.java73
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/URIishTest.java42
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/TreeWalkBasicDiffTest.java77
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/PathFilterGroupTest.java150
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/util/ChangeIdUtilTest.java3
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FileUtilTest.java26
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/util/PathsTest.java118
-rw-r--r--org.eclipse.jgit.ui/BUCK7
-rw-r--r--org.eclipse.jgit.ui/src/org/eclipse/jgit/awtui/AwtCredentialsProvider.java7
-rw-r--r--org.eclipse.jgit/.settings/.api_filters40
-rw-r--r--org.eclipse.jgit/BUCK20
-rw-r--r--org.eclipse.jgit/META-INF/MANIFEST.MF4
-rw-r--r--org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties11
-rw-r--r--org.eclipse.jgit/resources/org/eclipse/jgit/internal/ketch/KetchText.properties13
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java130
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java9
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java13
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java13
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java44
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java27
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/LsRemoteCommand.java42
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java25
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java2
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java21
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java4
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/StashDropCommand.java11
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleUpdateCommand.java3
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/TransportCommand.java1
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/errors/EmtpyCommitException.java62
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/dircache/BaseDirCacheEditor.java76
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java7
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheBuildIterator.java5
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java10
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEditor.java180
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java19
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/errors/CorruptObjectException.java41
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/errors/DirCacheNameConflictException.java (renamed from org.eclipse.jgit/src/org/eclipse/jgit/lib/GitlinkTreeEntry.java)55
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java32
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java23
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java11
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ElectionRound.java141
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchConstants.java82
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchLeader.java624
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchLeaderCache.java126
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchPreReceive.java155
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchReplica.java755
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchSystem.java301
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchText.java70
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LagCheck.java183
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LeaderSnapshot.java164
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LocalReplica.java229
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LogIndex.java119
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Proposal.java391
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ProposalRound.java299
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/RemoteGitReplica.java316
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaConfig.java233
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaFetchRequest.java96
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaPushRequest.java168
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaSnapshot.java109
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Round.java116
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/StageBuilder.java270
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/package-info.java4
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollector.java73
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjDatabase.java7
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackCompactor.java15
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRefDatabase.java5
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRepository.java10
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/InMemoryRepository.java37
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java23
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java145
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LazyObjectIdSetFile.java106
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java70
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java19
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryPackParser.java14
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndex.java9
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java49
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectoryRename.java23
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriter.java52
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/AlwaysFailUpdate.java98
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/Command.java316
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTree.java411
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeBatch.java222
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeDatabase.java372
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeNames.java89
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeRename.java121
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeUpdate.java176
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/Scanner.java286
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java3
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/BlobBasedConfig.java10
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/FileTreeEntry.java115
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java685
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdOwnerMap.java4
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdRef.java36
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdSet.java (renamed from org.eclipse.jgit/src/org/eclipse/jgit/lib/SymlinkTreeEntry.java)47
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdSubclassMap.java3
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/Ref.java17
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java4
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/RefWriter.java26
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java12
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/SymbolicRef.java11
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/Tree.java601
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/TreeEntry.java256
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java23
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/merge/Merger.java26
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/notes/NonNoteEntry.java26
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommit.java65
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java39
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java4
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java8
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java10
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java84
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/BundleFetchConnection.java27
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/ChainingCredentialsProvider.java17
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/Connection.java7
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/CredentialsProvider.java14
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/Daemon.java10
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java15
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/JschSession.java15
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRCCredentialsProvider.java3
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/PackParser.java5
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/ProgressSpinner.java149
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java9
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java22
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java4
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandInputStream.java19
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java129
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java6
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java2
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkFetchConnection.java11
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java30
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/treewalk/EmptyTreeIterator.java5
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/treewalk/NameConflictTreeWalk.java37
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java48
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java27
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/PathFilterGroup.java6
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/util/ChangeIdUtil.java8
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java4
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/util/Paths.java188
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/util/RawParseUtils.java90
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/util/io/StreamCopyThread.java22
-rw-r--r--tools/default.defs42
-rw-r--r--tools/git.defs9
228 files changed, 14729 insertions, 3908 deletions
diff --git a/.buckconfig b/.buckconfig
new file mode 100644
index 0000000000..b2e07acccf
--- /dev/null
+++ b/.buckconfig
@@ -0,0 +1,15 @@
+[buildfile]
+ includes = //tools/default.defs
+
+[java]
+ src_roots = src, resources, tst
+
+[project]
+ ignore = .git
+
+[cache]
+ mode = dir
+
+[download]
+ maven_repo = http://repo1.maven.org/maven2
+ in_build = true
diff --git a/.buckversion b/.buckversion
new file mode 100644
index 0000000000..9daac2cea3
--- /dev/null
+++ b/.buckversion
@@ -0,0 +1 @@
+1b03b4313b91b634bd604fc3487a05f877e59dee
diff --git a/.gitignore b/.gitignore
index 139e5aee6d..6c62199484 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
/target
/.project
+/buck-cache
+/buck-out
diff --git a/BUCK b/BUCK
new file mode 100644
index 0000000000..f19b7bdc5d
--- /dev/null
+++ b/BUCK
@@ -0,0 +1,45 @@
+java_library(
+ name = 'jgit',
+ exported_deps = ['//org.eclipse.jgit:jgit'],
+ visibility = ['PUBLIC'],
+)
+
+genrule(
+ name = 'jgit_src',
+ cmd = 'ln -s $(location //org.eclipse.jgit:jgit_src) $OUT',
+ out = 'jgit_src.zip',
+ visibility = ['PUBLIC'],
+)
+
+java_library(
+ name = 'jgit-servlet',
+ exported_deps = [
+ ':jgit',
+ '//org.eclipse.jgit.http.server:jgit-servlet'
+ ],
+ visibility = ['PUBLIC'],
+)
+
+java_library(
+ name = 'jgit-archive',
+ exported_deps = [
+ ':jgit',
+ '//org.eclipse.jgit.archive:jgit-archive'
+ ],
+ visibility = ['PUBLIC'],
+)
+
+java_library(
+ name = 'junit',
+ exported_deps = [
+ ':jgit',
+ '//org.eclipse.jgit.junit:junit'
+ ],
+ visibility = ['PUBLIC'],
+)
+
+genrule(
+ name = 'jgit_bin',
+ cmd = 'ln -s $(location //org.eclipse.jgit.pgm:jgit) $OUT',
+ out = 'jgit_bin',
+)
diff --git a/lib/BUCK b/lib/BUCK
new file mode 100644
index 0000000000..524612bde6
--- /dev/null
+++ b/lib/BUCK
@@ -0,0 +1,125 @@
+maven_jar(
+ name = 'jsch',
+ bin_sha1 = '658b682d5c817b27ae795637dfec047c63d29935',
+ src_sha1 = '791359d94d6edcace686a56d0727ee093a2f7c33',
+ group = 'com.jcraft',
+ artifact = 'jsch',
+ version = '0.1.53',
+)
+
+maven_jar(
+ name = 'javaewah',
+ bin_sha1 = 'eceaf316a8faf0e794296ebe158ae110c7d72a5a',
+ src_sha1 = 'a50d78eb630e05439461f3130b94b3bcd1ea6f03',
+ group = 'com.googlecode.javaewah',
+ artifact = 'JavaEWAH',
+ version = '0.7.9',
+)
+
+maven_jar(
+ name = 'httpcomponents',
+ bin_sha1 = '4c47155e3e6c9a41a28db36680b828ced53b8af4',
+ src_sha1 = 'af4d76be0c46ee26b0d9d1d4a34d244a633cac84',
+ group = 'org.apache.httpcomponents',
+ artifact = 'httpclient',
+ version = '4.3.6',
+)
+
+maven_jar(
+ name = 'httpcore',
+ bin_sha1 = 'f91b7a4aadc5cf486df6e4634748d7dd7a73f06d',
+ src_sha1 = '1b0aa62a6a91e9fa00c16f0a4a2c874804ed3b1e',
+ group = 'org.apache.httpcomponents',
+ artifact = 'httpcore',
+ version = '4.3.3',
+)
+
+maven_jar(
+ name = 'commons-logging',
+ bin_sha1 = 'f6f66e966c70a83ffbdb6f17a0919eaf7c8aca7f',
+ src_sha1 = '28bb0405fddaf04f15058fbfbe01fe2780d7d3b6',
+ group = 'commons-logging',
+ artifact = 'commons-logging',
+ version = '1.1.3',
+)
+
+maven_jar(
+ name = 'slf4j-api',
+ bin_sha1 = '0081d61b7f33ebeab314e07de0cc596f8e858d97',
+ src_sha1 = '58d38f68d4a867d4552ae27960bb348d7eaa1297',
+ group = 'org.slf4j',
+ artifact = 'slf4j-api',
+ version = '1.7.2',
+)
+
+maven_jar(
+ name = 'slf4j-simple',
+ bin_sha1 = '760055906d7353ba4f7ce1b8908bc6b2e91f39fa',
+ src_sha1 = '09474919128b3a7fcf21a5f9c907f5251f234544',
+ group = 'org.slf4j',
+ artifact = 'slf4j-simple',
+ version = '1.7.2',
+)
+
+maven_jar(
+ name = 'servlet-api',
+ bin_sha1 = '3cd63d075497751784b2fa84be59432f4905bf7c',
+ src_sha1 = 'ab3976d4574c48d22dc1abf6a9e8bd0fdf928223',
+ group = 'javax.servlet',
+ artifact = 'javax.servlet-api',
+ version = '3.1.0',
+)
+
+maven_jar(
+ name = 'commons-compress',
+ bin_sha1 = 'c7d9b580aff9e9f1998361f16578e63e5c064699',
+ src_sha1 = '396b81bdfd0fb617178e1707ef64832215307c78',
+ group = 'org.apache.commons',
+ artifact = 'commons-compress',
+ version = '1.6',
+)
+
+maven_jar(
+ name = 'tukaani-xz',
+ bin_sha1 = '66db21c8484120cb6a51b5b3ea47b6f383942bec',
+ src_sha1 = '6396220725701d767c553902c41120d7bf38e9f5',
+ group = 'org.tukaani',
+ artifact = 'xz',
+ version = '1.3',
+)
+
+maven_jar(
+ name = 'args4j',
+ bin_sha1 = '139441471327b9cc6d56436cb2a31e60eb6ed2ba',
+ src_sha1 = '22631b78cc8f60a6918557e8cbdb33e90f63a77f',
+ group = 'args4j',
+ artifact = 'args4j',
+ version = '2.0.15',
+)
+
+maven_jar(
+ name = 'junit',
+ bin_sha1 = '4e031bb61df09069aeb2bffb4019e7a5034a4ee0',
+ src_sha1 = '28e0ad201304e4a4abf999ca0570b7cffc352c3c',
+ group = 'junit',
+ artifact = 'junit',
+ version = '4.11',
+)
+
+maven_jar(
+ name = 'hamcrest-library',
+ bin_sha1 = '4785a3c21320980282f9f33d0d1264a69040538f',
+ src_sha1 = '047a7ee46628ab7133129cd7cef1e92657bc275e',
+ group = 'org.hamcrest',
+ artifact = 'hamcrest-library',
+ version = '1.3',
+)
+
+maven_jar(
+ name = 'hamcrest-core',
+ bin_sha1 = '42a25dc3219429f0e5d060061f71acb49bf010a0',
+ src_sha1 = '1dc37250fbc78e23a65a67fbbaf71d2e9cbc3c0b',
+ group = 'org.hamcrest',
+ artifact = 'hamcrest-core',
+ version = '1.3',
+)
diff --git a/lib/jetty/BUCK b/lib/jetty/BUCK
new file mode 100644
index 0000000000..6e7dec3062
--- /dev/null
+++ b/lib/jetty/BUCK
@@ -0,0 +1,56 @@
+VERSION = '9.2.13.v20150730'
+GROUP = 'org.eclipse.jetty'
+
+maven_jar(
+ name = 'servlet',
+ bin_sha1 = '5ad6e38015a97ae9a60b6c2ad744ccfa9cf93a50',
+ src_sha1 = '78fbec19321150552d91f9e079c2f2ca33222b01',
+ group = GROUP,
+ artifact = 'jetty-servlet',
+ version = VERSION,
+)
+
+maven_jar(
+ name = 'security',
+ bin_sha1 = 'cc7c7f27ec4cc279253be1675d9e47e58b995943',
+ src_sha1 = '75632ebdf8bd651faafb97106c92496db59e165d',
+ group = GROUP,
+ artifact = 'jetty-security',
+ version = VERSION,
+)
+
+maven_jar(
+ name = 'server',
+ bin_sha1 = '5be7d1da0a7abffd142de3091d160717c120b6ab',
+ src_sha1 = '203e123f83efe2a5b8a9c74854c7897fe3563302',
+ group = GROUP,
+ artifact = 'jetty-server',
+ version = VERSION,
+)
+
+maven_jar(
+ name = 'http',
+ bin_sha1 = '23a745d9177ef67ef53cc46b9b70c5870082efc2',
+ src_sha1 = '5f87f7ff2057cd4b0995bc4fffe17b2aff64c130',
+ group = GROUP,
+ artifact = 'jetty-http',
+ version = VERSION,
+)
+
+maven_jar(
+ name = 'io',
+ bin_sha1 = '7a351e6a1b63dfd56b6632623f7ca2793ffb67ad',
+ src_sha1 = 'bbd61a84b748fc295456e1c5c3070aaf40a68f62',
+ group = GROUP,
+ artifact = 'jetty-io',
+ version = VERSION,
+)
+
+maven_jar(
+ name = 'util',
+ bin_sha1 = 'c101476360a7cdd0670462de04053507d5e70c97',
+ src_sha1 = '15ceecce141971b4e0facb861b3d10120ad6ce03',
+ group = GROUP,
+ artifact = 'jetty-util',
+ version = VERSION,
+)
diff --git a/org.eclipse.jgit.archive/BUCK b/org.eclipse.jgit.archive/BUCK
new file mode 100644
index 0000000000..ae170324e3
--- /dev/null
+++ b/org.eclipse.jgit.archive/BUCK
@@ -0,0 +1,13 @@
+java_library(
+ name = 'jgit-archive',
+ srcs = glob(
+ ['src/**'],
+ excludes = ['src/org/eclipse/jgit/archive/FormatActivator.java'],
+ ),
+ resources = glob(['resources/**']),
+ provided_deps = [
+ '//org.eclipse.jgit:jgit',
+ '//lib:commons-compress',
+ ],
+ visibility = ['PUBLIC'],
+)
diff --git a/org.eclipse.jgit.http.apache/BUCK b/org.eclipse.jgit.http.apache/BUCK
new file mode 100644
index 0000000000..f48f33a1ae
--- /dev/null
+++ b/org.eclipse.jgit.http.apache/BUCK
@@ -0,0 +1,12 @@
+java_library(
+ name = 'http-apache',
+ srcs = glob(['src/**']),
+ resources = glob(['resources/**']),
+ deps = [
+ '//org.eclipse.jgit:jgit',
+ '//lib:commons-logging',
+ '//lib:httpcomponents',
+ '//lib:httpcore',
+ ],
+ visibility = ['PUBLIC'],
+)
diff --git a/org.eclipse.jgit.http.apache/META-INF/MANIFEST.MF b/org.eclipse.jgit.http.apache/META-INF/MANIFEST.MF
index 07f364c535..8058f309f9 100644
--- a/org.eclipse.jgit.http.apache/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.http.apache/META-INF/MANIFEST.MF
@@ -7,7 +7,8 @@ Bundle-RequiredExecutionEnvironment: JavaSE-1.7
Bundle-Localization: plugin
Bundle-Vendor: %Provider-Name
Bundle-ActivationPolicy: lazy
-Import-Package: org.apache.http;version="[4.1.0,5.0.0)",
+Import-Package: org.apache.commons.logging;version="[1.1.1,2.0.0)",
+ org.apache.http;version="[4.1.0,5.0.0)",
org.apache.http.client;version="[4.1.0,5.0.0)",
org.apache.http.client.methods;version="[4.1.0,5.0.0)",
org.apache.http.client.params;version="[4.1.0,5.0.0)",
diff --git a/org.eclipse.jgit.http.apache/src/org/eclipse/jgit/transport/http/apache/HttpClientConnection.java b/org.eclipse.jgit.http.apache/src/org/eclipse/jgit/transport/http/apache/HttpClientConnection.java
index d42d6f29ee..de81bf82bf 100644
--- a/org.eclipse.jgit.http.apache/src/org/eclipse/jgit/transport/http/apache/HttpClientConnection.java
+++ b/org.eclipse.jgit.http.apache/src/org/eclipse/jgit/transport/http/apache/HttpClientConnection.java
@@ -100,7 +100,7 @@ import org.eclipse.jgit.util.TemporaryBuffer.LocalFile;
public class HttpClientConnection implements HttpConnection {
HttpClient client;
- String urlStr;
+ URL url;
HttpUriRequest req;
@@ -176,16 +176,19 @@ public class HttpClientConnection implements HttpConnection {
/**
* @param urlStr
+ * @throws MalformedURLException
*/
- public HttpClientConnection(String urlStr) {
+ public HttpClientConnection(String urlStr) throws MalformedURLException {
this(urlStr, null);
}
/**
* @param urlStr
* @param proxy
+ * @throws MalformedURLException
*/
- public HttpClientConnection(String urlStr, Proxy proxy) {
+ public HttpClientConnection(String urlStr, Proxy proxy)
+ throws MalformedURLException {
this(urlStr, proxy, null);
}
@@ -193,10 +196,12 @@ public class HttpClientConnection implements HttpConnection {
* @param urlStr
* @param proxy
* @param cl
+ * @throws MalformedURLException
*/
- public HttpClientConnection(String urlStr, Proxy proxy, HttpClient cl) {
+ public HttpClientConnection(String urlStr, Proxy proxy, HttpClient cl)
+ throws MalformedURLException {
this.client = cl;
- this.urlStr = urlStr;
+ this.url = new URL(urlStr);
this.proxy = proxy;
}
@@ -206,11 +211,7 @@ public class HttpClientConnection implements HttpConnection {
}
public URL getURL() {
- try {
- return new URL(urlStr);
- } catch (MalformedURLException e) {
- return null;
- }
+ return url;
}
public String getResponseMessage() throws IOException {
@@ -250,11 +251,11 @@ public class HttpClientConnection implements HttpConnection {
public void setRequestMethod(String method) throws ProtocolException {
this.method = method;
if ("GET".equalsIgnoreCase(method)) //$NON-NLS-1$
- req = new HttpGet(urlStr);
+ req = new HttpGet(url.toString());
else if ("PUT".equalsIgnoreCase(method)) //$NON-NLS-1$
- req = new HttpPut(urlStr);
+ req = new HttpPut(url.toString());
else if ("POST".equalsIgnoreCase(method)) //$NON-NLS-1$
- req = new HttpPost(urlStr);
+ req = new HttpPost(url.toString());
else {
this.method = null;
throw new UnsupportedOperationException();
diff --git a/org.eclipse.jgit.http.server/BUCK b/org.eclipse.jgit.http.server/BUCK
new file mode 100644
index 0000000000..3743557aa5
--- /dev/null
+++ b/org.eclipse.jgit.http.server/BUCK
@@ -0,0 +1,10 @@
+java_library(
+ name = 'jgit-servlet',
+ srcs = glob(['src/**']),
+ resources = glob(['resources/**']),
+ provided_deps = [
+ '//org.eclipse.jgit:jgit',
+ '//lib:servlet-api',
+ ],
+ visibility = ['PUBLIC'],
+)
diff --git a/org.eclipse.jgit.http.test/BUCK b/org.eclipse.jgit.http.test/BUCK
new file mode 100644
index 0000000000..d2ced7a247
--- /dev/null
+++ b/org.eclipse.jgit.http.test/BUCK
@@ -0,0 +1,40 @@
+TESTS = glob(['tst/**/*.java'])
+
+for t in TESTS:
+ n = t[len('tst/'):len(t)-len('.java')].replace('/', '.')
+ java_test(
+ name = n,
+ labels = ['http'],
+ srcs = [t],
+ deps = [
+ ':helpers',
+ '//org.eclipse.jgit:jgit',
+ '//org.eclipse.jgit.http.apache:http-apache',
+ '//org.eclipse.jgit.http.server:jgit-servlet',
+ '//org.eclipse.jgit.junit:junit',
+ '//org.eclipse.jgit.junit.http:junit-http',
+ '//lib:hamcrest-core',
+ '//lib:hamcrest-library',
+ '//lib:junit',
+ '//lib:servlet-api',
+ '//lib/jetty:http',
+ '//lib/jetty:io',
+ '//lib/jetty:server',
+ '//lib/jetty:servlet',
+ '//lib/jetty:security',
+ '//lib/jetty:util',
+ ],
+ source_under_test = ['//org.eclipse.jgit.http.server:jgit-servlet'],
+ )
+
+java_library(
+ name = 'helpers',
+ srcs = glob(['src/**/*.java']),
+ deps = [
+ '//org.eclipse.jgit:jgit',
+ '//org.eclipse.jgit.http.server:jgit-servlet',
+ '//org.eclipse.jgit.junit:junit',
+ '//org.eclipse.jgit.junit.http:junit-http',
+ '//lib:junit',
+ ],
+)
diff --git a/org.eclipse.jgit.http.test/pom.xml b/org.eclipse.jgit.http.test/pom.xml
index dd52a89e6c..0af30f659d 100644
--- a/org.eclipse.jgit.http.test/pom.xml
+++ b/org.eclipse.jgit.http.test/pom.xml
@@ -134,6 +134,10 @@
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>-Djava.io.tmpdir=${project.build.directory} -Xmx300m</argLine>
+ <includes>
+ <include>**/*Test.java</include>
+ <include>**/*Tests.java</include>
+ </includes>
</configuration>
</plugin>
</plugins>
diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/DumbClientDumbServerTest.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/DumbClientDumbServerTest.java
index 362a09d64f..677132d732 100644
--- a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/DumbClientDumbServerTest.java
+++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/DumbClientDumbServerTest.java
@@ -140,8 +140,7 @@ public class DumbClientDumbServerTest extends HttpTestCase {
assertEquals("http", remoteURI.getScheme());
Map<String, Ref> map;
- Transport t = Transport.open(dst, remoteURI);
- try {
+ try (Transport t = Transport.open(dst, remoteURI)) {
// I didn't make up these public interface names, I just
// approved them for inclusion into the code base. Sorry.
// --spearce
@@ -149,14 +148,9 @@ public class DumbClientDumbServerTest extends HttpTestCase {
assertTrue("isa TransportHttp", t instanceof TransportHttp);
assertTrue("isa HttpTransport", t instanceof HttpTransport);
- FetchConnection c = t.openFetch();
- try {
+ try (FetchConnection c = t.openFetch()) {
map = c.getRefsMap();
- } finally {
- c.close();
}
- } finally {
- t.close();
}
assertNotNull("have map of refs", map);
@@ -201,11 +195,8 @@ public class DumbClientDumbServerTest extends HttpTestCase {
Repository dst = createBareRepository();
assertFalse(dst.hasObject(A_txt));
- Transport t = Transport.open(dst, remoteURI);
- try {
+ try (Transport t = Transport.open(dst, remoteURI)) {
t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
- } finally {
- t.close();
}
assertTrue(dst.hasObject(A_txt));
@@ -226,11 +217,8 @@ public class DumbClientDumbServerTest extends HttpTestCase {
Repository dst = createBareRepository();
assertFalse(dst.hasObject(A_txt));
- Transport t = Transport.open(dst, remoteURI);
- try {
+ try (Transport t = Transport.open(dst, remoteURI)) {
t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
- } finally {
- t.close();
}
assertTrue(dst.hasObject(A_txt));
@@ -265,8 +253,7 @@ public class DumbClientDumbServerTest extends HttpTestCase {
final RevCommit Q = src.commit().create();
final Repository db = src.getRepository();
- Transport t = Transport.open(db, remoteURI);
- try {
+ try (Transport t = Transport.open(db, remoteURI)) {
try {
t.push(NullProgressMonitor.INSTANCE, push(src, Q));
fail("push incorrectly completed against a dumb server");
@@ -274,8 +261,6 @@ public class DumbClientDumbServerTest extends HttpTestCase {
String exp = "remote does not support smart HTTP push";
assertEquals(exp, nse.getMessage());
}
- } finally {
- t.close();
}
}
}
diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/GitServletResponseTests.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/GitServletResponseTests.java
index fba1a52640..4b15d4b533 100644
--- a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/GitServletResponseTests.java
+++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/GitServletResponseTests.java
@@ -60,6 +60,7 @@ import org.eclipse.jgit.http.server.GitServlet;
import org.eclipse.jgit.http.server.resolver.DefaultReceivePackFactory;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.junit.http.HttpTestCase;
+import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.ObjectChecker;
@@ -221,8 +222,9 @@ public class GitServletResponseTests extends HttpTestCase {
preHook = null;
oc = new ObjectChecker() {
@Override
- public void checkCommit(byte[] raw) throws CorruptObjectException {
- throw new IllegalStateException();
+ public void checkCommit(AnyObjectId id, byte[] raw)
+ throws CorruptObjectException {
+ throw new CorruptObjectException("refusing all commits");
}
};
diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/HttpClientTests.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/HttpClientTests.java
index 6fb130231a..ce78442785 100644
--- a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/HttpClientTests.java
+++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/HttpClientTests.java
@@ -157,8 +157,7 @@ public class HttpClientTests extends HttpTestCase {
public void testRepositoryNotFound_Dumb() throws Exception {
URIish uri = toURIish("/dumb.none/not-found");
Repository dst = createBareRepository();
- Transport t = Transport.open(dst, uri);
- try {
+ try (Transport t = Transport.open(dst, uri)) {
try {
t.openFetch();
fail("connection opened to not found repository");
@@ -167,8 +166,6 @@ public class HttpClientTests extends HttpTestCase {
+ "/info/refs?service=git-upload-pack not found";
assertEquals(exp, err.getMessage());
}
- } finally {
- t.close();
}
}
@@ -176,8 +173,7 @@ public class HttpClientTests extends HttpTestCase {
public void testRepositoryNotFound_Smart() throws Exception {
URIish uri = toURIish("/smart.none/not-found");
Repository dst = createBareRepository();
- Transport t = Transport.open(dst, uri);
- try {
+ try (Transport t = Transport.open(dst, uri)) {
try {
t.openFetch();
fail("connection opened to not found repository");
@@ -186,8 +182,6 @@ public class HttpClientTests extends HttpTestCase {
+ "/info/refs?service=git-upload-pack not found";
assertEquals(exp, err.getMessage());
}
- } finally {
- t.close();
}
}
@@ -201,16 +195,9 @@ public class HttpClientTests extends HttpTestCase {
Repository dst = createBareRepository();
Ref head;
- Transport t = Transport.open(dst, dumbAuthNoneURI);
- try {
- FetchConnection c = t.openFetch();
- try {
- head = c.getRef(Constants.HEAD);
- } finally {
- c.close();
- }
- } finally {
- t.close();
+ try (Transport t = Transport.open(dst, dumbAuthNoneURI);
+ FetchConnection c = t.openFetch()) {
+ head = c.getRef(Constants.HEAD);
}
assertNotNull("has " + Constants.HEAD, head);
assertEquals(Q, head.getObjectId());
@@ -225,16 +212,9 @@ public class HttpClientTests extends HttpTestCase {
Repository dst = createBareRepository();
Ref head;
- Transport t = Transport.open(dst, dumbAuthNoneURI);
- try {
- FetchConnection c = t.openFetch();
- try {
- head = c.getRef(Constants.HEAD);
- } finally {
- c.close();
- }
- } finally {
- t.close();
+ try (Transport t = Transport.open(dst, dumbAuthNoneURI);
+ FetchConnection c = t.openFetch()) {
+ head = c.getRef(Constants.HEAD);
}
assertNull("has no " + Constants.HEAD, head);
}
@@ -249,16 +229,9 @@ public class HttpClientTests extends HttpTestCase {
Repository dst = createBareRepository();
Ref head;
- Transport t = Transport.open(dst, smartAuthNoneURI);
- try {
- FetchConnection c = t.openFetch();
- try {
- head = c.getRef(Constants.HEAD);
- } finally {
- c.close();
- }
- } finally {
- t.close();
+ try (Transport t = Transport.open(dst, smartAuthNoneURI);
+ FetchConnection c = t.openFetch()) {
+ head = c.getRef(Constants.HEAD);
}
assertNotNull("has " + Constants.HEAD, head);
assertEquals(Q, head.getObjectId());
@@ -268,16 +241,13 @@ public class HttpClientTests extends HttpTestCase {
public void testListRemote_Smart_WithQueryParameters() throws Exception {
URIish myURI = toURIish("/snone/do?r=1&p=test.git");
Repository dst = createBareRepository();
- Transport t = Transport.open(dst, myURI);
- try {
+ try (Transport t = Transport.open(dst, myURI)) {
try {
t.openFetch();
fail("test did not fail to find repository as expected");
} catch (NoRemoteRepositoryException err) {
// expected
}
- } finally {
- t.close();
}
List<AccessEvent> requests = getRequests();
@@ -296,62 +266,52 @@ public class HttpClientTests extends HttpTestCase {
@Test
public void testListRemote_Dumb_NeedsAuth() throws Exception {
Repository dst = createBareRepository();
- Transport t = Transport.open(dst, dumbAuthBasicURI);
- try {
+ try (Transport t = Transport.open(dst, dumbAuthBasicURI)) {
try {
t.openFetch();
fail("connection opened even info/refs needs auth basic");
} catch (TransportException err) {
String exp = dumbAuthBasicURI + ": "
- + JGitText.get().notAuthorized;
+ + JGitText.get().noCredentialsProvider;
assertEquals(exp, err.getMessage());
}
- } finally {
- t.close();
}
}
@Test
public void testListRemote_Dumb_Auth() throws Exception {
Repository dst = createBareRepository();
- Transport t = Transport.open(dst, dumbAuthBasicURI);
- t.setCredentialsProvider(new UsernamePasswordCredentialsProvider(
- AppServer.username, AppServer.password));
- try {
- t.openFetch();
- } finally {
- t.close();
+ try (Transport t = Transport.open(dst, dumbAuthBasicURI)) {
+ t.setCredentialsProvider(new UsernamePasswordCredentialsProvider(
+ AppServer.username, AppServer.password));
+ t.openFetch().close();
}
- t = Transport.open(dst, dumbAuthBasicURI);
- t.setCredentialsProvider(new UsernamePasswordCredentialsProvider(
- AppServer.username, ""));
- try {
- t.openFetch();
- fail("connection opened even info/refs needs auth basic and we provide wrong password");
- } catch (TransportException err) {
- String exp = dumbAuthBasicURI + ": "
- + JGitText.get().notAuthorized;
- assertEquals(exp, err.getMessage());
- } finally {
- t.close();
+ try (Transport t = Transport.open(dst, dumbAuthBasicURI)) {
+ t.setCredentialsProvider(new UsernamePasswordCredentialsProvider(
+ AppServer.username, ""));
+ try {
+ t.openFetch();
+ fail("connection opened even info/refs needs auth basic and we provide wrong password");
+ } catch (TransportException err) {
+ String exp = dumbAuthBasicURI + ": "
+ + JGitText.get().notAuthorized;
+ assertEquals(exp, err.getMessage());
+ }
}
}
@Test
public void testListRemote_Smart_UploadPackNeedsAuth() throws Exception {
Repository dst = createBareRepository();
- Transport t = Transport.open(dst, smartAuthBasicURI);
- try {
+ try (Transport t = Transport.open(dst, smartAuthBasicURI)) {
try {
t.openFetch();
fail("connection opened even though service disabled");
} catch (TransportException err) {
String exp = smartAuthBasicURI + ": "
- + JGitText.get().notAuthorized;
+ + JGitText.get().noCredentialsProvider;
assertEquals(exp, err.getMessage());
}
- } finally {
- t.close();
}
}
@@ -363,33 +323,24 @@ public class HttpClientTests extends HttpTestCase {
cfg.save();
Repository dst = createBareRepository();
- Transport t = Transport.open(dst, smartAuthNoneURI);
- try {
+ try (Transport t = Transport.open(dst, smartAuthNoneURI)) {
try {
t.openFetch();
fail("connection opened even though service disabled");
} catch (TransportException err) {
- String exp = smartAuthNoneURI + ": Git access forbidden";
+ String exp = smartAuthNoneURI + ": "
+ + JGitText.get().serviceNotEnabledNoName;
assertEquals(exp, err.getMessage());
}
- } finally {
- t.close();
}
}
@Test
public void testListRemoteWithoutLocalRepository() throws Exception {
- Transport t = Transport.open(smartAuthNoneURI);
- try {
- FetchConnection c = t.openFetch();
- try {
- Ref head = c.getRef(Constants.HEAD);
- assertNotNull(head);
- } finally {
- c.close();
- }
- } finally {
- t.close();
+ try (Transport t = Transport.open(smartAuthNoneURI);
+ FetchConnection c = t.openFetch()) {
+ Ref head = c.getRef(Constants.HEAD);
+ assertNotNull(head);
}
}
}
diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java
index 9ca0789e29..82861ed9b7 100644
--- a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java
+++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java
@@ -211,8 +211,7 @@ public class SmartClientSmartServerTest extends HttpTestCase {
assertEquals("http", remoteURI.getScheme());
Map<String, Ref> map;
- Transport t = Transport.open(dst, remoteURI);
- try {
+ try (Transport t = Transport.open(dst, remoteURI)) {
// I didn't make up these public interface names, I just
// approved them for inclusion into the code base. Sorry.
// --spearce
@@ -226,8 +225,6 @@ public class SmartClientSmartServerTest extends HttpTestCase {
} finally {
c.close();
}
- } finally {
- t.close();
}
assertNotNull("have map of refs", map);
@@ -257,8 +254,7 @@ public class SmartClientSmartServerTest extends HttpTestCase {
public void testListRemote_BadName() throws IOException, URISyntaxException {
Repository dst = createBareRepository();
URIish uri = new URIish(this.remoteURI.toString() + ".invalid");
- Transport t = Transport.open(dst, uri);
- try {
+ try (Transport t = Transport.open(dst, uri)) {
try {
t.openFetch();
fail("fetch connection opened");
@@ -266,8 +262,6 @@ public class SmartClientSmartServerTest extends HttpTestCase {
assertEquals(uri + ": Git repository not found",
notFound.getMessage());
}
- } finally {
- t.close();
}
List<AccessEvent> requests = getRequests();
@@ -288,11 +282,8 @@ public class SmartClientSmartServerTest extends HttpTestCase {
Repository dst = createBareRepository();
assertFalse(dst.hasObject(A_txt));
- Transport t = Transport.open(dst, remoteURI);
- try {
+ try (Transport t = Transport.open(dst, remoteURI)) {
t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
- } finally {
- t.close();
}
assertTrue(dst.hasObject(A_txt));
@@ -331,11 +322,8 @@ public class SmartClientSmartServerTest extends HttpTestCase {
// Bootstrap by doing the clone.
//
TestRepository dst = createTestRepository();
- Transport t = Transport.open(dst.getRepository(), remoteURI);
- try {
+ try (Transport t = Transport.open(dst.getRepository(), remoteURI)) {
t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
- } finally {
- t.close();
}
assertEquals(B, dst.getRepository().exactRef(master).getObjectId());
List<AccessEvent> cloneRequests = getRequests();
@@ -352,11 +340,8 @@ public class SmartClientSmartServerTest extends HttpTestCase {
// Now incrementally update.
//
- t = Transport.open(dst.getRepository(), remoteURI);
- try {
+ try (Transport t = Transport.open(dst.getRepository(), remoteURI)) {
t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
- } finally {
- t.close();
}
assertEquals(Z, dst.getRepository().exactRef(master).getObjectId());
@@ -394,11 +379,8 @@ public class SmartClientSmartServerTest extends HttpTestCase {
// Bootstrap by doing the clone.
//
TestRepository dst = createTestRepository();
- Transport t = Transport.open(dst.getRepository(), remoteURI);
- try {
+ try (Transport t = Transport.open(dst.getRepository(), remoteURI)) {
t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
- } finally {
- t.close();
}
assertEquals(B, dst.getRepository().exactRef(master).getObjectId());
List<AccessEvent> cloneRequests = getRequests();
@@ -418,11 +400,8 @@ public class SmartClientSmartServerTest extends HttpTestCase {
// Now incrementally update.
//
- t = Transport.open(dst.getRepository(), remoteURI);
- try {
+ try (Transport t = Transport.open(dst.getRepository(), remoteURI)) {
t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
- } finally {
- t.close();
}
assertEquals(Z, dst.getRepository().exactRef(master).getObjectId());
@@ -474,8 +453,7 @@ public class SmartClientSmartServerTest extends HttpTestCase {
Repository dst = createBareRepository();
assertFalse(dst.hasObject(A_txt));
- Transport t = Transport.open(dst, brokenURI);
- try {
+ try (Transport t = Transport.open(dst, brokenURI)) {
try {
t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
fail("fetch completed despite upload-pack being broken");
@@ -485,8 +463,6 @@ public class SmartClientSmartServerTest extends HttpTestCase {
+ " received Content-Type text/plain; charset=UTF-8";
assertEquals(exp, err.getMessage());
}
- } finally {
- t.close();
}
List<AccessEvent> requests = getRequests();
@@ -517,12 +493,10 @@ public class SmartClientSmartServerTest extends HttpTestCase {
final RevCommit Q = src.commit().add("Q", Q_txt).create();
final Repository db = src.getRepository();
final String dstName = Constants.R_HEADS + "new.branch";
- Transport t;
// push anonymous shouldn't be allowed.
//
- t = Transport.open(db, remoteURI);
- try {
+ try (Transport t = Transport.open(db, remoteURI)) {
final String srcExpr = Q.name();
final boolean forceUpdate = false;
final String localName = null;
@@ -538,8 +512,6 @@ public class SmartClientSmartServerTest extends HttpTestCase {
+ JGitText.get().authenticationNotSupported;
assertEquals(exp, e.getMessage());
}
- } finally {
- t.close();
}
List<AccessEvent> requests = getRequests();
@@ -560,12 +532,10 @@ public class SmartClientSmartServerTest extends HttpTestCase {
final RevCommit Q = src.commit().add("Q", Q_txt).create();
final Repository db = src.getRepository();
final String dstName = Constants.R_HEADS + "new.branch";
- Transport t;
enableReceivePack();
- t = Transport.open(db, remoteURI);
- try {
+ try (Transport t = Transport.open(db, remoteURI)) {
final String srcExpr = Q.name();
final boolean forceUpdate = false;
final String localName = null;
@@ -574,8 +544,6 @@ public class SmartClientSmartServerTest extends HttpTestCase {
RemoteRefUpdate u = new RemoteRefUpdate(src.getRepository(),
srcExpr, dstName, forceUpdate, localName, oldId);
t.push(NullProgressMonitor.INSTANCE, Collections.singleton(u));
- } finally {
- t.close();
}
assertTrue(remoteRepository.hasObject(Q_txt));
@@ -633,7 +601,6 @@ public class SmartClientSmartServerTest extends HttpTestCase {
final RevCommit Q = src.commit().add("Q", Q_bin).create();
final Repository db = src.getRepository();
final String dstName = Constants.R_HEADS + "new.branch";
- Transport t;
enableReceivePack();
@@ -642,8 +609,7 @@ public class SmartClientSmartServerTest extends HttpTestCase {
cfg.setInt("http", null, "postbuffer", 8 * 1024);
cfg.save();
- t = Transport.open(db, remoteURI);
- try {
+ try (Transport t = Transport.open(db, remoteURI)) {
final String srcExpr = Q.name();
final boolean forceUpdate = false;
final String localName = null;
@@ -652,8 +618,6 @@ public class SmartClientSmartServerTest extends HttpTestCase {
RemoteRefUpdate u = new RemoteRefUpdate(src.getRepository(),
srcExpr, dstName, forceUpdate, localName, oldId);
t.push(NullProgressMonitor.INSTANCE, Collections.singleton(u));
- } finally {
- t.close();
}
assertTrue(remoteRepository.hasObject(Q_bin));
diff --git a/org.eclipse.jgit.junit.http/BUCK b/org.eclipse.jgit.junit.http/BUCK
new file mode 100644
index 0000000000..68976a68ae
--- /dev/null
+++ b/org.eclipse.jgit.junit.http/BUCK
@@ -0,0 +1,18 @@
+java_library(
+ name = 'junit-http',
+ srcs = glob(['src/**']),
+ resources = glob(['resources/**']),
+ provided_deps = [
+ '//org.eclipse.jgit:jgit',
+ '//org.eclipse.jgit.http.server:jgit-servlet',
+ '//org.eclipse.jgit.junit:junit',
+ '//lib:junit',
+ '//lib:servlet-api',
+ '//lib/jetty:http',
+ '//lib/jetty:server',
+ '//lib/jetty:servlet',
+ '//lib/jetty:security',
+ '//lib/jetty:util',
+ ],
+ visibility = ['PUBLIC'],
+)
diff --git a/org.eclipse.jgit.junit/BUCK b/org.eclipse.jgit.junit/BUCK
new file mode 100644
index 0000000000..7e2543220a
--- /dev/null
+++ b/org.eclipse.jgit.junit/BUCK
@@ -0,0 +1,10 @@
+java_library(
+ name = 'junit',
+ srcs = glob(['src/**']),
+ resources = glob(['resources/**']),
+ provided_deps = [
+ '//org.eclipse.jgit:jgit',
+ '//lib:junit',
+ ],
+ visibility = ['PUBLIC'],
+)
diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java
index ac9685d375..8439c39c8b 100644
--- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java
+++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java
@@ -822,7 +822,7 @@ public class TestRepository<R extends Repository> {
break;
final byte[] bin = db.open(o, o.getType()).getCachedBytes();
- oc.checkCommit(bin);
+ oc.checkCommit(o, bin);
assertHash(o, bin);
}
@@ -832,7 +832,7 @@ public class TestRepository<R extends Repository> {
break;
final byte[] bin = db.open(o, o.getType()).getCachedBytes();
- oc.check(o.getType(), bin);
+ oc.check(o, o.getType(), bin);
assertHash(o, bin);
}
}
@@ -866,7 +866,7 @@ public class TestRepository<R extends Repository> {
Set<ObjectId> all = new HashSet<ObjectId>();
for (Ref r : db.getAllRefs().values())
all.add(r.getObjectId());
- pw.preparePack(m, all, Collections.<ObjectId> emptySet());
+ pw.preparePack(m, all, PackWriter.NONE);
final ObjectId name = pw.computeName();
@@ -1155,8 +1155,7 @@ public class TestRepository<R extends Repository> {
return self;
}
- private void insertChangeId(org.eclipse.jgit.lib.CommitBuilder c)
- throws IOException {
+ private void insertChangeId(org.eclipse.jgit.lib.CommitBuilder c) {
if (changeId == null)
return;
int idx = ChangeIdUtil.indexOfChangeId(message, "\n");
diff --git a/org.eclipse.jgit.pgm.test/BUCK b/org.eclipse.jgit.pgm.test/BUCK
new file mode 100644
index 0000000000..a3859c9b49
--- /dev/null
+++ b/org.eclipse.jgit.pgm.test/BUCK
@@ -0,0 +1,38 @@
+TESTS = glob(['tst/**/*.java'])
+
+for t in TESTS:
+ n = t[len('tst/'):len(t)-len('.java')].replace('/', '.')
+ java_test(
+ name = n,
+ labels = ['pgm'],
+ srcs = [t],
+ deps = [
+ ':helpers',
+ '//org.eclipse.jgit:jgit',
+ '//org.eclipse.jgit.archive:jgit-archive',
+ '//org.eclipse.jgit.junit:junit',
+ '//org.eclipse.jgit.pgm:pgm',
+ '//lib:hamcrest-core',
+ '//lib:hamcrest-library',
+ '//lib:javaewah',
+ '//lib:junit',
+ '//lib:slf4j-api',
+ '//lib:slf4j-simple',
+ '//lib:commons-compress',
+ '//lib:tukaani-xz',
+ ],
+ source_under_test = ['//org.eclipse.jgit.pgm:pgm'],
+ vm_args = ['-Xmx256m', '-Dfile.encoding=UTF-8'],
+ )
+
+java_library(
+ name = 'helpers',
+ srcs = glob(['src/**/*.java']),
+ deps = [
+ '//org.eclipse.jgit:jgit',
+ '//org.eclipse.jgit.pgm:pgm',
+ '//org.eclipse.jgit.junit:junit',
+ '//lib:args4j',
+ '//lib:junit',
+ ],
+)
diff --git a/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF
index db23c3b55f..2514fdff7e 100644
--- a/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF
@@ -11,6 +11,7 @@ Import-Package: org.eclipse.jgit.api;version="[4.2.0,4.3.0)",
org.eclipse.jgit.api.errors;version="[4.2.0,4.3.0)",
org.eclipse.jgit.diff;version="[4.2.0,4.3.0)",
org.eclipse.jgit.dircache;version="[4.2.0,4.3.0)",
+ org.eclipse.jgit.internal.storage.file;version="4.2.0",
org.eclipse.jgit.junit;version="[4.2.0,4.3.0)",
org.eclipse.jgit.lib;version="[4.2.0,4.3.0)",
org.eclipse.jgit.merge;version="[4.2.0,4.3.0)",
diff --git a/org.eclipse.jgit.pgm.test/org.eclipse.jgit.pgm--All-Tests (Java8) (de).launch b/org.eclipse.jgit.pgm.test/org.eclipse.jgit.pgm--All-Tests (Java8) (de).launch
new file mode 100644
index 0000000000..5c137f28fe
--- /dev/null
+++ b/org.eclipse.jgit.pgm.test/org.eclipse.jgit.pgm--All-Tests (Java8) (de).launch
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<launchConfiguration type="org.eclipse.jdt.junit.launchconfig">
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
+<listEntry value="/org.eclipse.jgit.pgm.test/tst"/>
+</listAttribute>
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
+<listEntry value="2"/>
+</listAttribute>
+<booleanAttribute key="org.eclipse.debug.core.appendEnvironmentVariables" value="true"/>
+<mapAttribute key="org.eclipse.debug.core.environmentVariables">
+<mapEntry key="LANG" value="de_DE.UTF-8"/>
+</mapAttribute>
+<listAttribute key="org.eclipse.debug.ui.favoriteGroups">
+<listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
+<listEntry value="org.eclipse.debug.ui.launchGroup.run"/>
+</listAttribute>
+<stringAttribute key="org.eclipse.jdt.junit.CONTAINER" value="=org.eclipse.jgit.pgm.test/tst"/>
+<booleanAttribute key="org.eclipse.jdt.junit.KEEPRUNNING_ATTR" value="false"/>
+<stringAttribute key="org.eclipse.jdt.junit.TESTNAME" value=""/>
+<stringAttribute key="org.eclipse.jdt.junit.TEST_KIND" value="org.eclipse.jdt.junit.loader.junit4"/>
+<booleanAttribute key="org.eclipse.jdt.launching.ATTR_USE_START_ON_FIRST_THREAD" value="true"/>
+<listAttribute key="org.eclipse.jdt.launching.CLASSPATH">
+<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry containerPath=&quot;org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.7&quot; path=&quot;1&quot; type=&quot;4&quot;/&gt;&#10;"/>
+<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry id=&quot;org.eclipse.jdt.launching.classpathentry.defaultClasspath&quot;&gt;&#10;&lt;memento exportedEntriesOnly=&quot;false&quot; project=&quot;org.eclipse.jgit.pgm.test&quot;/&gt;&#10;&lt;/runtimeClasspathEntry&gt;&#10;"/>
+</listAttribute>
+<booleanAttribute key="org.eclipse.jdt.launching.DEFAULT_CLASSPATH" value="false"/>
+<stringAttribute key="org.eclipse.jdt.launching.JRE_CONTAINER" value="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
+<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value=""/>
+<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="org.eclipse.jgit.pgm.test"/>
+</launchConfiguration>
diff --git a/org.eclipse.jgit.pgm.test/src/org/eclipse/jgit/lib/CLIRepositoryTestCase.java b/org.eclipse.jgit.pgm.test/src/org/eclipse/jgit/lib/CLIRepositoryTestCase.java
index 559a6d5d40..a6af077aa5 100644
--- a/org.eclipse.jgit.pgm.test/src/org/eclipse/jgit/lib/CLIRepositoryTestCase.java
+++ b/org.eclipse.jgit.pgm.test/src/org/eclipse/jgit/lib/CLIRepositoryTestCase.java
@@ -46,12 +46,16 @@ import static org.junit.Assert.assertEquals;
import java.io.File;
import java.io.IOException;
+import java.nio.file.Path;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import org.eclipse.jgit.junit.JGitTestUtil;
import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
import org.eclipse.jgit.pgm.CLIGitCommand;
+import org.eclipse.jgit.pgm.CLIGitCommand.Result;
+import org.eclipse.jgit.pgm.TextBuiltin.TerminatedByHelpException;
import org.junit.Before;
public class CLIRepositoryTestCase extends LocalDiskRepositoryTestCase {
@@ -69,13 +73,59 @@ public class CLIRepositoryTestCase extends LocalDiskRepositoryTestCase {
trash = db.getWorkTree();
}
+ /**
+ * Executes specified git commands (with arguments)
+ *
+ * @param cmds
+ * each string argument must be a valid git command line, e.g.
+ * "git branch -h"
+ * @return command output
+ * @throws Exception
+ */
+ protected String[] executeUnchecked(String... cmds) throws Exception {
+ List<String> result = new ArrayList<String>(cmds.length);
+ for (String cmd : cmds) {
+ result.addAll(CLIGitCommand.executeUnchecked(cmd, db));
+ }
+ return result.toArray(new String[0]);
+ }
+
+ /**
+ * Executes specified git commands (with arguments), throws exception and
+ * stops execution on first command which output contains a 'fatal:' error
+ *
+ * @param cmds
+ * each string argument must be a valid git command line, e.g.
+ * "git branch -h"
+ * @return command output
+ * @throws Exception
+ */
protected String[] execute(String... cmds) throws Exception {
List<String> result = new ArrayList<String>(cmds.length);
- for (String cmd : cmds)
- result.addAll(CLIGitCommand.execute(cmd, db));
+ for (String cmd : cmds) {
+ Result r = CLIGitCommand.executeRaw(cmd, db);
+ if (r.ex instanceof TerminatedByHelpException) {
+ result.addAll(r.errLines());
+ } else if (r.ex != null) {
+ throw r.ex;
+ }
+ result.addAll(r.outLines());
+ }
return result.toArray(new String[0]);
}
+ /**
+ * @param link
+ * the path of the symbolic link to create
+ * @param target
+ * the target of the symbolic link
+ * @return the path to the symbolic link
+ * @throws Exception
+ */
+ protected Path writeLink(String link, String target) throws Exception {
+ return JGitTestUtil.writeLink(db, link, target);
+ }
+
protected File writeTrashFile(final String name, final String data)
throws IOException {
return JGitTestUtil.writeTrashFile(db, name, data);
@@ -173,15 +223,36 @@ public class CLIRepositoryTestCase extends LocalDiskRepositoryTestCase {
}
protected void assertArrayOfLinesEquals(String[] expected, String[] actual) {
- assertEquals(toText(expected), toText(actual));
+ assertEquals(toString(expected), toString(actual));
+ }
+
+ public static String toString(String... lines) {
+ return toString(Arrays.asList(lines));
}
- private static String toText(String[] lines) {
+ public static String toString(List<String> lines) {
StringBuilder b = new StringBuilder();
for (String s : lines) {
- b.append(s);
- b.append('\n');
+ // trim indentation, to simplify tests
+ s = s.trim();
+ if (s != null && !s.isEmpty()) {
+ b.append(s);
+ b.append('\n');
+ }
+ }
+ // delete last line break to allow simpler tests with one line compare
+ if (b.length() > 0 && b.charAt(b.length() - 1) == '\n') {
+ b.deleteCharAt(b.length() - 1);
}
return b.toString();
}
+
+ public static boolean contains(List<String> lines, String str) {
+ for (String s : lines) {
+ if (s.contains(str)) {
+ return true;
+ }
+ }
+ return false;
+ }
}
diff --git a/org.eclipse.jgit.pgm.test/src/org/eclipse/jgit/pgm/CLIGitCommand.java b/org.eclipse.jgit.pgm.test/src/org/eclipse/jgit/pgm/CLIGitCommand.java
index d77b1505ae..3f396563c2 100644
--- a/org.eclipse.jgit.pgm.test/src/org/eclipse/jgit/pgm/CLIGitCommand.java
+++ b/org.eclipse.jgit.pgm.test/src/org/eclipse/jgit/pgm/CLIGitCommand.java
@@ -42,71 +42,140 @@
*/
package org.eclipse.jgit.pgm;
+import static org.junit.Assert.assertNull;
+
import java.io.ByteArrayOutputStream;
-import java.text.MessageFormat;
+import java.io.File;
+
+import java.io.IOException;
+import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.pgm.internal.CLIText;
-import org.eclipse.jgit.pgm.opt.CmdLineParser;
-import org.eclipse.jgit.pgm.opt.SubcommandHandler;
+import org.eclipse.jgit.pgm.TextBuiltin.TerminatedByHelpException;
import org.eclipse.jgit.util.IO;
-import org.kohsuke.args4j.Argument;
-public class CLIGitCommand {
- @Argument(index = 0, metaVar = "metaVar_command", required = true, handler = SubcommandHandler.class)
- private TextBuiltin subcommand;
+public class CLIGitCommand extends Main {
- @Argument(index = 1, metaVar = "metaVar_arg")
- private List<String> arguments = new ArrayList<String>();
+ private final Result result;
- public TextBuiltin getSubcommand() {
- return subcommand;
+ private final Repository db;
+
+ public CLIGitCommand(Repository db) {
+ super();
+ this.db = db;
+ result = new Result();
}
- public List<String> getArguments() {
- return arguments;
+ /**
+ * Executes git commands (with arguments) specified on the command line. The
+ * git repository (same for all commands) can be specified via system
+ * property "-Dgit_work_tree=path_to_work_tree". If the property is not set,
+ * current directory is used.
+ *
+ * @param args
+ * each element in the array must be a valid git command line,
+ * e.g. "git branch -h"
+ * @throws Exception
+ */
+ public static void main(String[] args) throws Exception {
+ String workDir = System.getProperty("git_work_tree");
+ if (workDir == null) {
+ workDir = ".";
+ System.out.println(
+ "System property 'git_work_tree' not specified, using current directory: "
+ + new File(workDir).getAbsolutePath());
+ }
+ try (Repository db = new FileRepository(workDir + "/.git")) {
+ for (String cmd : args) {
+ List<String> result = execute(cmd, db);
+ for (String line : result) {
+ System.out.println(line);
+ }
+ }
+ }
}
public static List<String> execute(String str, Repository db)
throws Exception {
+ Result result = executeRaw(str, db);
+ return getOutput(result);
+ }
+
+ public static Result executeRaw(String str, Repository db)
+ throws Exception {
+ CLIGitCommand cmd = new CLIGitCommand(db);
+ cmd.run(str);
+ return cmd.result;
+ }
+
+ public static List<String> executeUnchecked(String str, Repository db)
+ throws Exception {
+ CLIGitCommand cmd = new CLIGitCommand(db);
+ try {
+ cmd.run(str);
+ return getOutput(cmd.result);
+ } catch (Throwable e) {
+ return cmd.result.errLines();
+ }
+ }
+
+ private static List<String> getOutput(Result result) {
+ if (result.ex instanceof TerminatedByHelpException) {
+ return result.errLines();
+ }
+ return result.outLines();
+ }
+
+ private void run(String commandLine) throws Exception {
+ String[] argv = convertToMainArgs(commandLine);
try {
- return IO.readLines(new String(rawExecute(str, db)));
- } catch (Die e) {
- return IO.readLines(MessageFormat.format(CLIText.get().fatalError,
- e.getMessage()));
+ super.run(argv);
+ } catch (TerminatedByHelpException e) {
+ // this is not a failure, super called exit() on help
+ } finally {
+ writer.flush();
}
}
- public static byte[] rawExecute(String str, Repository db)
+ private static String[] convertToMainArgs(String str)
throws Exception {
String[] args = split(str);
- if (!args[0].equalsIgnoreCase("git") || args.length < 2)
+ if (!args[0].equalsIgnoreCase("git") || args.length < 2) {
throw new IllegalArgumentException(
"Expected 'git <command> [<args>]', was:" + str);
+ }
String[] argv = new String[args.length - 1];
System.arraycopy(args, 1, argv, 0, args.length - 1);
+ return argv;
+ }
- CLIGitCommand bean = new CLIGitCommand();
- final CmdLineParser clp = new CmdLineParser(bean);
- clp.parseArgument(argv);
-
- final TextBuiltin cmd = bean.getSubcommand();
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- cmd.outs = baos;
- if (cmd.requiresRepository())
- cmd.init(db, null);
- else
- cmd.init(null, null);
- try {
- cmd.execute(bean.getArguments().toArray(
- new String[bean.getArguments().size()]));
- } finally {
- if (cmd.outw != null)
- cmd.outw.flush();
+ @Override
+ PrintWriter createErrorWriter() {
+ return new PrintWriter(result.err);
+ }
+
+ void init(final TextBuiltin cmd) throws IOException {
+ cmd.outs = result.out;
+ cmd.errs = result.err;
+ super.init(cmd);
+ }
+
+ @Override
+ protected Repository openGitDir(String aGitdir) throws IOException {
+ assertNull(aGitdir);
+ return db;
+ }
+
+ @Override
+ void exit(int status, Exception t) throws Exception {
+ if (t == null) {
+ t = new IllegalStateException(Integer.toString(status));
}
- return baos.toByteArray();
+ result.ex = t;
+ throw t;
}
/**
@@ -164,4 +233,36 @@ public class CLIGitCommand {
return list.toArray(new String[list.size()]);
}
+ public static class Result {
+ public final ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+ public final ByteArrayOutputStream err = new ByteArrayOutputStream();
+
+ public Exception ex;
+
+ public byte[] outBytes() {
+ return out.toByteArray();
+ }
+
+ public byte[] errBytes() {
+ return err.toByteArray();
+ }
+
+ public String outString() {
+ return out.toString();
+ }
+
+ public List<String> outLines() {
+ return IO.readLines(out.toString());
+ }
+
+ public String errString() {
+ return err.toString();
+ }
+
+ public List<String> errLines() {
+ return IO.readLines(err.toString());
+ }
+ }
+
}
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/AddTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/AddTest.java
index 4253080a66..3edd9b88e8 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/AddTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/AddTest.java
@@ -45,15 +45,12 @@ package org.eclipse.jgit.pgm;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
-
-import java.lang.Exception;
-import java.lang.String;
+import static org.junit.Assert.fail;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.lib.CLIRepositoryTestCase;
import org.junit.Before;
-import org.junit.Ignore;
import org.junit.Test;
public class AddTest extends CLIRepositoryTestCase {
@@ -66,14 +63,16 @@ public class AddTest extends CLIRepositoryTestCase {
git = new Git(db);
}
- @Ignore("args4j exit()s on error instead of throwing, JVM goes down")
@Test
public void testAddNothing() throws Exception {
- assertEquals("fatal: Argument \"filepattern\" is required", //
- execute("git add")[0]);
+ try {
+ execute("git add");
+ fail("Must die");
+ } catch (Die e) {
+ // expected, requires argument
+ }
}
- @Ignore("args4j exit()s for --help, too")
@Test
public void testAddUsage() throws Exception {
execute("git add --help");
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ArchiveTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ArchiveTest.java
index 4222a2dcc3..a503ffdad0 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ArchiveTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ArchiveTest.java
@@ -52,17 +52,15 @@ import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
-import java.io.InputStreamReader;
import java.io.IOException;
+import java.io.InputStreamReader;
import java.io.OutputStream;
-import java.lang.Object;
-import java.lang.String;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
-import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
@@ -71,9 +69,7 @@ import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.lib.CLIRepositoryTestCase;
import org.eclipse.jgit.lib.FileMode;
-import org.eclipse.jgit.pgm.CLIGitCommand;
import org.junit.Before;
-import org.junit.Ignore;
import org.junit.Test;
public class ArchiveTest extends CLIRepositoryTestCase {
@@ -89,25 +85,26 @@ public class ArchiveTest extends CLIRepositoryTestCase {
emptyTree = db.resolve("HEAD^{tree}").abbreviate(12).name();
}
- @Ignore("Some versions of java.util.zip refuse to write an empty ZIP")
@Test
public void testEmptyArchive() throws Exception {
- byte[] result = CLIGitCommand.rawExecute(
- "git archive --format=zip " + emptyTree, db);
+ byte[] result = CLIGitCommand.executeRaw(
+ "git archive --format=zip " + emptyTree, db).outBytes();
assertArrayEquals(new String[0], listZipEntries(result));
}
@Test
public void testEmptyTar() throws Exception {
- byte[] result = CLIGitCommand.rawExecute(
- "git archive --format=tar " + emptyTree, db);
+ byte[] result = CLIGitCommand.executeRaw(
+ "git archive --format=tar " + emptyTree, db).outBytes();
assertArrayEquals(new String[0], listTarEntries(result));
}
@Test
public void testUnrecognizedFormat() throws Exception {
- String[] expect = new String[] { "fatal: Unknown archive format 'nonsense'" };
- String[] actual = execute("git archive --format=nonsense " + emptyTree);
+ String[] expect = new String[] {
+ "fatal: Unknown archive format 'nonsense'", "" };
+ String[] actual = executeUnchecked(
+ "git archive --format=nonsense " + emptyTree);
assertArrayEquals(expect, actual);
}
@@ -120,8 +117,8 @@ public class ArchiveTest extends CLIRepositoryTestCase {
git.add().addFilepattern("c").call();
git.commit().setMessage("populate toplevel").call();
- byte[] result = CLIGitCommand.rawExecute(
- "git archive --format=zip HEAD", db);
+ byte[] result = CLIGitCommand.executeRaw(
+ "git archive --format=zip HEAD", db).outBytes();
assertArrayEquals(new String[] { "a", "c" },
listZipEntries(result));
}
@@ -135,8 +132,8 @@ public class ArchiveTest extends CLIRepositoryTestCase {
@Test
public void testDefaultFormatIsTar() throws Exception {
commitGreeting();
- byte[] result = CLIGitCommand.rawExecute(
- "git archive HEAD", db);
+ byte[] result = CLIGitCommand.executeRaw(
+ "git archive HEAD", db).outBytes();
assertArrayEquals(new String[] { "greeting" },
listTarEntries(result));
}
@@ -302,8 +299,8 @@ public class ArchiveTest extends CLIRepositoryTestCase {
git.add().addFilepattern("b").call();
git.commit().setMessage("add subdir").call();
- byte[] result = CLIGitCommand.rawExecute(
- "git archive --format=zip master", db);
+ byte[] result = CLIGitCommand.executeRaw(
+ "git archive --format=zip master", db).outBytes();
String[] expect = { "a", "b.c", "b0c", "b/", "b/a", "b/b", "c" };
String[] actual = listZipEntries(result);
@@ -328,8 +325,8 @@ public class ArchiveTest extends CLIRepositoryTestCase {
git.add().addFilepattern("b").call();
git.commit().setMessage("add subdir").call();
- byte[] result = CLIGitCommand.rawExecute(
- "git archive --format=tar master", db);
+ byte[] result = CLIGitCommand.executeRaw(
+ "git archive --format=tar master", db).outBytes();
String[] expect = { "a", "b.c", "b0c", "b/", "b/a", "b/b", "c" };
String[] actual = listTarEntries(result);
@@ -349,8 +346,8 @@ public class ArchiveTest extends CLIRepositoryTestCase {
@Test
public void testArchivePrefixOption() throws Exception {
commitBazAndFooSlashBar();
- byte[] result = CLIGitCommand.rawExecute(
- "git archive --prefix=x/ --format=zip master", db);
+ byte[] result = CLIGitCommand.executeRaw(
+ "git archive --prefix=x/ --format=zip master", db).outBytes();
String[] expect = { "x/baz", "x/foo/", "x/foo/bar" };
String[] actual = listZipEntries(result);
@@ -362,8 +359,8 @@ public class ArchiveTest extends CLIRepositoryTestCase {
@Test
public void testTarPrefixOption() throws Exception {
commitBazAndFooSlashBar();
- byte[] result = CLIGitCommand.rawExecute(
- "git archive --prefix=x/ --format=tar master", db);
+ byte[] result = CLIGitCommand.executeRaw(
+ "git archive --prefix=x/ --format=tar master", db).outBytes();
String[] expect = { "x/baz", "x/foo/", "x/foo/bar" };
String[] actual = listTarEntries(result);
@@ -381,8 +378,8 @@ public class ArchiveTest extends CLIRepositoryTestCase {
@Test
public void testPrefixDoesNotNormalizeDoubleSlash() throws Exception {
commitFoo();
- byte[] result = CLIGitCommand.rawExecute(
- "git archive --prefix=x// --format=zip master", db);
+ byte[] result = CLIGitCommand.executeRaw(
+ "git archive --prefix=x// --format=zip master", db).outBytes();
String[] expect = { "x//foo" };
assertArrayEquals(expect, listZipEntries(result));
}
@@ -390,8 +387,8 @@ public class ArchiveTest extends CLIRepositoryTestCase {
@Test
public void testPrefixDoesNotNormalizeDoubleSlashInTar() throws Exception {
commitFoo();
- byte[] result = CLIGitCommand.rawExecute(
- "git archive --prefix=x// --format=tar master", db);
+ byte[] result = CLIGitCommand.executeRaw(
+ "git archive --prefix=x// --format=tar master", db).outBytes();
String[] expect = { "x//foo" };
assertArrayEquals(expect, listTarEntries(result));
}
@@ -408,8 +405,8 @@ public class ArchiveTest extends CLIRepositoryTestCase {
@Test
public void testPrefixWithoutTrailingSlash() throws Exception {
commitBazAndFooSlashBar();
- byte[] result = CLIGitCommand.rawExecute(
- "git archive --prefix=my- --format=zip master", db);
+ byte[] result = CLIGitCommand.executeRaw(
+ "git archive --prefix=my- --format=zip master", db).outBytes();
String[] expect = { "my-baz", "my-foo/", "my-foo/bar" };
String[] actual = listZipEntries(result);
@@ -421,8 +418,8 @@ public class ArchiveTest extends CLIRepositoryTestCase {
@Test
public void testTarPrefixWithoutTrailingSlash() throws Exception {
commitBazAndFooSlashBar();
- byte[] result = CLIGitCommand.rawExecute(
- "git archive --prefix=my- --format=tar master", db);
+ byte[] result = CLIGitCommand.executeRaw(
+ "git archive --prefix=my- --format=tar master", db).outBytes();
String[] expect = { "my-baz", "my-foo/", "my-foo/bar" };
String[] actual = listTarEntries(result);
@@ -441,8 +438,8 @@ public class ArchiveTest extends CLIRepositoryTestCase {
git.submoduleAdd().setURI("./.").setPath("b").call().close();
git.commit().setMessage("add submodule").call();
- byte[] result = CLIGitCommand.rawExecute(
- "git archive --format=zip master", db);
+ byte[] result = CLIGitCommand.executeRaw(
+ "git archive --format=zip master", db).outBytes();
String[] expect = { ".gitmodules", "a", "b/", "c" };
String[] actual = listZipEntries(result);
@@ -461,8 +458,8 @@ public class ArchiveTest extends CLIRepositoryTestCase {
git.submoduleAdd().setURI("./.").setPath("b").call().close();
git.commit().setMessage("add submodule").call();
- byte[] result = CLIGitCommand.rawExecute(
- "git archive --format=tar master", db);
+ byte[] result = CLIGitCommand.executeRaw(
+ "git archive --format=tar master", db).outBytes();
String[] expect = { ".gitmodules", "a", "b/", "c" };
String[] actual = listTarEntries(result);
@@ -491,8 +488,8 @@ public class ArchiveTest extends CLIRepositoryTestCase {
git.commit().setMessage("three files with different modes").call();
- byte[] zipData = CLIGitCommand.rawExecute(
- "git archive --format=zip master", db);
+ byte[] zipData = CLIGitCommand.executeRaw(
+ "git archive --format=zip master", db).outBytes();
writeRaw("zip-with-modes.zip", zipData);
assertContainsEntryWithMode("zip-with-modes.zip", "-rw-", "plain");
assertContainsEntryWithMode("zip-with-modes.zip", "-rwx", "executable");
@@ -520,8 +517,8 @@ public class ArchiveTest extends CLIRepositoryTestCase {
git.commit().setMessage("three files with different modes").call();
- byte[] archive = CLIGitCommand.rawExecute(
- "git archive --format=tar master", db);
+ byte[] archive = CLIGitCommand.executeRaw(
+ "git archive --format=tar master", db).outBytes();
writeRaw("with-modes.tar", archive);
assertTarContainsEntry("with-modes.tar", "-rw-r--r--", "plain");
assertTarContainsEntry("with-modes.tar", "-rwxr-xr-x", "executable");
@@ -543,8 +540,8 @@ public class ArchiveTest extends CLIRepositoryTestCase {
git.add().addFilepattern("1234567890").call();
git.commit().setMessage("file with long name").call();
- byte[] result = CLIGitCommand.rawExecute(
- "git archive --format=zip HEAD", db);
+ byte[] result = CLIGitCommand.executeRaw(
+ "git archive --format=zip HEAD", db).outBytes();
assertArrayEquals(l.toArray(new String[l.size()]),
listZipEntries(result));
}
@@ -563,8 +560,8 @@ public class ArchiveTest extends CLIRepositoryTestCase {
git.add().addFilepattern("1234567890").call();
git.commit().setMessage("file with long name").call();
- byte[] result = CLIGitCommand.rawExecute(
- "git archive --format=tar HEAD", db);
+ byte[] result = CLIGitCommand.executeRaw(
+ "git archive --format=tar HEAD", db).outBytes();
assertArrayEquals(l.toArray(new String[l.size()]),
listTarEntries(result));
}
@@ -576,8 +573,8 @@ public class ArchiveTest extends CLIRepositoryTestCase {
git.add().addFilepattern("xyzzy").call();
git.commit().setMessage("add file with content").call();
- byte[] result = CLIGitCommand.rawExecute(
- "git archive --format=zip HEAD", db);
+ byte[] result = CLIGitCommand.executeRaw(
+ "git archive --format=zip HEAD", db).outBytes();
assertArrayEquals(new String[] { payload },
zipEntryContent(result, "xyzzy"));
}
@@ -589,8 +586,8 @@ public class ArchiveTest extends CLIRepositoryTestCase {
git.add().addFilepattern("xyzzy").call();
git.commit().setMessage("add file with content").call();
- byte[] result = CLIGitCommand.rawExecute(
- "git archive --format=tar HEAD", db);
+ byte[] result = CLIGitCommand.executeRaw(
+ "git archive --format=tar HEAD", db).outBytes();
assertArrayEquals(new String[] { payload },
tarEntryContent(result, "xyzzy"));
}
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/BranchTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/BranchTest.java
index d1bd5baceb..55f4d8b1b8 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/BranchTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/BranchTest.java
@@ -43,11 +43,17 @@
package org.eclipse.jgit.pgm;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.File;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.lib.CLIRepositoryTestCase;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.pgm.internal.CLIText;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Before;
import org.junit.Test;
@@ -63,9 +69,19 @@ public class BranchTest extends CLIRepositoryTestCase {
}
@Test
+ public void testHelpAfterDelete() throws Exception {
+ String err = toString(executeUnchecked("git branch -d"));
+ String help = toString(executeUnchecked("git branch -h"));
+ String errAndHelp = toString(executeUnchecked("git branch -d -h"));
+ assertEquals(CLIText.fatalError(CLIText.get().branchNameRequired), err);
+ assertEquals(toString(err, help), errAndHelp);
+ }
+
+ @Test
public void testList() throws Exception {
+ assertEquals("* master", toString(execute("git branch")));
assertEquals("* master 6fd41be initial commit",
- execute("git branch -v")[0]);
+ toString(execute("git branch -v")));
}
@Test
@@ -73,26 +89,188 @@ public class BranchTest extends CLIRepositoryTestCase {
RefUpdate updateRef = db.updateRef(Constants.HEAD, true);
updateRef.setNewObjectId(db.resolve("6fd41be"));
updateRef.update();
- assertEquals("* (no branch) 6fd41be initial commit",
- execute("git branch -v")[0]);
+ assertEquals(
+ toString("* (no branch) 6fd41be initial commit",
+ "master 6fd41be initial commit"),
+ toString(execute("git branch -v")));
}
@Test
public void testListContains() throws Exception {
try (Git git = new Git(db)) {
- git.branchCreate().setName("initial").call();
+ git.branchCreate().setName("initial").call();
RevCommit second = git.commit().setMessage("second commit")
.call();
- assertArrayOfLinesEquals(new String[] { " initial", "* master", "" },
- execute("git branch --contains 6fd41be"));
- assertArrayOfLinesEquals(new String[] { "* master", "" },
- execute("git branch --contains " + second.name()));
+ assertEquals(toString(" initial", "* master"),
+ toString(execute("git branch --contains 6fd41be")));
+ assertEquals("* master",
+ toString(execute("git branch --contains " + second.name())));
}
}
@Test
public void testExistingBranch() throws Exception {
assertEquals("fatal: A branch named 'master' already exists.",
- execute("git branch master")[0]);
+ toString(executeUnchecked("git branch master")));
+ }
+
+ @Test
+ public void testRenameSingleArg() throws Exception {
+ try {
+ toString(execute("git branch -m"));
+ fail("Must die");
+ } catch (Die e) {
+ // expected, requires argument
+ }
+ String result = toString(execute("git branch -m slave"));
+ assertEquals("", result);
+ result = toString(execute("git branch -a"));
+ assertEquals("* slave", result);
+ }
+
+ @Test
+ public void testRenameTwoArgs() throws Exception {
+ String result = toString(execute("git branch -m master slave"));
+ assertEquals("", result);
+ result = toString(execute("git branch -a"));
+ assertEquals("* slave", result);
+ }
+
+ @Test
+ public void testCreate() throws Exception {
+ try {
+ toString(execute("git branch a b"));
+ fail("Must die");
+ } catch (Die e) {
+ // expected, too many arguments
+ }
+ String result = toString(execute("git branch second"));
+ assertEquals("", result);
+ result = toString(execute("git branch"));
+ assertEquals(toString("* master", "second"), result);
+ result = toString(execute("git branch -v"));
+ assertEquals(toString("* master 6fd41be initial commit",
+ "second 6fd41be initial commit"), result);
+ }
+
+ @Test
+ public void testDelete() throws Exception {
+ try {
+ toString(execute("git branch -d"));
+ fail("Must die");
+ } catch (Die e) {
+ // expected, requires argument
+ }
+ String result = toString(execute("git branch second"));
+ assertEquals("", result);
+ result = toString(execute("git branch -d second"));
+ assertEquals("", result);
+ result = toString(execute("git branch"));
+ assertEquals("* master", result);
+ }
+
+ @Test
+ public void testDeleteMultiple() throws Exception {
+ String result = toString(execute("git branch second",
+ "git branch third", "git branch fourth"));
+ assertEquals("", result);
+ result = toString(execute("git branch -d second third fourth"));
+ assertEquals("", result);
+ result = toString(execute("git branch"));
+ assertEquals("* master", result);
+ }
+
+ @Test
+ public void testDeleteForce() throws Exception {
+ try {
+ toString(execute("git branch -D"));
+ fail("Must die");
+ } catch (Die e) {
+ // expected, requires argument
+ }
+ String result = toString(execute("git branch second"));
+ assertEquals("", result);
+ result = toString(execute("git checkout second"));
+ assertEquals("Switched to branch 'second'", result);
+
+ File a = writeTrashFile("a", "a");
+ assertTrue(a.exists());
+ execute("git add a", "git commit -m 'added a'");
+
+ result = toString(execute("git checkout master"));
+ assertEquals("Switched to branch 'master'", result);
+
+ result = toString(execute("git branch"));
+ assertEquals(toString("* master", "second"), result);
+
+ try {
+ toString(execute("git branch -d second"));
+ fail("Must die");
+ } catch (Die e) {
+ // expected, the current HEAD is on second and not merged to master
+ }
+ result = toString(execute("git branch -D second"));
+ assertEquals("", result);
+
+ result = toString(execute("git branch"));
+ assertEquals("* master", result);
+ }
+
+ @Test
+ public void testDeleteForceMultiple() throws Exception {
+ String result = toString(execute("git branch second",
+ "git branch third", "git branch fourth"));
+
+ assertEquals("", result);
+ result = toString(execute("git checkout second"));
+ assertEquals("Switched to branch 'second'", result);
+
+ File a = writeTrashFile("a", "a");
+ assertTrue(a.exists());
+ execute("git add a", "git commit -m 'added a'");
+
+ result = toString(execute("git checkout master"));
+ assertEquals("Switched to branch 'master'", result);
+
+ result = toString(execute("git branch"));
+ assertEquals(toString("fourth", "* master", "second", "third"), result);
+
+ try {
+ toString(execute("git branch -d second third fourth"));
+ fail("Must die");
+ } catch (Die e) {
+ // expected, the current HEAD is on second and not merged to master
+ }
+ result = toString(execute("git branch"));
+ assertEquals(toString("fourth", "* master", "second", "third"), result);
+
+ result = toString(execute("git branch -D second third fourth"));
+ assertEquals("", result);
+
+ result = toString(execute("git branch"));
+ assertEquals("* master", result);
+ }
+
+ @Test
+ public void testCreateFromOldCommit() throws Exception {
+ File a = writeTrashFile("a", "a");
+ assertTrue(a.exists());
+ execute("git add a", "git commit -m 'added a'");
+ File b = writeTrashFile("b", "b");
+ assertTrue(b.exists());
+ execute("git add b", "git commit -m 'added b'");
+ String result = toString(execute("git log -n 1 --reverse"));
+ String firstCommitId = result.substring("commit ".length(),
+ result.indexOf('\n'));
+
+ result = toString(execute("git branch -f second " + firstCommitId));
+ assertEquals("", result);
+
+ result = toString(execute("git branch"));
+ assertEquals(toString("* master", "second"), result);
+
+ result = toString(execute("git checkout second"));
+ assertEquals("Switched to branch 'second'", result);
+ assertFalse(b.exists());
}
}
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CheckoutTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CheckoutTest.java
index cb36d057e4..e690ad6964 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CheckoutTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CheckoutTest.java
@@ -44,9 +44,14 @@ package org.eclipse.jgit.pgm;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
import java.util.List;
import org.eclipse.jgit.api.Git;
@@ -59,7 +64,9 @@ import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.treewalk.FileTreeIterator;
import org.eclipse.jgit.treewalk.FileTreeIterator.FileEntry;
import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.FileUtils;
+import org.junit.Assume;
import org.junit.Test;
public class CheckoutTest extends CLIRepositoryTestCase {
@@ -109,14 +116,14 @@ public class CheckoutTest extends CLIRepositoryTestCase {
assertStringArrayEquals(
"fatal: A branch named 'master' already exists.",
- execute("git checkout -b master"));
+ executeUnchecked("git checkout -b master"));
}
}
@Test
public void testCheckoutNewBranchOnBranchToBeBorn() throws Exception {
assertStringArrayEquals("fatal: You are on a branch yet to be born",
- execute("git checkout -b side"));
+ executeUnchecked("git checkout -b side"));
}
@Test
@@ -599,4 +606,34 @@ public class CheckoutTest extends CLIRepositoryTestCase {
assertEquals("Hello world b", read(b));
}
}
+
+ @Test
+ public void testCheckouSingleFile() throws Exception {
+ try (Git git = new Git(db)) {
+ File a = writeTrashFile("a", "file a");
+ git.add().addFilepattern(".").call();
+ git.commit().setMessage("commit file a").call();
+ writeTrashFile("a", "b");
+ assertEquals("b", read(a));
+ assertEquals("[]", Arrays.toString(execute("git checkout -- a")));
+ assertEquals("file a", read(a));
+ }
+ }
+
+ @Test
+ public void testCheckoutLink() throws Exception {
+ Assume.assumeTrue(FS.DETECTED.supportsSymlinks());
+ try (Git git = new Git(db)) {
+ Path path = writeLink("a", "link_a");
+ assertTrue(Files.isSymbolicLink(path));
+ git.add().addFilepattern(".").call();
+ git.commit().setMessage("commit link a").call();
+ deleteTrashFile("a");
+ writeTrashFile("a", "Hello world a");
+ assertFalse(Files.isSymbolicLink(path));
+ assertEquals("[]", Arrays.toString(execute("git checkout -- a")));
+ assertEquals("link_a", FileUtils.readSymLink(path.toFile()));
+ assertTrue(Files.isSymbolicLink(path));
+ }
+ }
}
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CommitTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CommitTest.java
new file mode 100644
index 0000000000..6bccb6d4a7
--- /dev/null
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CommitTest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2015, Andrey Loskutov <loskutov@gmx.de>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.pgm;
+
+import static org.junit.Assert.assertEquals;
+
+import org.eclipse.jgit.lib.CLIRepositoryTestCase;
+import org.junit.Test;
+
+public class CommitTest extends CLIRepositoryTestCase {
+
+ @Test
+ public void testCommitPath() throws Exception {
+ writeTrashFile("a", "a");
+ writeTrashFile("b", "a");
+ String result = toString(execute("git add a"));
+ assertEquals("", result);
+
+ result = toString(execute("git status -- a"));
+ assertEquals(toString("On branch master", "Changes to be committed:",
+ "new file: a"), result);
+
+ result = toString(execute("git status -- b"));
+ assertEquals(toString("On branch master", "Untracked files:", "b"),
+ result);
+
+ result = toString(execute("git commit a -m 'added a'"));
+ assertEquals(
+ "[master 8cb3ef7e5171aaee1792df6302a5a0cd30425f7a] added a",
+ result);
+
+ result = toString(execute("git status -- a"));
+ assertEquals("On branch master", result);
+
+ result = toString(execute("git status -- b"));
+ assertEquals(toString("On branch master", "Untracked files:", "b"),
+ result);
+ }
+
+ @Test
+ public void testCommitAll() throws Exception {
+ writeTrashFile("a", "a");
+ writeTrashFile("b", "a");
+ String result = toString(execute("git add a b"));
+ assertEquals("", result);
+
+ result = toString(execute("git status -- a b"));
+ assertEquals(toString("On branch master", "Changes to be committed:",
+ "new file: a", "new file: b"), result);
+
+ result = toString(execute("git commit -m 'added a b'"));
+ assertEquals(
+ "[master 3c93fa8e3a28ee26690498be78016edcb3a38c73] added a b",
+ result);
+
+ result = toString(execute("git status -- a b"));
+ assertEquals("On branch master", result);
+ }
+
+}
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DescribeTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DescribeTest.java
index 6352a26524..086e72e9a4 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DescribeTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DescribeTest.java
@@ -43,9 +43,15 @@
package org.eclipse.jgit.pgm;
import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Arrays;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.lib.CLIRepositoryTestCase;
+import org.eclipse.jgit.pgm.internal.CLIText;
import org.junit.Before;
import org.junit.Test;
@@ -67,17 +73,15 @@ public class DescribeTest extends CLIRepositoryTestCase {
@Test
public void testNoHead() throws Exception {
- assertArrayEquals(
- new String[] { "fatal: No names found, cannot describe anything." },
- execute("git describe"));
+ assertEquals(CLIText.fatalError(CLIText.get().noNamesFound),
+ toString(executeUnchecked("git describe")));
}
@Test
public void testHeadNoTag() throws Exception {
git.commit().setMessage("initial commit").call();
- assertArrayEquals(
- new String[] { "fatal: No names found, cannot describe anything." },
- execute("git describe"));
+ assertEquals(CLIText.fatalError(CLIText.get().noNamesFound),
+ toString(executeUnchecked("git describe")));
}
@Test
@@ -103,4 +107,22 @@ public class DescribeTest extends CLIRepositoryTestCase {
assertArrayEquals(new String[] { "v1.0-0-g6fd41be", "" },
execute("git describe --long HEAD"));
}
+
+ @Test
+ public void testHelpArgumentBeforeUnknown() throws Exception {
+ String[] output = execute("git describe -h -XYZ");
+ String all = Arrays.toString(output);
+ assertTrue("Unexpected help output: " + all,
+ all.contains("jgit describe"));
+ assertFalse("Unexpected help output: " + all, all.contains("fatal"));
+ }
+
+ @Test
+ public void testHelpArgumentAfterUnknown() throws Exception {
+ String[] output = executeUnchecked("git describe -XYZ -h");
+ String all = Arrays.toString(output);
+ assertTrue("Unexpected help output: " + all,
+ all.contains("jgit describe"));
+ assertTrue("Unexpected help output: " + all, all.contains("fatal"));
+ }
}
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeTest.java
index 975e8c4f76..47199016d4 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeTest.java
@@ -50,6 +50,7 @@ import java.util.Iterator;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.lib.CLIRepositoryTestCase;
import org.eclipse.jgit.merge.MergeStrategy;
+import org.eclipse.jgit.pgm.internal.CLIText;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Before;
import org.junit.Test;
@@ -194,8 +195,9 @@ public class MergeTest extends CLIRepositoryTestCase {
@Test
public void testNoFastForwardAndSquash() throws Exception {
- assertEquals("fatal: You cannot combine --squash with --no-ff.",
- execute("git merge master --no-ff --squash")[0]);
+ assertEquals(
+ CLIText.fatalError(CLIText.get().cannotCombineSquashWithNoff),
+ executeUnchecked("git merge master --no-ff --squash")[0]);
}
@Test
@@ -209,8 +211,8 @@ public class MergeTest extends CLIRepositoryTestCase {
git.add().addFilepattern("file").call();
git.commit().setMessage("commit#2").call();
- assertEquals("fatal: Not possible to fast-forward, aborting.",
- execute("git merge master --ff-only")[0]);
+ assertEquals(CLIText.fatalError(CLIText.get().ffNotPossibleAborting),
+ executeUnchecked("git merge master --ff-only")[0]);
}
@Test
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/RepoTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/RepoTest.java
index 90efae286b..0eee771d2c 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/RepoTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/RepoTest.java
@@ -44,8 +44,11 @@ package org.eclipse.jgit.pgm;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
import java.io.File;
+import java.util.Arrays;
+
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.junit.JGitTestUtil;
import org.eclipse.jgit.lib.CLIRepositoryTestCase;
@@ -98,6 +101,31 @@ public class RepoTest extends CLIRepositoryTestCase {
}
@Test
+ public void testMissingPath() throws Exception {
+ try {
+ execute("git repo");
+ fail("Must die");
+ } catch (Die e) {
+ // expected, requires argument
+ }
+ }
+
+ /**
+ * See bug 484951: "git repo -h" should not print unexpected values
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testZombieHelpArgument() throws Exception {
+ String[] output = execute("git repo -h");
+ String all = Arrays.toString(output);
+ assertTrue("Unexpected help output: " + all,
+ all.contains("jgit repo"));
+ assertFalse("Unexpected help output: " + all,
+ all.contains("jgit repo VAL"));
+ }
+
+ @Test
public void testAddRepoManifest() throws Exception {
StringBuilder xmlContent = new StringBuilder();
xmlContent.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ResetTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ResetTest.java
index dae477928b..16c5889c48 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ResetTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ResetTest.java
@@ -48,6 +48,7 @@ import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.lib.CLIRepositoryTestCase;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Test;
public class ResetTest extends CLIRepositoryTestCase {
@@ -62,6 +63,20 @@ public class ResetTest extends CLIRepositoryTestCase {
}
@Test
+ public void testPathOptionHelp() throws Exception {
+ String[] result = execute("git reset -h");
+ assertTrue("Unexpected argument: " + result[1],
+ result[1].endsWith("[-- path ... ...]"));
+ }
+
+ @Test
+ public void testZombieArgument_Bug484951() throws Exception {
+ String[] result = execute("git reset -h");
+ assertFalse("Unexpected argument: " + result[0],
+ result[0].contains("[VAL ...]"));
+ }
+
+ @Test
public void testResetSelf() throws Exception {
RevCommit commit = git.commit().setMessage("initial commit").call();
assertStringArrayEquals("",
@@ -91,15 +106,28 @@ public class ResetTest extends CLIRepositoryTestCase {
@Test
public void testResetPathDoubleDash() throws Exception {
- resetPath(true);
+ resetPath(true, true);
}
@Test
public void testResetPathNoDoubleDash() throws Exception {
- resetPath(false);
+ resetPath(false, true);
+ }
+
+ @Test
+ public void testResetPathDoubleDashNoRef() throws Exception {
+ resetPath(true, false);
+ }
+
+ @Ignore("Currently we cannote recognize if a name is a commit-ish or a path, "
+ + "so 'git reset a' will not work if 'a' is not a branch name but a file path")
+ @Test
+ public void testResetPathNoDoubleDashNoRef() throws Exception {
+ resetPath(false, false);
}
- private void resetPath(boolean useDoubleDash) throws Exception {
+ private void resetPath(boolean useDoubleDash, boolean supplyCommit)
+ throws Exception {
// create files a and b
writeTrashFile("a", "Hello world a");
writeTrashFile("b", "Hello world b");
@@ -115,8 +143,9 @@ public class ResetTest extends CLIRepositoryTestCase {
git.add().addFilepattern(".").call();
// reset only file a
- String cmd = String.format("git reset %s%s a", commit.getId().name(),
- (useDoubleDash) ? " --" : "");
+ String cmd = String.format("git reset %s%s a",
+ supplyCommit ? commit.getId().name() : "",
+ useDoubleDash ? " --" : "");
assertStringArrayEquals("", execute(cmd));
assertEquals(commit.getId(),
git.getRepository().exactRef("HEAD").getObjectId());
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/StatusTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/StatusTest.java
index 854c52d88b..368047c602 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/StatusTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/StatusTest.java
@@ -44,6 +44,7 @@ package org.eclipse.jgit.pgm;
import static org.eclipse.jgit.lib.Constants.MASTER;
import static org.eclipse.jgit.lib.Constants.R_HEADS;
+import static org.junit.Assert.assertTrue;
import java.io.IOException;
@@ -56,6 +57,13 @@ import org.junit.Test;
public class StatusTest extends CLIRepositoryTestCase {
@Test
+ public void testPathOptionHelp() throws Exception {
+ String[] result = execute("git status -h");
+ assertTrue("Unexpected argument: " + result[1],
+ result[1].endsWith("[-- path ... ...]"));
+ }
+
+ @Test
public void testStatusDefault() throws Exception {
executeTest("git status", false, true);
}
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/TagTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/TagTest.java
index ab09db5a56..0fe25f550a 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/TagTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/TagTest.java
@@ -68,6 +68,6 @@ public class TagTest extends CLIRepositoryTestCase {
git.commit().setMessage("commit").call();
assertEquals("fatal: tag 'test' already exists",
- execute("git tag test")[0]);
+ executeUnchecked("git tag test")[0]);
}
}
diff --git a/org.eclipse.jgit.pgm/BUCK b/org.eclipse.jgit.pgm/BUCK
new file mode 100644
index 0000000000..edcf2fc28f
--- /dev/null
+++ b/org.eclipse.jgit.pgm/BUCK
@@ -0,0 +1,70 @@
+include_defs('//tools/git.defs')
+
+java_library(
+ name = 'pgm',
+ srcs = glob(['src/**']),
+ resources = glob(['resources/**']),
+ deps = [
+ ':services',
+ '//org.eclipse.jgit:jgit',
+ '//org.eclipse.jgit.archive:jgit-archive',
+ '//org.eclipse.jgit.http.apache:http-apache',
+ '//org.eclipse.jgit.ui:ui',
+ '//lib:args4j',
+ ],
+ visibility = ['PUBLIC'],
+)
+
+prebuilt_jar(
+ name = 'services',
+ binary_jar = ':services__jar',
+)
+
+genrule(
+ name = 'services__jar',
+ cmd = 'cd $SRCDIR ; zip -qr $OUT .',
+ srcs = glob(['META-INF/services/*']),
+ out = 'services.jar',
+)
+
+genrule(
+ name = 'jgit',
+ cmd = ''.join([
+ 'mkdir $TMP/META-INF &&',
+ 'cp $(location :binary_manifest) $TMP/META-INF/MANIFEST.MF &&',
+ 'cp $(location :jgit_jar) $TMP/jgit.jar &&',
+ 'cd $TMP && zip $TMP/jgit.jar META-INF/MANIFEST.MF &&',
+ 'cat $SRCDIR/jgit.sh $TMP/jgit.jar >$OUT &&',
+ 'chmod a+x $OUT',
+ ]),
+ srcs = ['jgit.sh'],
+ out = 'jgit',
+ visibility = ['PUBLIC'],
+)
+
+java_binary(
+ name = 'jgit_jar',
+ deps = [
+ ':pgm',
+ '//lib:slf4j-simple',
+ '//lib:tukaani-xz',
+ ],
+ blacklist = [
+ 'META-INF/DEPENDENCIES',
+ 'META-INF/maven/.*',
+ ],
+)
+
+genrule(
+ name = 'binary_manifest',
+ cmd = ';'.join(['echo "%s: %s" >>$OUT' % e for e in [
+ ('Manifest-Version', '1.0'),
+ ('Main-Class', 'org.eclipse.jgit.pgm.Main'),
+ ('Bundle-Version', git_version()),
+ ('Implementation-Title', 'JGit Command Line Interface'),
+ ('Implementation-Vendor', 'Eclipse.org - JGit'),
+ ('Implementation-Vendor-URL', 'http://www.eclipse.org/jgit/'),
+ ('Implementation-Vendor-Id', 'org.eclipse.jgit'),
+ ]] + ['echo >>$OUT']),
+ out = 'MANIFEST.MF',
+)
diff --git a/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF b/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF
index 567fd05750..bc9205c7ce 100644
--- a/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF
@@ -19,8 +19,10 @@ Import-Package: org.apache.commons.compress.archivers;version="[1.3,2.0)",
org.eclipse.jgit.dircache;version="[4.2.0,4.3.0)",
org.eclipse.jgit.errors;version="[4.2.0,4.3.0)",
org.eclipse.jgit.gitrepo;version="[4.2.0,4.3.0)",
+ org.eclipse.jgit.internal.ketch;version="[4.2.0,4.3.0)",
org.eclipse.jgit.internal.storage.file;version="[4.2.0,4.3.0)",
org.eclipse.jgit.internal.storage.pack;version="[4.2.0,4.3.0)",
+ org.eclipse.jgit.internal.storage.reftree;version="[4.2.0,4.3.0)",
org.eclipse.jgit.lib;version="[4.2.0,4.3.0)",
org.eclipse.jgit.merge;version="4.2.0",
org.eclipse.jgit.nls;version="[4.2.0,4.3.0)",
@@ -31,6 +33,7 @@ Import-Package: org.apache.commons.compress.archivers;version="[1.3,2.0)",
org.eclipse.jgit.storage.file;version="[4.2.0,4.3.0)",
org.eclipse.jgit.storage.pack;version="[4.2.0,4.3.0)",
org.eclipse.jgit.transport;version="[4.2.0,4.3.0)",
+ org.eclipse.jgit.transport.http.apache;version="[4.2.0,4.3.0)",
org.eclipse.jgit.transport.resolver;version="[4.2.0,4.3.0)",
org.eclipse.jgit.treewalk;version="[4.2.0,4.3.0)",
org.eclipse.jgit.treewalk.filter;version="[4.2.0,4.3.0)",
diff --git a/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin b/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin
index c13f63e80f..6aa20041b5 100644
--- a/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin
+++ b/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin
@@ -41,6 +41,7 @@ org.eclipse.jgit.pgm.debug.DiffAlgorithms
org.eclipse.jgit.pgm.debug.MakeCacheTree
org.eclipse.jgit.pgm.debug.ReadDirCache
org.eclipse.jgit.pgm.debug.RebuildCommitGraph
+org.eclipse.jgit.pgm.debug.RebuildRefTree
org.eclipse.jgit.pgm.debug.ShowCacheTree
org.eclipse.jgit.pgm.debug.ShowCommands
org.eclipse.jgit.pgm.debug.ShowDirCache
diff --git a/org.eclipse.jgit.pgm/pom.xml b/org.eclipse.jgit.pgm/pom.xml
index ca2ead2925..2642491321 100644
--- a/org.eclipse.jgit.pgm/pom.xml
+++ b/org.eclipse.jgit.pgm/pom.xml
@@ -95,6 +95,17 @@
</dependency>
<dependency>
+ <groupId>org.eclipse.jgit</groupId>
+ <artifactId>org.eclipse.jgit.http.apache</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpclient</artifactId>
+ </dependency>
+
+ <dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j-version}</version>
diff --git a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
index 335336da28..b4b1261b37 100644
--- a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
+++ b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
@@ -20,6 +20,7 @@ branchAlreadyExists=A branch named ''{0}'' already exists.
branchCreatedFrom=branch: Created from {0}
branchDetachedHEAD=detached HEAD
branchIsNotAnAncestorOfYourCurrentHEAD=The branch ''{0}'' is not an ancestor of your current HEAD.\nIf you are sure you want to delete it, run ''jgit branch -D {0}''.
+branchNameRequired=branch name required
branchNotFound=branch ''{0}'' not found.
cacheTreePathInfo="{0}": {1} entries, {2} children
cannotBeRenamed={0} cannot be renamed
@@ -89,7 +90,9 @@ metaVar_author=AUTHOR
metaVar_base=base
metaVar_blameL=START,END
metaVar_blameReverse=START..END
+metaVar_branchAndStartPoint=branch [start-name]
metaVar_branchName=branch
+metaVar_branchNames=branch ...
metaVar_bucket=BUCKET
metaVar_command=command
metaVar_commandDetail=DETAIL
@@ -109,6 +112,7 @@ metaVar_message=message
metaVar_n=n
metaVar_name=name
metaVar_object=object
+metaVar_oldNewBranchNames=[oldbranch] newbranch
metaVar_op=OP
metaVar_pass=PASS
metaVar_path=path
@@ -125,6 +129,7 @@ metaVar_treeish=tree-ish
metaVar_uriish=uri-ish
metaVar_url=URL
metaVar_user=USER
+metaVar_values=value ...
metaVar_version=VERSION
mostCommonlyUsedCommandsAre=The most commonly used commands are:
needApprovalToDestroyCurrentRepository=Need approval to destroy current repository
@@ -223,6 +228,7 @@ usage_MergeBase=Find as good common ancestors as possible for a merge
usage_MergesTwoDevelopmentHistories=Merges two development histories
usage_ReadDirCache= Read the DirCache 100 times
usage_RebuildCommitGraph=Recreate a repository from another one's commit graph
+usage_RebuildRefTree=Copy references into a RefTree
usage_Remote=Manage set of tracked repositories
usage_RepositoryToReadFrom=Repository to read from
usage_RepositoryToReceiveInto=Repository to receive into
@@ -337,6 +343,7 @@ usage_recordChangesToRepository=Record changes to the repository
usage_recurseIntoSubtrees=recurse into subtrees
usage_renameLimit=limit size of rename matrix
usage_reset=Reset current HEAD to the specified state
+usage_resetReference=Reset to given reference name
usage_resetHard=Resets the index and working tree
usage_resetSoft=Resets without touching the index file nor the working tree
usage_resetMixed=Resets the index but not the working tree
@@ -353,6 +360,7 @@ usage_tags=fetch all tags
usage_notags=do not fetch tags
usage_tagMessage=tag message
usage_untrackedFilesMode=show untracked files
+usage_updateRef=reference to update
usage_updateRemoteRefsFromAnotherRepository=Update remote refs from another repository
usage_useNameInsteadOfOriginToTrackUpstream=use <name> instead of 'origin' to track upstream
usage_checkoutBranchAfterClone=checkout named branch instead of remotes's HEAD
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Branch.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Branch.java
index 65aa24f356..045f3571e5 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Branch.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Branch.java
@@ -45,7 +45,6 @@ package org.eclipse.jgit.pgm;
import java.io.IOException;
import java.text.MessageFormat;
-import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
@@ -65,15 +64,18 @@ import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.RefUpdate.Result;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.pgm.internal.CLIText;
-import org.eclipse.jgit.pgm.opt.CmdLineParser;
+import org.eclipse.jgit.pgm.opt.OptionWithValuesListHandler;
import org.eclipse.jgit.revwalk.RevWalk;
import org.kohsuke.args4j.Argument;
-import org.kohsuke.args4j.ExampleMode;
import org.kohsuke.args4j.Option;
@Command(common = true, usage = "usage_listCreateOrDeleteBranches")
class Branch extends TextBuiltin {
+ private String otherBranch;
+ private boolean createForce;
+ private boolean rename;
+
@Option(name = "--remote", aliases = { "-r" }, usage = "usage_actOnRemoteTrackingBranches")
private boolean remote = false;
@@ -83,23 +85,69 @@ class Branch extends TextBuiltin {
@Option(name = "--contains", metaVar = "metaVar_commitish", usage = "usage_printOnlyBranchesThatContainTheCommit")
private String containsCommitish;
- @Option(name = "--delete", aliases = { "-d" }, usage = "usage_deleteFullyMergedBranch")
- private boolean delete = false;
+ private List<String> delete;
- @Option(name = "--delete-force", aliases = { "-D" }, usage = "usage_deleteBranchEvenIfNotMerged")
- private boolean deleteForce = false;
+ @Option(name = "--delete", aliases = {
+ "-d" }, metaVar = "metaVar_branchNames", usage = "usage_deleteFullyMergedBranch", handler = OptionWithValuesListHandler.class)
+ public void delete(List<String> names) {
+ if (names.isEmpty()) {
+ throw die(CLIText.get().branchNameRequired);
+ }
+ delete = names;
+ }
- @Option(name = "--create-force", aliases = { "-f" }, usage = "usage_forceCreateBranchEvenExists")
- private boolean createForce = false;
+ private List<String> deleteForce;
- @Option(name = "-m", usage = "usage_moveRenameABranch")
- private boolean rename = false;
+ @Option(name = "--delete-force", aliases = {
+ "-D" }, metaVar = "metaVar_branchNames", usage = "usage_deleteBranchEvenIfNotMerged", handler = OptionWithValuesListHandler.class)
+ public void deleteForce(List<String> names) {
+ if (names.isEmpty()) {
+ throw die(CLIText.get().branchNameRequired);
+ }
+ deleteForce = names;
+ }
+
+ @Option(name = "--create-force", aliases = {
+ "-f" }, metaVar = "metaVar_branchAndStartPoint", usage = "usage_forceCreateBranchEvenExists", handler = OptionWithValuesListHandler.class)
+ public void createForce(List<String> branchAndStartPoint) {
+ createForce = true;
+ if (branchAndStartPoint.isEmpty()) {
+ throw die(CLIText.get().branchNameRequired);
+ }
+ if (branchAndStartPoint.size() > 2) {
+ throw die(CLIText.get().tooManyRefsGiven);
+ }
+ if (branchAndStartPoint.size() == 1) {
+ branch = branchAndStartPoint.get(0);
+ } else {
+ branch = branchAndStartPoint.get(0);
+ otherBranch = branchAndStartPoint.get(1);
+ }
+ }
+
+ @Option(name = "--move", aliases = {
+ "-m" }, metaVar = "metaVar_oldNewBranchNames", usage = "usage_moveRenameABranch", handler = OptionWithValuesListHandler.class)
+ public void moveRename(List<String> currentAndNew) {
+ rename = true;
+ if (currentAndNew.isEmpty()) {
+ throw die(CLIText.get().branchNameRequired);
+ }
+ if (currentAndNew.size() > 2) {
+ throw die(CLIText.get().tooManyRefsGiven);
+ }
+ if (currentAndNew.size() == 1) {
+ branch = currentAndNew.get(0);
+ } else {
+ branch = currentAndNew.get(0);
+ otherBranch = currentAndNew.get(1);
+ }
+ }
@Option(name = "--verbose", aliases = { "-v" }, usage = "usage_beVerbose")
private boolean verbose = false;
- @Argument
- private List<String> branches = new ArrayList<String>();
+ @Argument(metaVar = "metaVar_name")
+ private String branch;
private final Map<String, Ref> printRefs = new LinkedHashMap<String, Ref>();
@@ -110,30 +158,33 @@ class Branch extends TextBuiltin {
@Override
protected void run() throws Exception {
- if (delete || deleteForce)
- delete(deleteForce);
- else {
- if (branches.size() > 2)
- throw die(CLIText.get().tooManyRefsGiven + new CmdLineParser(this).printExample(ExampleMode.ALL));
-
+ if (delete != null || deleteForce != null) {
+ if (delete != null) {
+ delete(delete, false);
+ }
+ if (deleteForce != null) {
+ delete(deleteForce, true);
+ }
+ } else {
if (rename) {
String src, dst;
- if (branches.size() == 1) {
+ if (otherBranch == null) {
final Ref head = db.getRef(Constants.HEAD);
- if (head != null && head.isSymbolic())
+ if (head != null && head.isSymbolic()) {
src = head.getLeaf().getName();
- else
+ } else {
throw die(CLIText.get().cannotRenameDetachedHEAD);
- dst = branches.get(0);
+ }
+ dst = branch;
} else {
- src = branches.get(0);
+ src = branch;
final Ref old = db.getRef(src);
if (old == null)
throw die(MessageFormat.format(CLIText.get().doesNotExist, src));
if (!old.getName().startsWith(Constants.R_HEADS))
throw die(MessageFormat.format(CLIText.get().notABranch, src));
src = old.getName();
- dst = branches.get(1);
+ dst = otherBranch;
}
if (!dst.startsWith(Constants.R_HEADS))
@@ -145,13 +196,14 @@ class Branch extends TextBuiltin {
if (r.rename() != Result.RENAMED)
throw die(MessageFormat.format(CLIText.get().cannotBeRenamed, src));
- } else if (branches.size() > 0) {
- String newHead = branches.get(0);
+ } else if (createForce || branch != null) {
+ String newHead = branch;
String startBranch;
- if (branches.size() == 2)
- startBranch = branches.get(1);
- else
+ if (createForce) {
+ startBranch = otherBranch;
+ } else {
startBranch = Constants.HEAD;
+ }
Ref startRef = db.getRef(startBranch);
ObjectId startAt = db.resolve(startBranch + "^0"); //$NON-NLS-1$
if (startRef != null) {
@@ -164,22 +216,27 @@ class Branch extends TextBuiltin {
}
startBranch = Repository.shortenRefName(startBranch);
String newRefName = newHead;
- if (!newRefName.startsWith(Constants.R_HEADS))
+ if (!newRefName.startsWith(Constants.R_HEADS)) {
newRefName = Constants.R_HEADS + newRefName;
- if (!Repository.isValidRefName(newRefName))
+ }
+ if (!Repository.isValidRefName(newRefName)) {
throw die(MessageFormat.format(CLIText.get().notAValidRefName, newRefName));
- if (!createForce && db.resolve(newRefName) != null)
+ }
+ if (!createForce && db.resolve(newRefName) != null) {
throw die(MessageFormat.format(CLIText.get().branchAlreadyExists, newHead));
+ }
RefUpdate updateRef = db.updateRef(newRefName);
updateRef.setNewObjectId(startAt);
updateRef.setForceUpdate(createForce);
updateRef.setRefLogMessage(MessageFormat.format(CLIText.get().branchCreatedFrom, startBranch), false);
Result update = updateRef.update();
- if (update == Result.REJECTED)
+ if (update == Result.REJECTED) {
throw die(MessageFormat.format(CLIText.get().couldNotCreateBranch, newHead, update.toString()));
+ }
} else {
- if (verbose)
+ if (verbose) {
rw = new RevWalk(db);
+ }
list();
}
}
@@ -249,7 +306,8 @@ class Branch extends TextBuiltin {
outw.println();
}
- private void delete(boolean force) throws IOException {
+ private void delete(List<String> branches, boolean force)
+ throws IOException {
String current = db.getBranch();
ObjectId head = db.resolve(Constants.HEAD);
for (String branch : branches) {
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Checkout.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Checkout.java
index 45794629ec..94517dbf2f 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Checkout.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Checkout.java
@@ -60,7 +60,7 @@ import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.pgm.internal.CLIText;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
-import org.kohsuke.args4j.spi.StopOptionHandler;
+import org.kohsuke.args4j.spi.RestOfArgumentsHandler;
@Command(common = true, usage = "usage_checkout")
class Checkout extends TextBuiltin {
@@ -74,11 +74,10 @@ class Checkout extends TextBuiltin {
@Option(name = "--orphan", usage = "usage_orphan")
private boolean orphan = false;
- @Argument(required = true, index = 0, metaVar = "metaVar_name", usage = "usage_checkout")
+ @Argument(required = false, index = 0, metaVar = "metaVar_name", usage = "usage_checkout")
private String name;
- @Argument(index = 1)
- @Option(name = "--", metaVar = "metaVar_paths", multiValued = true, handler = StopOptionHandler.class)
+ @Option(name = "--", metaVar = "metaVar_paths", multiValued = true, handler = RestOfArgumentsHandler.class)
private List<String> paths = new ArrayList<String>();
@Override
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Clone.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Clone.java
index cd6953cb05..04078287fb 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Clone.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Clone.java
@@ -50,6 +50,7 @@ import org.eclipse.jgit.api.CloneCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.InvalidRemoteException;
import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.TextProgressMonitor;
import org.eclipse.jgit.pgm.internal.CLIText;
import org.eclipse.jgit.transport.URIish;
import org.eclipse.jgit.util.SystemReader;
@@ -70,6 +71,9 @@ class Clone extends AbstractFetchCommand {
@Option(name = "--bare", usage = "usage_bareClone")
private boolean isBare;
+ @Option(name = "--quiet", usage = "usage_quiet")
+ private Boolean quiet;
+
@Argument(index = 0, required = true, metaVar = "metaVar_uriish")
private String sourceUri;
@@ -109,10 +113,16 @@ class Clone extends AbstractFetchCommand {
command.setGitDir(gitdir == null ? null : new File(gitdir));
command.setDirectory(localNameF);
- outw.println(MessageFormat.format(CLIText.get().cloningInto, localName));
+ boolean msgs = quiet == null || !quiet.booleanValue();
+ if (msgs) {
+ command.setProgressMonitor(new TextProgressMonitor(errw));
+ outw.println(MessageFormat.format(
+ CLIText.get().cloningInto, localName));
+ outw.flush();
+ }
try {
db = command.call().getRepository();
- if (db.resolve(Constants.HEAD) == null)
+ if (msgs && db.resolve(Constants.HEAD) == null)
outw.println(CLIText.get().clonedEmptyRepository);
} catch (InvalidRemoteException e) {
throw die(MessageFormat.format(CLIText.get().doesNotExist,
@@ -121,8 +131,9 @@ class Clone extends AbstractFetchCommand {
if (db != null)
db.close();
}
-
- outw.println();
- outw.flush();
+ if (msgs) {
+ outw.println();
+ outw.flush();
+ }
}
}
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Daemon.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Daemon.java
index 04182d6dbe..03f3fac0b6 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Daemon.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Daemon.java
@@ -45,18 +45,29 @@ package org.eclipse.jgit.pgm;
import java.io.File;
import java.net.InetSocketAddress;
+import java.net.URISyntaxException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
+import org.eclipse.jgit.internal.ketch.KetchLeader;
+import org.eclipse.jgit.internal.ketch.KetchLeaderCache;
+import org.eclipse.jgit.internal.ketch.KetchPreReceive;
+import org.eclipse.jgit.internal.ketch.KetchSystem;
+import org.eclipse.jgit.internal.ketch.KetchText;
+import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.pgm.internal.CLIText;
import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.eclipse.jgit.storage.file.WindowCacheConfig;
import org.eclipse.jgit.storage.pack.PackConfig;
import org.eclipse.jgit.transport.DaemonClient;
import org.eclipse.jgit.transport.DaemonService;
+import org.eclipse.jgit.transport.ReceivePack;
import org.eclipse.jgit.transport.resolver.FileResolver;
+import org.eclipse.jgit.transport.resolver.ReceivePackFactory;
+import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException;
+import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
import org.eclipse.jgit.util.FS;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
@@ -90,6 +101,13 @@ class Daemon extends TextBuiltin {
@Option(name = "--export-all", usage = "usage_exportWithoutGitDaemonExportOk")
boolean exportAll;
+ @Option(name = "--ketch")
+ KetchServerType ketchServerType;
+
+ enum KetchServerType {
+ LEADER;
+ }
+
@Argument(required = true, metaVar = "metaVar_directory", usage = "usage_directoriesToExport")
final List<File> directory = new ArrayList<File>();
@@ -146,7 +164,9 @@ class Daemon extends TextBuiltin {
service(d, n).setOverridable(true);
for (final String n : forbidOverride)
service(d, n).setOverridable(false);
-
+ if (ketchServerType == KetchServerType.LEADER) {
+ startKetchLeader(d);
+ }
d.start();
outw.println(MessageFormat.format(CLIText.get().listeningOn, d.getAddress()));
}
@@ -159,4 +179,29 @@ class Daemon extends TextBuiltin {
throw die(MessageFormat.format(CLIText.get().serviceNotSupported, n));
return svc;
}
+
+ private void startKetchLeader(org.eclipse.jgit.transport.Daemon daemon) {
+ KetchSystem system = new KetchSystem();
+ final KetchLeaderCache leaders = new KetchLeaderCache(system);
+ final ReceivePackFactory<DaemonClient> factory;
+
+ factory = daemon.getReceivePackFactory();
+ daemon.setReceivePackFactory(new ReceivePackFactory<DaemonClient>() {
+ @Override
+ public ReceivePack create(DaemonClient req, Repository repo)
+ throws ServiceNotEnabledException,
+ ServiceNotAuthorizedException {
+ ReceivePack rp = factory.create(req, repo);
+ KetchLeader leader;
+ try {
+ leader = leaders.get(repo);
+ } catch (URISyntaxException err) {
+ throw new ServiceNotEnabledException(
+ KetchText.get().invalidFollowerUri, err);
+ }
+ rp.setPreReceiveHook(new KetchPreReceive(leader));
+ return rp;
+ }
+ });
+ }
} \ No newline at end of file
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Die.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Die.java
index f07df1a4b5..a25f1e9305 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Die.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Die.java
@@ -86,6 +86,21 @@ public class Die extends RuntimeException {
* @since 3.4
*/
public Die(boolean aborted) {
+ this(aborted, null);
+ }
+
+ /**
+ * Construct a new exception reflecting the fact that the command execution
+ * has been aborted before running.
+ *
+ * @param aborted
+ * boolean indicating the fact the execution has been aborted
+ * @param cause
+ * can be null
+ * @since 4.2
+ */
+ public Die(boolean aborted, final Throwable cause) {
+ super(cause != null ? cause.getMessage() : null, cause);
this.aborted = aborted;
}
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Main.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Main.java
index ceb0d6b2fe..d701f22c38 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Main.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Main.java
@@ -62,6 +62,8 @@ import org.eclipse.jgit.lib.RepositoryBuilder;
import org.eclipse.jgit.pgm.internal.CLIText;
import org.eclipse.jgit.pgm.opt.CmdLineParser;
import org.eclipse.jgit.pgm.opt.SubcommandHandler;
+import org.eclipse.jgit.transport.HttpTransport;
+import org.eclipse.jgit.transport.http.apache.HttpClientConnectionFactory;
import org.eclipse.jgit.util.CachedAuthenticator;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.CmdLineException;
@@ -88,13 +90,23 @@ public class Main {
@Argument(index = 1, metaVar = "metaVar_arg")
private List<String> arguments = new ArrayList<String>();
+ PrintWriter writer;
+
+ /**
+ *
+ */
+ public Main() {
+ HttpTransport.setConnectionFactory(new HttpClientConnectionFactory());
+ }
+
/**
* Execute the command line.
*
* @param argv
* arguments.
+ * @throws Exception
*/
- public static void main(final String[] argv) {
+ public static void main(final String[] argv) throws Exception {
new Main().run(argv);
}
@@ -113,8 +125,10 @@ public class Main {
*
* @param argv
* arguments.
+ * @throws Exception
*/
- protected void run(final String[] argv) {
+ protected void run(final String[] argv) throws Exception {
+ writer = createErrorWriter();
try {
if (!installConsole()) {
AwtAuthenticator.install();
@@ -123,12 +137,14 @@ public class Main {
configureHttpProxy();
execute(argv);
} catch (Die err) {
- if (err.isAborted())
- System.exit(1);
- System.err.println(MessageFormat.format(CLIText.get().fatalError, err.getMessage()));
- if (showStackTrace)
- err.printStackTrace();
- System.exit(128);
+ if (err.isAborted()) {
+ exit(1, err);
+ }
+ writer.println(CLIText.fatalError(err.getMessage()));
+ if (showStackTrace) {
+ err.printStackTrace(writer);
+ }
+ exit(128, err);
} catch (Exception err) {
// Try to detect errno == EPIPE and exit normally if that happens
// There may be issues with operating system versions and locale,
@@ -136,46 +152,54 @@ public class Main {
// under other circumstances.
if (err.getClass() == IOException.class) {
// Linux, OS X
- if (err.getMessage().equals("Broken pipe")) //$NON-NLS-1$
- System.exit(0);
+ if (err.getMessage().equals("Broken pipe")) { //$NON-NLS-1$
+ exit(0, err);
+ }
// Windows
- if (err.getMessage().equals("The pipe is being closed")) //$NON-NLS-1$
- System.exit(0);
+ if (err.getMessage().equals("The pipe is being closed")) { //$NON-NLS-1$
+ exit(0, err);
+ }
}
if (!showStackTrace && err.getCause() != null
- && err instanceof TransportException)
- System.err.println(MessageFormat.format(CLIText.get().fatalError, err.getCause().getMessage()));
+ && err instanceof TransportException) {
+ writer.println(CLIText.fatalError(err.getCause().getMessage()));
+ }
if (err.getClass().getName().startsWith("org.eclipse.jgit.errors.")) { //$NON-NLS-1$
- System.err.println(MessageFormat.format(CLIText.get().fatalError, err.getMessage()));
- if (showStackTrace)
+ writer.println(CLIText.fatalError(err.getMessage()));
+ if (showStackTrace) {
err.printStackTrace();
- System.exit(128);
+ }
+ exit(128, err);
}
err.printStackTrace();
- System.exit(1);
+ exit(1, err);
}
if (System.out.checkError()) {
- System.err.println(CLIText.get().unknownIoErrorStdout);
- System.exit(1);
+ writer.println(CLIText.get().unknownIoErrorStdout);
+ exit(1, null);
}
- if (System.err.checkError()) {
+ if (writer.checkError()) {
// No idea how to present an error here, most likely disk full or
// broken pipe
- System.exit(1);
+ exit(1, null);
}
}
+ PrintWriter createErrorWriter() {
+ return new PrintWriter(System.err);
+ }
+
private void execute(final String[] argv) throws Exception {
- final CmdLineParser clp = new CmdLineParser(this);
- PrintWriter writer = new PrintWriter(System.err);
+ final CmdLineParser clp = new SubcommandLineParser(this);
+
try {
clp.parseArgument(argv);
} catch (CmdLineException err) {
if (argv.length > 0 && !help && !version) {
- writer.println(MessageFormat.format(CLIText.get().fatalError, err.getMessage()));
+ writer.println(CLIText.fatalError(err.getMessage()));
writer.flush();
- System.exit(1);
+ exit(1, err);
}
}
@@ -191,22 +215,24 @@ public class Main {
writer.println(CLIText.get().mostCommonlyUsedCommandsAre);
final CommandRef[] common = CommandCatalog.common();
int width = 0;
- for (final CommandRef c : common)
+ for (final CommandRef c : common) {
width = Math.max(width, c.getName().length());
+ }
width += 2;
for (final CommandRef c : common) {
writer.print(' ');
writer.print(c.getName());
- for (int i = c.getName().length(); i < width; i++)
+ for (int i = c.getName().length(); i < width; i++) {
writer.print(' ');
+ }
writer.print(CLIText.get().resourceBundle().getString(c.getUsage()));
writer.println();
}
writer.println();
}
writer.flush();
- System.exit(1);
+ exit(1, null);
}
if (version) {
@@ -215,21 +241,39 @@ public class Main {
}
final TextBuiltin cmd = subcommand;
- if (cmd.requiresRepository())
- cmd.init(openGitDir(gitdir), null);
- else
- cmd.init(null, gitdir);
+ init(cmd);
try {
cmd.execute(arguments.toArray(new String[arguments.size()]));
} finally {
- if (cmd.outw != null)
+ if (cmd.outw != null) {
cmd.outw.flush();
- if (cmd.errw != null)
+ }
+ if (cmd.errw != null) {
cmd.errw.flush();
+ }
+ }
+ }
+
+ void init(final TextBuiltin cmd) throws IOException {
+ if (cmd.requiresRepository()) {
+ cmd.init(openGitDir(gitdir), null);
+ } else {
+ cmd.init(null, gitdir);
}
}
/**
+ * @param status
+ * @param t
+ * can be {@code null}
+ * @throws Exception
+ */
+ void exit(int status, Exception t) throws Exception {
+ writer.flush();
+ System.exit(status);
+ }
+
+ /**
* Evaluate the {@code --git-dir} option and open the repository.
*
* @param aGitdir
@@ -278,7 +322,7 @@ public class Main {
throws IllegalAccessException, InvocationTargetException,
NoSuchMethodException, ClassNotFoundException {
try {
- Class.forName(name).getMethod("install").invoke(null); //$NON-NLS-1$
+ Class.forName(name).getMethod("install").invoke(null); //$NON-NLS-1$
} catch (InvocationTargetException e) {
if (e.getCause() instanceof RuntimeException)
throw (RuntimeException) e.getCause();
@@ -332,4 +376,19 @@ public class Main {
}
}
}
+
+ /**
+ * Parser for subcommands which doesn't stop parsing on help options and so
+ * proceeds all specified options
+ */
+ static class SubcommandLineParser extends CmdLineParser {
+ public SubcommandLineParser(Object bean) {
+ super(bean);
+ }
+
+ @Override
+ protected boolean containsHelp(String... args) {
+ return false;
+ }
+ }
}
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Merge.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Merge.java
index cd65af9549..e739b58ae7 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Merge.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Merge.java
@@ -148,9 +148,12 @@ class Merge extends TextBuiltin {
break;
case FAST_FORWARD:
ObjectId oldHeadId = oldHead.getObjectId();
- outw.println(MessageFormat.format(CLIText.get().updating, oldHeadId
- .abbreviate(7).name(), result.getNewHead().abbreviate(7)
- .name()));
+ if (oldHeadId != null) {
+ String oldId = oldHeadId.abbreviate(7).name();
+ String newId = result.getNewHead().abbreviate(7).name();
+ outw.println(MessageFormat.format(CLIText.get().updating, oldId,
+ newId));
+ }
outw.println(result.getMergeStatus().toString());
break;
case CHECKOUT_CONFLICT:
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Remote.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Remote.java
index 70868e920e..24916bd1c2 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Remote.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Remote.java
@@ -144,7 +144,7 @@ class Remote extends TextBuiltin {
}
@Override
- public void printUsageAndExit(final String message, final CmdLineParser clp)
+ public void printUsage(final String message, final CmdLineParser clp)
throws IOException {
errw.println(message);
errw.println("jgit remote [--verbose (-v)] [--help (-h)]"); //$NON-NLS-1$
@@ -160,7 +160,6 @@ class Remote extends TextBuiltin {
errw.println();
errw.flush();
- throw die(true);
}
private void print(List<RemoteConfig> remotes) throws IOException {
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Repo.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Repo.java
index db88008e10..ea59527fed 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Repo.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Repo.java
@@ -55,7 +55,7 @@ class Repo extends TextBuiltin {
@Option(name = "--groups", aliases = { "-g" }, usage = "usage_groups")
private String groups = "default"; //$NON-NLS-1$
- @Argument(required = true, usage = "usage_pathToXml")
+ @Argument(required = true, metaVar = "metaVar_path", usage = "usage_pathToXml")
private String path;
@Option(name = "--record-remote-branch", usage = "usage_branches")
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Reset.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Reset.java
index 4d3af4b560..9cee37b791 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Reset.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Reset.java
@@ -51,7 +51,7 @@ import org.eclipse.jgit.api.ResetCommand;
import org.eclipse.jgit.api.ResetCommand.ResetType;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
-import org.kohsuke.args4j.spi.StopOptionHandler;
+import org.kohsuke.args4j.spi.RestOfArgumentsHandler;
@Command(common = true, usage = "usage_reset")
class Reset extends TextBuiltin {
@@ -65,12 +65,12 @@ class Reset extends TextBuiltin {
@Option(name = "--hard", usage = "usage_resetHard")
private boolean hard = false;
- @Argument(required = true, index = 0, metaVar = "metaVar_name", usage = "usage_reset")
+ @Argument(required = false, index = 0, metaVar = "metaVar_commitish", usage = "usage_resetReference")
private String commit;
- @Argument(index = 1)
- @Option(name = "--", metaVar = "metaVar_paths", multiValued = true, handler = StopOptionHandler.class)
- private List<String> paths = new ArrayList<String>();
+ @Argument(required = false, index = 1, metaVar = "metaVar_paths")
+ @Option(name = "--", metaVar = "metaVar_paths", multiValued = true, handler = RestOfArgumentsHandler.class)
+ private List<String> paths = new ArrayList<>();
@Override
protected void run() throws Exception {
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/RevParse.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/RevParse.java
index e32fc9cab4..c5ecb8496e 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/RevParse.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/RevParse.java
@@ -75,7 +75,13 @@ class RevParse extends TextBuiltin {
if (all) {
Map<String, Ref> allRefs = db.getRefDatabase().getRefs(ALL);
for (final Ref r : allRefs.values()) {
- outw.println(r.getObjectId().name());
+ ObjectId objectId = r.getObjectId();
+ // getRefs skips dangling symrefs, so objectId should never be
+ // null.
+ if (objectId == null) {
+ throw new NullPointerException();
+ }
+ outw.println(objectId.name());
}
} else {
if (verify && commits.size() > 1) {
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Status.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Status.java
index be82d070f7..6a6322131a 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Status.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Status.java
@@ -59,8 +59,9 @@ import org.eclipse.jgit.lib.IndexDiff.StageState;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.pgm.internal.CLIText;
+import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
-
+import org.kohsuke.args4j.spi.RestOfArgumentsHandler;
import org.eclipse.jgit.pgm.opt.UntrackedFilesHandler;
/**
@@ -83,7 +84,8 @@ class Status extends TextBuiltin {
@Option(name = "--untracked-files", aliases = { "-u", "-uno", "-uall" }, usage = "usage_untrackedFilesMode", handler = UntrackedFilesHandler.class)
protected String untrackedFilesMode = "all"; // default value //$NON-NLS-1$
- @Option(name = "--", metaVar = "metaVar_path", multiValued = true)
+ @Argument(required = false, index = 0, metaVar = "metaVar_paths")
+ @Option(name = "--", metaVar = "metaVar_paths", multiValued = true, handler = RestOfArgumentsHandler.class)
protected List<String> filterPaths;
@Override
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/TextBuiltin.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/TextBuiltin.java
index 56cfc7e8ef..0dc549c7d7 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/TextBuiltin.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/TextBuiltin.java
@@ -212,17 +212,20 @@ public abstract class TextBuiltin {
*/
protected void parseArguments(final String[] args) throws IOException {
final CmdLineParser clp = new CmdLineParser(this);
+ help = containsHelp(args);
try {
clp.parseArgument(args);
} catch (CmdLineException err) {
- if (!help) {
- this.errw.println(MessageFormat.format(CLIText.get().fatalError, err.getMessage()));
- throw die(true);
+ this.errw.println(CLIText.fatalError(err.getMessage()));
+ if (help) {
+ printUsage("", clp); //$NON-NLS-1$
}
+ throw die(true, err);
}
if (help) {
- printUsageAndExit(clp);
+ printUsage("", clp); //$NON-NLS-1$
+ throw new TerminatedByHelpException();
}
argWalk = clp.getRevWalkGently();
@@ -246,6 +249,20 @@ public abstract class TextBuiltin {
* @throws IOException
*/
public void printUsageAndExit(final String message, final CmdLineParser clp) throws IOException {
+ printUsage(message, clp);
+ throw die(true);
+ }
+
+ /**
+ * @param message
+ * non null
+ * @param clp
+ * parser used to print options
+ * @throws IOException
+ * @since 4.2
+ */
+ protected void printUsage(final String message, final CmdLineParser clp)
+ throws IOException {
errw.println(message);
errw.print("jgit "); //$NON-NLS-1$
errw.print(commandName);
@@ -257,12 +274,19 @@ public abstract class TextBuiltin {
errw.println();
errw.flush();
- throw die(true);
}
/**
- * @return the resource bundle that will be passed to args4j for purpose
- * of string localization
+ * @return error writer, typically this is standard error.
+ * @since 4.2
+ */
+ public ThrowingPrintWriter getErrorWriter() {
+ return errw;
+ }
+
+ /**
+ * @return the resource bundle that will be passed to args4j for purpose of
+ * string localization
*/
protected ResourceBundle getResourceBundle() {
return CLIText.get().resourceBundle();
@@ -324,6 +348,19 @@ public abstract class TextBuiltin {
return new Die(aborted);
}
+ /**
+ * @param aborted
+ * boolean indicating that the execution has been aborted before
+ * running
+ * @param cause
+ * why the command has failed.
+ * @return a runtime exception the caller is expected to throw
+ * @since 4.2
+ */
+ protected static Die die(boolean aborted, final Throwable cause) {
+ return new Die(aborted, cause);
+ }
+
String abbreviateRef(String dst, boolean abbreviateRemote) {
if (dst.startsWith(R_HEADS))
dst = dst.substring(R_HEADS.length());
@@ -333,4 +370,36 @@ public abstract class TextBuiltin {
dst = dst.substring(R_REMOTES.length());
return dst;
}
+
+ /**
+ * @param args
+ * non null
+ * @return true if the given array contains help option
+ * @since 4.2
+ */
+ public static boolean containsHelp(String[] args) {
+ for (String str : args) {
+ if (str.equals("-h") || str.equals("--help")) { //$NON-NLS-1$ //$NON-NLS-2$
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Exception thrown by {@link TextBuiltin} if it proceeds 'help' option
+ *
+ * @since 4.2
+ */
+ public static class TerminatedByHelpException extends Die {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Default constructor
+ */
+ public TerminatedByHelpException() {
+ super(true);
+ }
+
+ }
}
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/RebuildRefTree.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/RebuildRefTree.java
new file mode 100644
index 0000000000..fbd4672f28
--- /dev/null
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/RebuildRefTree.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2015, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.pgm.debug;
+
+import static org.eclipse.jgit.lib.Constants.HEAD;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jgit.internal.storage.reftree.RefTree;
+import org.eclipse.jgit.internal.storage.reftree.RefTreeDatabase;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.pgm.Command;
+import org.eclipse.jgit.pgm.TextBuiltin;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.kohsuke.args4j.Option;
+
+@Command(usage = "usage_RebuildRefTree")
+class RebuildRefTree extends TextBuiltin {
+ @Option(name = "--enable", usage = "set extensions.refsStorage = reftree")
+ boolean enable;
+
+ private String txnNamespace;
+ private String txnCommitted;
+
+ @Override
+ protected void run() throws Exception {
+ try (ObjectReader reader = db.newObjectReader();
+ RevWalk rw = new RevWalk(reader);
+ ObjectInserter inserter = db.newObjectInserter()) {
+ RefDatabase refDb = db.getRefDatabase();
+ if (refDb instanceof RefTreeDatabase) {
+ RefTreeDatabase d = (RefTreeDatabase) refDb;
+ refDb = d.getBootstrap();
+ txnNamespace = d.getTxnNamespace();
+ txnCommitted = d.getTxnCommitted();
+ } else {
+ RefTreeDatabase d = new RefTreeDatabase(db, refDb);
+ txnNamespace = d.getTxnNamespace();
+ txnCommitted = d.getTxnCommitted();
+ }
+
+ errw.format("Rebuilding %s from %s", //$NON-NLS-1$
+ txnCommitted, refDb.getClass().getSimpleName());
+ errw.println();
+ errw.flush();
+
+ CommitBuilder b = new CommitBuilder();
+ Ref ref = refDb.exactRef(txnCommitted);
+ RefUpdate update = refDb.newUpdate(txnCommitted, true);
+ ObjectId oldTreeId;
+
+ if (ref != null && ref.getObjectId() != null) {
+ ObjectId oldId = ref.getObjectId();
+ update.setExpectedOldObjectId(oldId);
+ b.setParentId(oldId);
+ oldTreeId = rw.parseCommit(oldId).getTree();
+ } else {
+ update.setExpectedOldObjectId(ObjectId.zeroId());
+ oldTreeId = ObjectId.zeroId();
+ }
+
+ RefTree tree = rebuild(refDb);
+ b.setTreeId(tree.writeTree(inserter));
+ b.setAuthor(new PersonIdent(db));
+ b.setCommitter(b.getAuthor());
+ if (b.getTreeId().equals(oldTreeId)) {
+ return;
+ }
+
+ update.setNewObjectId(inserter.insert(b));
+ inserter.flush();
+
+ RefUpdate.Result result = update.update(rw);
+ switch (result) {
+ case NEW:
+ case FAST_FORWARD:
+ break;
+ default:
+ throw die(String.format("%s: %s", update.getName(), result)); //$NON-NLS-1$
+ }
+
+ if (enable && !(db.getRefDatabase() instanceof RefTreeDatabase)) {
+ StoredConfig cfg = db.getConfig();
+ cfg.setInt("core", null, "repositoryformatversion", 1); //$NON-NLS-1$ //$NON-NLS-2$
+ cfg.setString("extensions", null, "refsStorage", "reftree"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+ cfg.save();
+ errw.println("Enabled reftree."); //$NON-NLS-1$
+ errw.flush();
+ }
+ }
+ }
+
+ private RefTree rebuild(RefDatabase refdb) throws IOException {
+ RefTree tree = RefTree.newEmptyTree();
+ List<org.eclipse.jgit.internal.storage.reftree.Command> cmds
+ = new ArrayList<>();
+
+ Ref head = refdb.exactRef(HEAD);
+ if (head != null) {
+ cmds.add(new org.eclipse.jgit.internal.storage.reftree.Command(
+ null,
+ head));
+ }
+
+ for (Ref r : refdb.getRefs(RefDatabase.ALL).values()) {
+ if (r.getName().equals(txnCommitted)
+ || r.getName().startsWith(txnNamespace)) {
+ continue;
+ }
+ cmds.add(new org.eclipse.jgit.internal.storage.reftree.Command(
+ null,
+ db.peel(r)));
+ }
+ tree.apply(cmds);
+ return tree;
+ }
+}
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java
index f5d581ad01..2812137266 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java
@@ -74,6 +74,19 @@ public class CLIText extends TranslationBundle {
return MessageFormat.format(get().lineFormat, line);
}
+ /**
+ * Format the given argument as fatal error using the format defined by
+ * {@link #fatalError} ("fatal: " by default).
+ *
+ * @param message
+ * the message to format
+ * @return the formatted line
+ * @since 4.2
+ */
+ public static String fatalError(String message) {
+ return MessageFormat.format(get().fatalError, message);
+ }
+
// @formatter:off
/***/ public String alreadyOnBranch;
/***/ public String alreadyUpToDate;
@@ -85,6 +98,7 @@ public class CLIText extends TranslationBundle {
/***/ public String branchCreatedFrom;
/***/ public String branchDetachedHEAD;
/***/ public String branchIsNotAnAncestorOfYourCurrentHEAD;
+ /***/ public String branchNameRequired;
/***/ public String branchNotFound;
/***/ public String cacheTreePathInfo;
/***/ public String configFileNotFound;
@@ -184,6 +198,7 @@ public class CLIText extends TranslationBundle {
/***/ public String metaVar_uriish;
/***/ public String metaVar_url;
/***/ public String metaVar_user;
+ /***/ public String metaVar_values;
/***/ public String metaVar_version;
/***/ public String mostCommonlyUsedCommandsAre;
/***/ public String needApprovalToDestroyCurrentRepository;
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/opt/CmdLineParser.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/opt/CmdLineParser.java
index 3f77aa6687..b531ba65a4 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/opt/CmdLineParser.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/opt/CmdLineParser.java
@@ -43,19 +43,18 @@
package org.eclipse.jgit.pgm.opt;
+import java.io.IOException;
+import java.io.Writer;
import java.lang.reflect.Field;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ResourceBundle;
-import org.kohsuke.args4j.Argument;
-import org.kohsuke.args4j.CmdLineException;
-import org.kohsuke.args4j.IllegalAnnotationError;
-import org.kohsuke.args4j.NamedOptionDef;
-import org.kohsuke.args4j.Option;
-import org.kohsuke.args4j.OptionDef;
-import org.kohsuke.args4j.spi.OptionHandler;
-import org.kohsuke.args4j.spi.Setter;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.pgm.Die;
import org.eclipse.jgit.pgm.TextBuiltin;
import org.eclipse.jgit.pgm.internal.CLIText;
import org.eclipse.jgit.revwalk.RevCommit;
@@ -63,6 +62,15 @@ import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.treewalk.AbstractTreeIterator;
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.CmdLineException;
+import org.kohsuke.args4j.IllegalAnnotationError;
+import org.kohsuke.args4j.NamedOptionDef;
+import org.kohsuke.args4j.Option;
+import org.kohsuke.args4j.OptionDef;
+import org.kohsuke.args4j.spi.OptionHandler;
+import org.kohsuke.args4j.spi.RestOfArgumentsHandler;
+import org.kohsuke.args4j.spi.Setter;
/**
* Extended command line parser which handles --foo=value arguments.
@@ -80,12 +88,17 @@ public class CmdLineParser extends org.kohsuke.args4j.CmdLineParser {
registerHandler(RefSpec.class, RefSpecHandler.class);
registerHandler(RevCommit.class, RevCommitHandler.class);
registerHandler(RevTree.class, RevTreeHandler.class);
+ registerHandler(List.class, OptionWithValuesListHandler.class);
}
private final Repository db;
private RevWalk walk;
+ private boolean seenHelp;
+
+ private TextBuiltin cmd;
+
/**
* Creates a new command line owner that parses arguments/options and set
* them into the given object.
@@ -117,8 +130,12 @@ public class CmdLineParser extends org.kohsuke.args4j.CmdLineParser {
*/
public CmdLineParser(final Object bean, Repository repo) {
super(bean);
- if (repo == null && bean instanceof TextBuiltin)
- repo = ((TextBuiltin) bean).getRepository();
+ if (bean instanceof TextBuiltin) {
+ cmd = (TextBuiltin) bean;
+ }
+ if (repo == null && cmd != null) {
+ repo = cmd.getRepository();
+ }
this.db = repo;
}
@@ -143,9 +160,75 @@ public class CmdLineParser extends org.kohsuke.args4j.CmdLineParser {
}
tmp.add(str);
+
+ if (containsHelp(args)) {
+ // suppress exceptions on required parameters if help is present
+ seenHelp = true;
+ // stop argument parsing here
+ break;
+ }
+ }
+ List<OptionHandler> backup = null;
+ if (seenHelp) {
+ backup = unsetRequiredOptions();
}
- super.parseArgument(tmp.toArray(new String[tmp.size()]));
+ try {
+ super.parseArgument(tmp.toArray(new String[tmp.size()]));
+ } catch (Die e) {
+ if (!seenHelp) {
+ throw e;
+ }
+ printToErrorWriter(CLIText.fatalError(e.getMessage()));
+ } finally {
+ // reset "required" options to defaults for correct command printout
+ if (backup != null && !backup.isEmpty()) {
+ restoreRequiredOptions(backup);
+ }
+ seenHelp = false;
+ }
+ }
+
+ private void printToErrorWriter(String error) {
+ if (cmd == null) {
+ System.err.println(error);
+ } else {
+ try {
+ cmd.getErrorWriter().println(error);
+ } catch (IOException e1) {
+ System.err.println(error);
+ }
+ }
+ }
+
+ private List<OptionHandler> unsetRequiredOptions() {
+ List<OptionHandler> options = getOptions();
+ List<OptionHandler> backup = new ArrayList<>(options);
+ for (Iterator<OptionHandler> iterator = options.iterator(); iterator
+ .hasNext();) {
+ OptionHandler handler = iterator.next();
+ if (handler.option instanceof NamedOptionDef
+ && handler.option.required()) {
+ iterator.remove();
+ }
+ }
+ return backup;
+ }
+
+ private void restoreRequiredOptions(List<OptionHandler> backup) {
+ List<OptionHandler> options = getOptions();
+ options.clear();
+ options.addAll(backup);
+ }
+
+ /**
+ * @param args
+ * non null
+ * @return true if the given array contains help option
+ * @since 4.2
+ */
+ protected boolean containsHelp(final String... args) {
+ return TextBuiltin.containsHelp(args);
}
/**
@@ -181,7 +264,7 @@ public class CmdLineParser extends org.kohsuke.args4j.CmdLineParser {
return walk;
}
- static class MyOptionDef extends OptionDef {
+ class MyOptionDef extends OptionDef {
public MyOptionDef(OptionDef o) {
super(o.usage(), o.metaVar(), o.required(), o.handler(), o
@@ -201,6 +284,11 @@ public class CmdLineParser extends org.kohsuke.args4j.CmdLineParser {
return metaVar();
}
}
+
+ @Override
+ public boolean required() {
+ return seenHelp ? false : super.required();
+ }
}
@Override
@@ -211,4 +299,55 @@ public class CmdLineParser extends org.kohsuke.args4j.CmdLineParser {
return super.createOptionHandler(new MyOptionDef(o), setter);
}
+
+ @SuppressWarnings("unchecked")
+ private List<OptionHandler> getOptions() {
+ List<OptionHandler> options = null;
+ try {
+ Field field = org.kohsuke.args4j.CmdLineParser.class
+ .getDeclaredField("options"); //$NON-NLS-1$
+ field.setAccessible(true);
+ options = (List<OptionHandler>) field.get(this);
+ } catch (NoSuchFieldException | SecurityException
+ | IllegalArgumentException | IllegalAccessException e) {
+ // ignore
+ }
+ if (options == null) {
+ return Collections.emptyList();
+ }
+ return options;
+ }
+
+ @Override
+ public void printSingleLineUsage(Writer w, ResourceBundle rb) {
+ List<OptionHandler> options = getOptions();
+ if (options.isEmpty()) {
+ super.printSingleLineUsage(w, rb);
+ return;
+ }
+ List<OptionHandler> backup = new ArrayList<>(options);
+ boolean changed = sortRestOfArgumentsHandlerToTheEnd(options);
+ try {
+ super.printSingleLineUsage(w, rb);
+ } finally {
+ if (changed) {
+ options.clear();
+ options.addAll(backup);
+ }
+ }
+ }
+
+ private boolean sortRestOfArgumentsHandlerToTheEnd(
+ List<OptionHandler> options) {
+ for (int i = 0; i < options.size(); i++) {
+ OptionHandler handler = options.get(i);
+ if (handler instanceof RestOfArgumentsHandler
+ || handler instanceof PathTreeFilterHandler) {
+ options.remove(i);
+ options.add(handler);
+ return true;
+ }
+ }
+ return false;
+ }
}
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/opt/OptionWithValuesListHandler.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/opt/OptionWithValuesListHandler.java
new file mode 100644
index 0000000000..3de7a81091
--- /dev/null
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/opt/OptionWithValuesListHandler.java
@@ -0,0 +1,52 @@
+package org.eclipse.jgit.pgm.opt;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jgit.pgm.internal.CLIText;
+import org.kohsuke.args4j.CmdLineException;
+import org.kohsuke.args4j.CmdLineParser;
+import org.kohsuke.args4j.OptionDef;
+import org.kohsuke.args4j.spi.OptionHandler;
+import org.kohsuke.args4j.spi.Parameters;
+import org.kohsuke.args4j.spi.Setter;
+
+/**
+ * Handler which allows to parse option with few values
+ *
+ * @since 4.2
+ */
+public class OptionWithValuesListHandler extends OptionHandler<List<?>> {
+
+ /**
+ * @param parser
+ * @param option
+ * @param setter
+ */
+ public OptionWithValuesListHandler(CmdLineParser parser,
+ OptionDef option, Setter<List<?>> setter) {
+ super(parser, option, setter);
+ }
+
+ @Override
+ public int parseArguments(Parameters params) throws CmdLineException {
+ final List<String> list = new ArrayList<>();
+ for (int idx = 0; idx < params.size(); idx++) {
+ final String p;
+ try {
+ p = params.getParameter(idx);
+ } catch (CmdLineException cle) {
+ break;
+ }
+ list.add(p);
+ }
+ setter.addValue(list);
+ return list.size();
+ }
+
+ @Override
+ public String getDefaultMetaVariable() {
+ return CLIText.get().metaVar_values;
+ }
+
+}
diff --git a/org.eclipse.jgit.test/BUCK b/org.eclipse.jgit.test/BUCK
new file mode 100644
index 0000000000..3df3336b4e
--- /dev/null
+++ b/org.eclipse.jgit.test/BUCK
@@ -0,0 +1,95 @@
+PKG = 'tst/org/eclipse/jgit/'
+HELPERS = glob(['src/**/*.java']) + [PKG + c for c in [
+ 'api/AbstractRemoteCommandTest.java',
+ 'diff/AbstractDiffTestCase.java',
+ 'internal/storage/file/GcTestCase.java',
+ 'internal/storage/file/PackIndexTestCase.java',
+ 'internal/storage/file/XInputStream.java',
+ 'nls/GermanTranslatedBundle.java',
+ 'nls/MissingPropertyBundle.java',
+ 'nls/NoPropertiesBundle.java',
+ 'nls/NonTranslatedBundle.java',
+ 'revwalk/RevQueueTestCase.java',
+ 'revwalk/RevWalkTestCase.java',
+ 'transport/SpiTransport.java',
+ 'treewalk/FileTreeIteratorWithTimeControl.java',
+ 'treewalk/filter/AlwaysCloneTreeFilter.java',
+ 'test/resources/SampleDataRepositoryTestCase.java',
+ 'util/CPUTimeStopWatch.java',
+ 'util/io/Strings.java',
+]]
+
+DATA = [
+ PKG + 'lib/empty.gitindex.dat',
+ PKG + 'lib/sorttest.gitindex.dat',
+]
+
+TESTS = glob(
+ ['tst/**/*.java'],
+ excludes = HELPERS + DATA,
+)
+
+DEPS = {
+ PKG + 'nls/RootLocaleTest.java': [
+ '//org.eclipse.jgit.pgm:pgm',
+ '//org.eclipse.jgit.ui:ui',
+ ],
+}
+
+for src in TESTS:
+ name = src[len('tst/'):len(src)-len('.java')].replace('/', '.')
+ labels = []
+ if name.startswith('org.eclipse.jgit.'):
+ l = name[len('org.eclipse.jgit.'):]
+ if l.startswith('internal.storage.'):
+ l = l[len('internal.storage.'):]
+ i = l.find('.')
+ if i > 0:
+ labels.append(l[:i])
+ else:
+ labels.append(i)
+ if 'lib' not in labels:
+ labels.append('lib')
+
+ java_test(
+ name = name,
+ labels = labels,
+ srcs = [src],
+ deps = [
+ ':helpers',
+ ':tst_rsrc',
+ '//org.eclipse.jgit:jgit',
+ '//org.eclipse.jgit.junit:junit',
+ '//lib:hamcrest-core',
+ '//lib:hamcrest-library',
+ '//lib:javaewah',
+ '//lib:junit',
+ '//lib:slf4j-api',
+ '//lib:slf4j-simple',
+ ] + DEPS.get(src, []),
+ source_under_test = ['//org.eclipse.jgit:jgit'],
+ vm_args = ['-Xmx256m', '-Dfile.encoding=UTF-8'],
+ )
+
+java_library(
+ name = 'helpers',
+ srcs = HELPERS,
+ resources = DATA,
+ deps = [
+ '//org.eclipse.jgit:jgit',
+ '//org.eclipse.jgit.junit:junit',
+ '//lib:junit',
+ ],
+)
+
+prebuilt_jar(
+ name = 'tst_rsrc',
+ binary_jar = ':tst_rsrc_jar',
+)
+
+genrule(
+ name = 'tst_rsrc_jar',
+ cmd = 'cd $SRCDIR/tst-rsrc ; zip -qr $OUT .',
+ srcs = glob(['tst-rsrc/**']),
+ out = 'tst_rsrc.jar',
+)
diff --git a/org.eclipse.jgit.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.test/META-INF/MANIFEST.MF
index 37fd367171..f78fe5b24b 100644
--- a/org.eclipse.jgit.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.test/META-INF/MANIFEST.MF
@@ -26,6 +26,7 @@ Import-Package: com.googlecode.javaewah;version="[0.7.9,0.8.0)",
org.eclipse.jgit.internal.storage.dfs;version="[4.2.0,4.3.0)",
org.eclipse.jgit.internal.storage.file;version="[4.2.0,4.3.0)",
org.eclipse.jgit.internal.storage.pack;version="[4.2.0,4.3.0)",
+ org.eclipse.jgit.internal.storage.reftree;version="[4.2.0,4.3.0)",
org.eclipse.jgit.junit;version="[4.2.0,4.3.0)",
org.eclipse.jgit.lib;version="[4.2.0,4.3.0)",
org.eclipse.jgit.merge;version="[4.2.0,4.3.0)",
diff --git a/org.eclipse.jgit.test/org.eclipse.jgit.core--All-Tests (Java 8) (de).launch b/org.eclipse.jgit.test/org.eclipse.jgit.core--All-Tests (Java 8) (de).launch
new file mode 100644
index 0000000000..f12a529e14
--- /dev/null
+++ b/org.eclipse.jgit.test/org.eclipse.jgit.core--All-Tests (Java 8) (de).launch
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<launchConfiguration type="org.eclipse.jdt.junit.launchconfig">
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
+<listEntry value="/org.eclipse.jgit.test/tst"/>
+</listAttribute>
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
+<listEntry value="2"/>
+</listAttribute>
+<booleanAttribute key="org.eclipse.debug.core.appendEnvironmentVariables" value="true"/>
+<mapAttribute key="org.eclipse.debug.core.environmentVariables">
+<mapEntry key="LANG" value="de_DE.UTF-8"/>
+</mapAttribute>
+<listAttribute key="org.eclipse.debug.ui.favoriteGroups">
+<listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
+<listEntry value="org.eclipse.debug.ui.launchGroup.run"/>
+</listAttribute>
+<stringAttribute key="org.eclipse.jdt.junit.CONTAINER" value="=org.eclipse.jgit.test/tst"/>
+<booleanAttribute key="org.eclipse.jdt.junit.KEEPRUNNING_ATTR" value="false"/>
+<stringAttribute key="org.eclipse.jdt.junit.TESTNAME" value=""/>
+<stringAttribute key="org.eclipse.jdt.junit.TEST_KIND" value="org.eclipse.jdt.junit.loader.junit4"/>
+<booleanAttribute key="org.eclipse.jdt.launching.ATTR_USE_START_ON_FIRST_THREAD" value="true"/>
+<listAttribute key="org.eclipse.jdt.launching.CLASSPATH">
+<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry containerPath=&quot;org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6&quot; path=&quot;1&quot; type=&quot;4&quot;/&gt;&#10;"/>
+<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry id=&quot;org.eclipse.jdt.launching.classpathentry.defaultClasspath&quot;&gt;&#10;&lt;memento exportedEntriesOnly=&quot;false&quot; project=&quot;org.eclipse.jgit.test&quot;/&gt;&#10;&lt;/runtimeClasspathEntry&gt;&#10;"/>
+</listAttribute>
+<booleanAttribute key="org.eclipse.jdt.launching.DEFAULT_CLASSPATH" value="false"/>
+<stringAttribute key="org.eclipse.jdt.launching.JRE_CONTAINER" value="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
+<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value=""/>
+<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="org.eclipse.jgit.test"/>
+<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Xmx256m"/>
+</launchConfiguration>
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java
index d4bd68e686..4fefdfddab 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java
@@ -43,6 +43,7 @@
*/
package org.eclipse.jgit.api;
+import static org.eclipse.jgit.util.FileUtils.RECURSIVE;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@@ -797,7 +798,6 @@ public class AddCommandTest extends RepositoryTestCase {
assertEquals("[a.txt, mode:100644, content:more content,"
+ " assume-unchanged:false][b.txt, mode:100644,"
- + "" + ""
+ " content:content, assume-unchanged:true]",
indexState(CONTENT
| ASSUME_UNCHANGED));
@@ -805,6 +805,102 @@ public class AddCommandTest extends RepositoryTestCase {
}
@Test
+ public void testReplaceFileWithDirectory()
+ throws IOException, NoFilepatternException, GitAPIException {
+ try (Git git = new Git(db)) {
+ writeTrashFile("df", "before replacement");
+ git.add().addFilepattern("df").call();
+ assertEquals("[df, mode:100644, content:before replacement]",
+ indexState(CONTENT));
+ FileUtils.delete(new File(db.getWorkTree(), "df"));
+ writeTrashFile("df/f", "after replacement");
+ git.add().addFilepattern("df").call();
+ assertEquals("[df/f, mode:100644, content:after replacement]",
+ indexState(CONTENT));
+ }
+ }
+
+ @Test
+ public void testReplaceDirectoryWithFile()
+ throws IOException, NoFilepatternException, GitAPIException {
+ try (Git git = new Git(db)) {
+ writeTrashFile("df/f", "before replacement");
+ git.add().addFilepattern("df").call();
+ assertEquals("[df/f, mode:100644, content:before replacement]",
+ indexState(CONTENT));
+ FileUtils.delete(new File(db.getWorkTree(), "df"), RECURSIVE);
+ writeTrashFile("df", "after replacement");
+ git.add().addFilepattern("df").call();
+ assertEquals("[df, mode:100644, content:after replacement]",
+ indexState(CONTENT));
+ }
+ }
+
+ @Test
+ public void testReplaceFileByPartOfDirectory()
+ throws IOException, NoFilepatternException, GitAPIException {
+ try (Git git = new Git(db)) {
+ writeTrashFile("src/main", "df", "before replacement");
+ writeTrashFile("src/main", "z", "z");
+ writeTrashFile("z", "z2");
+ git.add().addFilepattern("src/main/df")
+ .addFilepattern("src/main/z")
+ .addFilepattern("z")
+ .call();
+ assertEquals(
+ "[src/main/df, mode:100644, content:before replacement]" +
+ "[src/main/z, mode:100644, content:z]" +
+ "[z, mode:100644, content:z2]",
+ indexState(CONTENT));
+ FileUtils.delete(new File(db.getWorkTree(), "src/main/df"));
+ writeTrashFile("src/main/df", "a", "after replacement");
+ writeTrashFile("src/main/df", "b", "unrelated file");
+ git.add().addFilepattern("src/main/df/a").call();
+ assertEquals(
+ "[src/main/df/a, mode:100644, content:after replacement]" +
+ "[src/main/z, mode:100644, content:z]" +
+ "[z, mode:100644, content:z2]",
+ indexState(CONTENT));
+ }
+ }
+
+ @Test
+ public void testReplaceDirectoryConflictsWithFile()
+ throws IOException, NoFilepatternException, GitAPIException {
+ DirCache dc = db.lockDirCache();
+ try (ObjectInserter oi = db.newObjectInserter()) {
+ DirCacheBuilder builder = dc.builder();
+ File f = writeTrashFile("a", "df", "content");
+ addEntryToBuilder("a", f, oi, builder, 1);
+
+ f = writeTrashFile("a", "df", "other content");
+ addEntryToBuilder("a/df", f, oi, builder, 3);
+
+ f = writeTrashFile("a", "df", "our content");
+ addEntryToBuilder("a/df", f, oi, builder, 2);
+
+ f = writeTrashFile("z", "z");
+ addEntryToBuilder("z", f, oi, builder, 0);
+ builder.commit();
+ }
+ assertEquals(
+ "[a, mode:100644, stage:1, content:content]" +
+ "[a/df, mode:100644, stage:2, content:our content]" +
+ "[a/df, mode:100644, stage:3, content:other content]" +
+ "[z, mode:100644, content:z]",
+ indexState(CONTENT));
+
+ try (Git git = new Git(db)) {
+ FileUtils.delete(new File(db.getWorkTree(), "a"), RECURSIVE);
+ writeTrashFile("a", "merged");
+ git.add().addFilepattern("a").call();
+ assertEquals("[a, mode:100644, content:merged]" +
+ "[z, mode:100644, content:z]",
+ indexState(CONTENT));
+ }
+ }
+
+ @Test
public void testExecutableRetention() throws Exception {
StoredConfig config = db.getConfig();
config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java
index 0d03047d53..b39a68a22e 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java
@@ -46,12 +46,15 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.fail;
import java.io.File;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
+import org.eclipse.jgit.api.errors.EmtpyCommitException;
import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.dircache.DirCache;
@@ -477,6 +480,34 @@ public class CommitCommandTest extends RepositoryTestCase {
}
@Test
+ public void commitEmptyCommits() throws Exception {
+ try (Git git = new Git(db)) {
+
+ writeTrashFile("file1", "file1");
+ git.add().addFilepattern("file1").call();
+ RevCommit initial = git.commit().setMessage("initial commit")
+ .call();
+
+ RevCommit emptyFollowUp = git.commit()
+ .setAuthor("New Author", "newauthor@example.org")
+ .setMessage("no change").call();
+
+ assertNotEquals(initial.getId(), emptyFollowUp.getId());
+ assertEquals(initial.getTree().getId(),
+ emptyFollowUp.getTree().getId());
+
+ try {
+ git.commit().setAuthor("New Author", "newauthor@example.org")
+ .setMessage("again no change").setAllowEmpty(false)
+ .call();
+ fail("Didn't get the expected EmtpyCommitException");
+ } catch (EmtpyCommitException e) {
+ // expect this exception
+ }
+ }
+ }
+
+ @Test
public void commitOnlyShouldCommitUnmergedPathAndNotAffectOthers()
throws Exception {
DirCache index = db.lockDirCache();
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PathCheckoutCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PathCheckoutCommandTest.java
index db811cdf59..3343af06dd 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PathCheckoutCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PathCheckoutCommandTest.java
@@ -43,10 +43,12 @@
package org.eclipse.jgit.api;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.io.File;
import java.io.IOException;
+import java.nio.file.Path;
import org.eclipse.jgit.api.CheckoutCommand.Stage;
import org.eclipse.jgit.api.errors.JGitInternalException;
@@ -59,6 +61,9 @@ import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.RepositoryState;
import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.FileUtils;
+import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
@@ -73,6 +78,8 @@ public class PathCheckoutCommandTest extends RepositoryTestCase {
private static final String FILE3 = "Test3.txt";
+ private static final String LINK = "link";
+
Git git;
RevCommit initialCommit;
@@ -99,6 +106,64 @@ public class PathCheckoutCommandTest extends RepositoryTestCase {
}
@Test
+ public void testUpdateSymLink() throws Exception {
+ Assume.assumeTrue(FS.DETECTED.supportsSymlinks());
+
+ Path path = writeLink(LINK, FILE1);
+ git.add().addFilepattern(LINK).call();
+ git.commit().setMessage("Added link").call();
+ assertEquals("3", read(path.toFile()));
+
+ writeLink(LINK, FILE2);
+ assertEquals("c", read(path.toFile()));
+
+ CheckoutCommand co = git.checkout();
+ co.addPath(LINK).call();
+
+ assertEquals("3", read(path.toFile()));
+ }
+
+ @Test
+ public void testUpdateBrokenSymLinkToDirectory() throws Exception {
+ Assume.assumeTrue(FS.DETECTED.supportsSymlinks());
+
+ Path path = writeLink(LINK, "f");
+ git.add().addFilepattern(LINK).call();
+ git.commit().setMessage("Added link").call();
+ assertEquals("f", FileUtils.readSymLink(path.toFile()));
+ assertTrue(path.toFile().exists());
+
+ writeLink(LINK, "link_to_nowhere");
+ assertFalse(path.toFile().exists());
+ assertEquals("link_to_nowhere", FileUtils.readSymLink(path.toFile()));
+
+ CheckoutCommand co = git.checkout();
+ co.addPath(LINK).call();
+
+ assertEquals("f", FileUtils.readSymLink(path.toFile()));
+ }
+
+ @Test
+ public void testUpdateBrokenSymLink() throws Exception {
+ Assume.assumeTrue(FS.DETECTED.supportsSymlinks());
+
+ Path path = writeLink(LINK, FILE1);
+ git.add().addFilepattern(LINK).call();
+ git.commit().setMessage("Added link").call();
+ assertEquals("3", read(path.toFile()));
+ assertEquals(FILE1, FileUtils.readSymLink(path.toFile()));
+
+ writeLink(LINK, "link_to_nowhere");
+ assertFalse(path.toFile().exists());
+ assertEquals("link_to_nowhere", FileUtils.readSymLink(path.toFile()));
+
+ CheckoutCommand co = git.checkout();
+ co.addPath(LINK).call();
+
+ assertEquals("3", read(path.toFile()));
+ }
+
+ @Test
public void testUpdateWorkingDirectory() throws Exception {
CheckoutCommand co = git.checkout();
File written = writeTrashFile(FILE1, "");
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java
index a67f2b912a..66f25e8e51 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java
@@ -65,6 +65,7 @@ import org.eclipse.jgit.junit.RepositoryTestCase;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk;
@@ -139,8 +140,8 @@ public class ResetCommandTest extends RepositoryTestCase {
AmbiguousObjectException, IOException, GitAPIException {
setupRepository();
ObjectId prevHead = db.resolve(Constants.HEAD);
- git.reset().setMode(ResetType.HARD).setRef(initialCommit.getName())
- .call();
+ assertSameAsHead(git.reset().setMode(ResetType.HARD)
+ .setRef(initialCommit.getName()).call());
// check if HEAD points to initial commit now
ObjectId head = db.resolve(Constants.HEAD);
assertEquals(initialCommit, head);
@@ -176,8 +177,8 @@ public class ResetCommandTest extends RepositoryTestCase {
AmbiguousObjectException, IOException, GitAPIException {
setupRepository();
ObjectId prevHead = db.resolve(Constants.HEAD);
- git.reset().setMode(ResetType.SOFT).setRef(initialCommit.getName())
- .call();
+ assertSameAsHead(git.reset().setMode(ResetType.SOFT)
+ .setRef(initialCommit.getName()).call());
// check if HEAD points to initial commit now
ObjectId head = db.resolve(Constants.HEAD);
assertEquals(initialCommit, head);
@@ -197,8 +198,8 @@ public class ResetCommandTest extends RepositoryTestCase {
AmbiguousObjectException, IOException, GitAPIException {
setupRepository();
ObjectId prevHead = db.resolve(Constants.HEAD);
- git.reset().setMode(ResetType.MIXED).setRef(initialCommit.getName())
- .call();
+ assertSameAsHead(git.reset().setMode(ResetType.MIXED)
+ .setRef(initialCommit.getName()).call());
// check if HEAD points to initial commit now
ObjectId head = db.resolve(Constants.HEAD);
assertEquals(initialCommit, head);
@@ -241,7 +242,8 @@ public class ResetCommandTest extends RepositoryTestCase {
assertTrue(bEntry.getLength() > 0);
assertTrue(bEntry.getLastModified() > 0);
- git.reset().setMode(ResetType.MIXED).setRef(commit2.getName()).call();
+ assertSameAsHead(git.reset().setMode(ResetType.MIXED)
+ .setRef(commit2.getName()).call());
cache = db.readDirCache();
@@ -280,7 +282,7 @@ public class ResetCommandTest extends RepositoryTestCase {
+ "[a.txt, mode:100644, stage:3]",
indexState(0));
- git.reset().setMode(ResetType.MIXED).call();
+ assertSameAsHead(git.reset().setMode(ResetType.MIXED).call());
assertEquals("[a.txt, mode:100644]" + "[b.txt, mode:100644]",
indexState(0));
@@ -298,8 +300,8 @@ public class ResetCommandTest extends RepositoryTestCase {
// 'a.txt' has already been modified in setupRepository
// 'notAddedToIndex.txt' has been added to repository
- git.reset().addPath(indexFile.getName())
- .addPath(untrackedFile.getName()).call();
+ assertSameAsHead(git.reset().addPath(indexFile.getName())
+ .addPath(untrackedFile.getName()).call());
DirCacheEntry postReset = DirCache.read(db.getIndexFile(), db.getFS())
.getEntry(indexFile.getName());
@@ -329,7 +331,7 @@ public class ResetCommandTest extends RepositoryTestCase {
git.add().addFilepattern(untrackedFile.getName()).call();
// 'dir/b.txt' has already been modified in setupRepository
- git.reset().addPath("dir").call();
+ assertSameAsHead(git.reset().addPath("dir").call());
DirCacheEntry postReset = DirCache.read(db.getIndexFile(), db.getFS())
.getEntry("dir/b.txt");
@@ -358,9 +360,9 @@ public class ResetCommandTest extends RepositoryTestCase {
// 'a.txt' has already been modified in setupRepository
// 'notAddedToIndex.txt' has been added to repository
// reset to the inital commit
- git.reset().setRef(initialCommit.getName())
- .addPath(indexFile.getName())
- .addPath(untrackedFile.getName()).call();
+ assertSameAsHead(git.reset().setRef(initialCommit.getName())
+ .addPath(indexFile.getName()).addPath(untrackedFile.getName())
+ .call());
// check that HEAD hasn't moved
ObjectId head = db.resolve(Constants.HEAD);
@@ -397,7 +399,7 @@ public class ResetCommandTest extends RepositoryTestCase {
+ "[b.txt, mode:100644]",
indexState(0));
- git.reset().addPath(file).call();
+ assertSameAsHead(git.reset().addPath(file).call());
assertEquals("[a.txt, mode:100644]" + "[b.txt, mode:100644]",
indexState(0));
@@ -409,7 +411,7 @@ public class ResetCommandTest extends RepositoryTestCase {
writeTrashFile("a.txt", "content");
git.add().addFilepattern("a.txt").call();
// Should assume an empty tree, like in C Git 1.8.2
- git.reset().addPath("a.txt").call();
+ assertSameAsHead(git.reset().addPath("a.txt").call());
DirCache cache = db.readDirCache();
DirCacheEntry aEntry = cache.getEntry("a.txt");
@@ -421,7 +423,8 @@ public class ResetCommandTest extends RepositoryTestCase {
git = new Git(db);
writeTrashFile("a.txt", "content");
git.add().addFilepattern("a.txt").call();
- git.reset().setRef("doesnotexist").addPath("a.txt").call();
+ assertSameAsHead(
+ git.reset().setRef("doesnotexist").addPath("a.txt").call());
}
@Test
@@ -431,7 +434,7 @@ public class ResetCommandTest extends RepositoryTestCase {
git.add().addFilepattern("a.txt").call();
writeTrashFile("a.txt", "modified");
// should use default mode MIXED
- git.reset().call();
+ assertSameAsHead(git.reset().call());
DirCache cache = db.readDirCache();
DirCacheEntry aEntry = cache.getEntry("a.txt");
@@ -452,7 +455,7 @@ public class ResetCommandTest extends RepositoryTestCase {
git.add().addFilepattern(untrackedFile.getName()).call();
- git.reset().setRef(tagName).setMode(HARD).call();
+ assertSameAsHead(git.reset().setRef(tagName).setMode(HARD).call());
ObjectId head = db.resolve(Constants.HEAD);
assertEquals(secondCommit, head);
@@ -486,7 +489,8 @@ public class ResetCommandTest extends RepositoryTestCase {
result.getMergeStatus());
assertNotNull(db.readSquashCommitMsg());
- g.reset().setMode(ResetType.HARD).setRef(first.getName()).call();
+ assertSameAsHead(g.reset().setMode(ResetType.HARD)
+ .setRef(first.getName()).call());
assertNull(db.readSquashCommitMsg());
}
@@ -497,7 +501,7 @@ public class ResetCommandTest extends RepositoryTestCase {
File fileA = writeTrashFile("a.txt", "content");
git.add().addFilepattern("a.txt").call();
// Should assume an empty tree, like in C Git 1.8.2
- git.reset().setMode(ResetType.HARD).call();
+ assertSameAsHead(git.reset().setMode(ResetType.HARD).call());
DirCache cache = db.readDirCache();
DirCacheEntry aEntry = cache.getEntry("a.txt");
@@ -558,4 +562,14 @@ public class ResetCommandTest extends RepositoryTestCase {
return dc.getEntry(path) != null;
}
+ /**
+ * Asserts that a certain ref is similar to repos HEAD.
+ * @param ref
+ * @throws IOException
+ */
+ private void assertSameAsHead(Ref ref) throws IOException {
+ Ref headRef = db.getRef(Constants.HEAD);
+ assertEquals(headRef.getName(), ref.getName());
+ assertEquals(headRef.getObjectId(), ref.getObjectId());
+ }
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeWorkingTreeIteratorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeWorkingTreeIteratorTest.java
index 6ad19a2491..4215ba23e5 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeWorkingTreeIteratorTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeWorkingTreeIteratorTest.java
@@ -56,7 +56,6 @@ import java.util.Collections;
import java.util.List;
import org.eclipse.jgit.attributes.Attribute.State;
-import org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.junit.JGitTestUtil;
import org.eclipse.jgit.junit.RepositoryTestCase;
import org.eclipse.jgit.lib.FileMode;
@@ -253,7 +252,7 @@ public class AttributesNodeWorkingTreeIteratorTest extends RepositoryTestCase {
writeTrashFile(name, data.toString());
}
- private TreeWalk beginWalk() throws CorruptObjectException {
+ private TreeWalk beginWalk() {
TreeWalk newWalk = new TreeWalk(db);
newWalk.addTree(new FileTreeIterator(db));
return newWalk;
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCachePathEditTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCachePathEditTest.java
index 63ec85861d..c85e156352 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCachePathEditTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCachePathEditTest.java
@@ -43,11 +43,13 @@
package org.eclipse.jgit.dircache;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
+import org.eclipse.jgit.errors.DirCacheNameConflictException;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
import org.junit.Test;
@@ -154,6 +156,125 @@ public class DirCachePathEditTest {
assertEquals(DirCacheEntry.STAGE_3, entries.get(2).getStage());
}
+ @Test
+ public void testFileReplacesTree() throws Exception {
+ DirCache dc = DirCache.newInCore();
+ DirCacheEditor editor = dc.editor();
+ editor.add(new AddEdit("a"));
+ editor.add(new AddEdit("b/c"));
+ editor.add(new AddEdit("b/d"));
+ editor.add(new AddEdit("e"));
+ editor.finish();
+
+ editor = dc.editor();
+ editor.add(new AddEdit("b"));
+ editor.finish();
+
+ assertEquals(3, dc.getEntryCount());
+ assertEquals("a", dc.getEntry(0).getPathString());
+ assertEquals("b", dc.getEntry(1).getPathString());
+ assertEquals("e", dc.getEntry(2).getPathString());
+
+ dc.clear();
+ editor = dc.editor();
+ editor.add(new AddEdit("A.c"));
+ editor.add(new AddEdit("A/c"));
+ editor.add(new AddEdit("A0c"));
+ editor.finish();
+
+ editor = dc.editor();
+ editor.add(new AddEdit("A"));
+ editor.finish();
+ assertEquals(3, dc.getEntryCount());
+ assertEquals("A", dc.getEntry(0).getPathString());
+ assertEquals("A.c", dc.getEntry(1).getPathString());
+ assertEquals("A0c", dc.getEntry(2).getPathString());
+ }
+
+ @Test
+ public void testTreeReplacesFile() throws Exception {
+ DirCache dc = DirCache.newInCore();
+ DirCacheEditor editor = dc.editor();
+ editor.add(new AddEdit("a"));
+ editor.add(new AddEdit("ab"));
+ editor.add(new AddEdit("b"));
+ editor.add(new AddEdit("e"));
+ editor.finish();
+
+ editor = dc.editor();
+ editor.add(new AddEdit("b/c/d/f"));
+ editor.add(new AddEdit("b/g/h/i"));
+ editor.finish();
+
+ assertEquals(5, dc.getEntryCount());
+ assertEquals("a", dc.getEntry(0).getPathString());
+ assertEquals("ab", dc.getEntry(1).getPathString());
+ assertEquals("b/c/d/f", dc.getEntry(2).getPathString());
+ assertEquals("b/g/h/i", dc.getEntry(3).getPathString());
+ assertEquals("e", dc.getEntry(4).getPathString());
+ }
+
+ @Test
+ public void testDuplicateFiles() throws Exception {
+ DirCache dc = DirCache.newInCore();
+ DirCacheEditor editor = dc.editor();
+ editor.add(new AddEdit("a"));
+ editor.add(new AddEdit("a"));
+
+ try {
+ editor.finish();
+ fail("Expected DirCacheNameConflictException to be thrown");
+ } catch (DirCacheNameConflictException e) {
+ assertEquals("a a", e.getMessage());
+ assertEquals("a", e.getPath1());
+ assertEquals("a", e.getPath2());
+ }
+ }
+
+ @Test
+ public void testFileOverlapsTree() throws Exception {
+ DirCache dc = DirCache.newInCore();
+ DirCacheEditor editor = dc.editor();
+ editor.add(new AddEdit("a"));
+ editor.add(new AddEdit("a/b").setReplace(false));
+ try {
+ editor.finish();
+ fail("Expected DirCacheNameConflictException to be thrown");
+ } catch (DirCacheNameConflictException e) {
+ assertEquals("a a/b", e.getMessage());
+ assertEquals("a", e.getPath1());
+ assertEquals("a/b", e.getPath2());
+ }
+
+ editor = dc.editor();
+ editor.add(new AddEdit("A.c"));
+ editor.add(new AddEdit("A/c").setReplace(false));
+ editor.add(new AddEdit("A0c"));
+ editor.add(new AddEdit("A"));
+ try {
+ editor.finish();
+ fail("Expected DirCacheNameConflictException to be thrown");
+ } catch (DirCacheNameConflictException e) {
+ assertEquals("A A/c", e.getMessage());
+ assertEquals("A", e.getPath1());
+ assertEquals("A/c", e.getPath2());
+ }
+
+ editor = dc.editor();
+ editor.add(new AddEdit("A.c"));
+ editor.add(new AddEdit("A/b/c/d").setReplace(false));
+ editor.add(new AddEdit("A/b/c"));
+ editor.add(new AddEdit("A0c"));
+ try {
+ editor.finish();
+ fail("Expected DirCacheNameConflictException to be thrown");
+ } catch (DirCacheNameConflictException e) {
+ assertEquals("A/b/c A/b/c/d", e.getMessage());
+ assertEquals("A/b/c", e.getPath1());
+ assertEquals("A/b/c/d", e.getPath2());
+ }
+ }
+
private static DirCacheEntry createEntry(String path, int stage) {
DirCacheEntry entry = new DirCacheEntry(path, stage);
entry.setFileMode(FileMode.REGULAR_FILE);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java
index b6649b3f05..524d0b8e7e 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java
@@ -409,6 +409,7 @@ public class RepoCommandTest extends RepositoryTestCase {
.append("<project path=\"foo\" name=\"").append(defaultUri)
.append("\" revision=\"").append(BRANCH).append("\" >")
.append("<copyfile src=\"hello.txt\" dest=\"Hello\" />")
+ .append("<copyfile src=\"hello.txt\" dest=\"foo/Hello\" />")
.append("</project>").append("</manifest>");
JGitTestUtil.writeTrashFile(tempDb, "manifest.xml",
xmlContent.toString());
@@ -423,8 +424,12 @@ public class RepoCommandTest extends RepositoryTestCase {
.getRepository();
// The Hello file should exist
File hello = new File(localDb.getWorkTree(), "Hello");
- localDb.close();
assertTrue("The Hello file should exist", hello.exists());
+ // The foo/Hello file should be skipped.
+ File foohello = new File(localDb.getWorkTree(), "foo/Hello");
+ assertFalse(
+ "The foo/Hello file should be skipped", foohello.exists());
+ localDb.close();
// The content of Hello file should be expected
BufferedReader reader = new BufferedReader(new FileReader(hello));
String content = reader.readLine();
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreNodeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreNodeTest.java
index 5893d8c407..c026efcde3 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreNodeTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreNodeTest.java
@@ -55,7 +55,6 @@ import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
-import org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.ignore.IgnoreNode.MatchResult;
import org.eclipse.jgit.junit.RepositoryTestCase;
import org.eclipse.jgit.lib.FileMode;
@@ -509,7 +508,7 @@ public class IgnoreNodeTest extends RepositoryTestCase {
.toString());
}
- private void beginWalk() throws CorruptObjectException {
+ private void beginWalk() {
walk = new TreeWalk(db);
walk.addTree(new FileTreeIterator(db));
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileRepositoryBuilderTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileRepositoryBuilderTest.java
index 2d72d2373b..dca356434b 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileRepositoryBuilderTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileRepositoryBuilderTest.java
@@ -107,7 +107,7 @@ public class FileRepositoryBuilderTest extends LocalDiskRepositoryTestCase {
Repository r = createWorkRepository();
StoredConfig config = r.getConfig();
config.setLong(ConfigConstants.CONFIG_CORE_SECTION, null,
- ConfigConstants.CONFIG_KEY_REPO_FORMAT_VERSION, 1);
+ ConfigConstants.CONFIG_KEY_REPO_FORMAT_VERSION, 999999);
config.save();
try {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java
index bc880a13ef..01d6ee68e8 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java
@@ -43,11 +43,13 @@
package org.eclipse.jgit.internal.storage.file;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static org.eclipse.jgit.internal.storage.pack.PackWriter.NONE;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@@ -66,19 +68,19 @@ import java.util.Set;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.internal.storage.file.PackIndex.MutableEntry;
import org.eclipse.jgit.internal.storage.pack.PackWriter;
-import org.eclipse.jgit.internal.storage.pack.PackWriter.ObjectIdSet;
import org.eclipse.jgit.junit.JGitTestUtil;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.junit.TestRepository.BranchBuilder;
-import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdSet;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.revwalk.RevBlob;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.storage.pack.PackConfig;
+import org.eclipse.jgit.storage.pack.PackStatistics;
import org.eclipse.jgit.test.resources.SampleDataRepositoryTestCase;
import org.eclipse.jgit.transport.PackParser;
import org.junit.After;
@@ -87,9 +89,6 @@ import org.junit.Test;
public class PackWriterTest extends SampleDataRepositoryTestCase {
- private static final Set<ObjectId> EMPTY_SET_OBJECT = Collections
- .<ObjectId> emptySet();
-
private static final List<RevObject> EMPTY_LIST_REVS = Collections
.<RevObject> emptyList();
@@ -170,7 +169,7 @@ public class PackWriterTest extends SampleDataRepositoryTestCase {
*/
@Test
public void testWriteEmptyPack1() throws IOException {
- createVerifyOpenPack(EMPTY_SET_OBJECT, EMPTY_SET_OBJECT, false, false);
+ createVerifyOpenPack(NONE, NONE, false, false);
assertEquals(0, writer.getObjectCount());
assertEquals(0, pack.getObjectCount());
@@ -203,8 +202,8 @@ public class PackWriterTest extends SampleDataRepositoryTestCase {
final ObjectId nonExisting = ObjectId
.fromString("0000000000000000000000000000000000000001");
try {
- createVerifyOpenPack(EMPTY_SET_OBJECT, Collections.singleton(
- nonExisting), false, false);
+ createVerifyOpenPack(NONE, Collections.singleton(nonExisting),
+ false, false);
fail("Should have thrown MissingObjectException");
} catch (MissingObjectException x) {
// expected
@@ -220,8 +219,8 @@ public class PackWriterTest extends SampleDataRepositoryTestCase {
public void testIgnoreNonExistingObjects() throws IOException {
final ObjectId nonExisting = ObjectId
.fromString("0000000000000000000000000000000000000001");
- createVerifyOpenPack(EMPTY_SET_OBJECT, Collections.singleton(
- nonExisting), false, true);
+ createVerifyOpenPack(NONE, Collections.singleton(nonExisting),
+ false, true);
// shouldn't throw anything
}
@@ -239,8 +238,8 @@ public class PackWriterTest extends SampleDataRepositoryTestCase {
final ObjectId nonExisting = ObjectId
.fromString("0000000000000000000000000000000000000001");
new GC(db).gc();
- createVerifyOpenPack(EMPTY_SET_OBJECT,
- Collections.singleton(nonExisting), false, true, true);
+ createVerifyOpenPack(NONE, Collections.singleton(nonExisting), false,
+ true, true);
// shouldn't throw anything
}
@@ -438,6 +437,38 @@ public class PackWriterTest extends SampleDataRepositoryTestCase {
}
@Test
+ public void testDeltaStatistics() throws Exception {
+ config.setDeltaCompress(true);
+ FileRepository repo = createBareRepository();
+ TestRepository<FileRepository> testRepo = new TestRepository<FileRepository>(repo);
+ ArrayList<RevObject> blobs = new ArrayList<>();
+ blobs.add(testRepo.blob(genDeltableData(1000)));
+ blobs.add(testRepo.blob(genDeltableData(1005)));
+
+ try (PackWriter pw = new PackWriter(repo)) {
+ NullProgressMonitor m = NullProgressMonitor.INSTANCE;
+ pw.preparePack(blobs.iterator());
+ pw.writePack(m, m, os);
+ PackStatistics stats = pw.getStatistics();
+ assertEquals(1, stats.getTotalDeltas());
+ assertTrue("Delta bytes not set.",
+ stats.byObjectType(OBJ_BLOB).getDeltaBytes() > 0);
+ }
+ }
+
+ // Generate consistent junk data for building files that delta well
+ private String genDeltableData(int length) {
+ assertTrue("Generated data must have a length > 0", length > 0);
+ char[] data = {'a', 'b', 'c', '\n'};
+ StringBuilder builder = new StringBuilder(length);
+ for (int i = 0; i < length; i++) {
+ builder.append(data[i % 4]);
+ }
+ return builder.toString();
+ }
+
+
+ @Test
public void testWriteIndex() throws Exception {
config.setIndexVersion(2);
writeVerifyPack4(false);
@@ -494,7 +525,7 @@ public class PackWriterTest extends SampleDataRepositoryTestCase {
RevCommit c2 = bb.commit().add("f", contentB).create();
testRepo.getRevWalk().parseHeaders(c2);
PackIndex pf2 = writePack(repo, Collections.singleton(c2),
- Collections.singleton(objectIdSet(pf1)));
+ Collections.<ObjectIdSet> singleton(pf1));
assertContent(
pf2,
Arrays.asList(c2.getId(), c2.getTree().getId(),
@@ -519,8 +550,7 @@ public class PackWriterTest extends SampleDataRepositoryTestCase {
pw.setReuseDeltaCommits(false);
for (ObjectIdSet idx : excludeObjects)
pw.excludeObjects(idx);
- pw.preparePack(NullProgressMonitor.INSTANCE, want,
- Collections.<ObjectId> emptySet());
+ pw.preparePack(NullProgressMonitor.INSTANCE, want, NONE);
String id = pw.computeName().getName();
File packdir = new File(repo.getObjectsDirectory(), "pack");
File packFile = new File(packdir, "pack-" + id + ".pack");
@@ -543,7 +573,7 @@ public class PackWriterTest extends SampleDataRepositoryTestCase {
final HashSet<ObjectId> interestings = new HashSet<ObjectId>();
interestings.add(ObjectId
.fromString("82c6b885ff600be425b4ea96dee75dca255b69e7"));
- createVerifyOpenPack(interestings, EMPTY_SET_OBJECT, false, false);
+ createVerifyOpenPack(interestings, NONE, false, false);
final ObjectId expectedOrder[] = new ObjectId[] {
ObjectId.fromString("82c6b885ff600be425b4ea96dee75dca255b69e7"),
@@ -699,12 +729,4 @@ public class PackWriterTest extends SampleDataRepositoryTestCase {
assertEquals(objectsOrder[i++].toObjectId(), me.toObjectId());
}
}
-
- private static ObjectIdSet objectIdSet(final PackIndex idx) {
- return new ObjectIdSet() {
- public boolean contains(AnyObjectId objectId) {
- return idx.hasObject(objectId);
- }
- };
- }
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java
index a92ff8d04e..f4d655f86b 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java
@@ -67,7 +67,6 @@ import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
-import org.eclipse.jgit.lib.FileTreeEntry;
import org.eclipse.jgit.lib.ObjectDatabase;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
@@ -75,7 +74,6 @@ import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.TagBuilder;
-import org.eclipse.jgit.lib.Tree;
import org.eclipse.jgit.lib.TreeFormatter;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTag;
@@ -420,29 +418,6 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase {
}
@Test
- public void test012_SubtreeExternalSorting() throws IOException {
- final ObjectId emptyBlob = insertEmptyBlob();
- final Tree t = new Tree(db);
- final FileTreeEntry e0 = t.addFile("a-");
- final FileTreeEntry e1 = t.addFile("a-b");
- final FileTreeEntry e2 = t.addFile("a/b");
- final FileTreeEntry e3 = t.addFile("a=");
- final FileTreeEntry e4 = t.addFile("a=b");
-
- e0.setId(emptyBlob);
- e1.setId(emptyBlob);
- e2.setId(emptyBlob);
- e3.setId(emptyBlob);
- e4.setId(emptyBlob);
-
- final Tree a = (Tree) t.findTreeMember("a");
- a.setId(insertTree(a));
- assertEquals(ObjectId
- .fromString("b47a8f0a4190f7572e11212769090523e23eb1ea"),
- insertTree(t));
- }
-
- @Test
public void test020_createBlobTag() throws IOException {
final ObjectId emptyId = insertEmptyBlob();
final TagBuilder t = new TagBuilder();
@@ -465,9 +440,8 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase {
@Test
public void test021_createTreeTag() throws IOException {
final ObjectId emptyId = insertEmptyBlob();
- final Tree almostEmptyTree = new Tree(db);
- almostEmptyTree.addEntry(new FileTreeEntry(almostEmptyTree, emptyId,
- "empty".getBytes(), false));
+ TreeFormatter almostEmptyTree = new TreeFormatter();
+ almostEmptyTree.append("empty", FileMode.REGULAR_FILE, emptyId);
final ObjectId almostEmptyTreeId = insertTree(almostEmptyTree);
final TagBuilder t = new TagBuilder();
t.setObjectId(almostEmptyTreeId, Constants.OBJ_TREE);
@@ -489,9 +463,8 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase {
@Test
public void test022_createCommitTag() throws IOException {
final ObjectId emptyId = insertEmptyBlob();
- final Tree almostEmptyTree = new Tree(db);
- almostEmptyTree.addEntry(new FileTreeEntry(almostEmptyTree, emptyId,
- "empty".getBytes(), false));
+ TreeFormatter almostEmptyTree = new TreeFormatter();
+ almostEmptyTree.append("empty", FileMode.REGULAR_FILE, emptyId);
final ObjectId almostEmptyTreeId = insertTree(almostEmptyTree);
final CommitBuilder almostEmptyCommit = new CommitBuilder();
almostEmptyCommit.setAuthor(new PersonIdent(author, 1154236443000L,
@@ -521,9 +494,8 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase {
@Test
public void test023_createCommitNonAnullii() throws IOException {
final ObjectId emptyId = insertEmptyBlob();
- final Tree almostEmptyTree = new Tree(db);
- almostEmptyTree.addEntry(new FileTreeEntry(almostEmptyTree, emptyId,
- "empty".getBytes(), false));
+ TreeFormatter almostEmptyTree = new TreeFormatter();
+ almostEmptyTree.append("empty", FileMode.REGULAR_FILE, emptyId);
final ObjectId almostEmptyTreeId = insertTree(almostEmptyTree);
CommitBuilder commit = new CommitBuilder();
commit.setTreeId(almostEmptyTreeId);
@@ -543,9 +515,8 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase {
@Test
public void test024_createCommitNonAscii() throws IOException {
final ObjectId emptyId = insertEmptyBlob();
- final Tree almostEmptyTree = new Tree(db);
- almostEmptyTree.addEntry(new FileTreeEntry(almostEmptyTree, emptyId,
- "empty".getBytes(), false));
+ TreeFormatter almostEmptyTree = new TreeFormatter();
+ almostEmptyTree.append("empty", FileMode.REGULAR_FILE, emptyId);
final ObjectId almostEmptyTreeId = insertTree(almostEmptyTree);
CommitBuilder commit = new CommitBuilder();
commit.setTreeId(almostEmptyTreeId);
@@ -747,14 +718,6 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase {
return emptyId;
}
- private ObjectId insertTree(Tree tree) throws IOException {
- try (ObjectInserter oi = db.newObjectInserter()) {
- ObjectId id = oi.insert(Constants.OBJ_TREE, tree.format());
- oi.flush();
- return id;
- }
- }
-
private ObjectId insertTree(TreeFormatter tree) throws IOException {
try (ObjectInserter oi = db.newObjectInserter()) {
ObjectId id = oi.insert(tree);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/LocalDiskRefTreeDatabaseTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/LocalDiskRefTreeDatabaseTest.java
new file mode 100644
index 0000000000..47f70d7bdb
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/LocalDiskRefTreeDatabaseTest.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.storage.reftree;
+
+import static org.eclipse.jgit.lib.Constants.HEAD;
+import static org.eclipse.jgit.lib.Constants.MASTER;
+import static org.eclipse.jgit.lib.Constants.ORIG_HEAD;
+import static org.eclipse.jgit.lib.Constants.R_HEADS;
+import static org.eclipse.jgit.lib.RefDatabase.ALL;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
+import org.junit.Before;
+import org.junit.Test;
+
+public class LocalDiskRefTreeDatabaseTest extends LocalDiskRepositoryTestCase {
+ private FileRepository repo;
+ private RefTreeDatabase refdb;
+ private RefDatabase bootstrap;
+
+ private TestRepository<FileRepository> testRepo;
+ private RevCommit A;
+ private RevCommit B;
+
+ @Before
+ public void setUp() throws Exception {
+ FileRepository init = createWorkRepository();
+ FileBasedConfig cfg = init.getConfig();
+ cfg.setInt("core", null, "repositoryformatversion", 1);
+ cfg.setString("extensions", null, "refsStorage", "reftree");
+ cfg.save();
+
+ repo = (FileRepository) new FileRepositoryBuilder()
+ .setGitDir(init.getDirectory())
+ .build();
+ refdb = (RefTreeDatabase) repo.getRefDatabase();
+ bootstrap = refdb.getBootstrap();
+ addRepoToClose(repo);
+
+ RefUpdate head = refdb.newUpdate(HEAD, true);
+ head.link(R_HEADS + MASTER);
+
+ testRepo = new TestRepository<>(init);
+ A = testRepo.commit().create();
+ B = testRepo.commit(testRepo.getRevWalk().parseCommit(A));
+ }
+
+ @Test
+ public void testHeadOrigHead() throws IOException {
+ RefUpdate master = refdb.newUpdate(HEAD, false);
+ master.setExpectedOldObjectId(ObjectId.zeroId());
+ master.setNewObjectId(A);
+ assertEquals(RefUpdate.Result.NEW, master.update());
+ assertEquals(A, refdb.exactRef(HEAD).getObjectId());
+
+ RefUpdate orig = refdb.newUpdate(ORIG_HEAD, true);
+ orig.setNewObjectId(B);
+ assertEquals(RefUpdate.Result.NEW, orig.update());
+
+ File origFile = new File(repo.getDirectory(), ORIG_HEAD);
+ assertEquals(B.name() + '\n', read(origFile));
+ assertEquals(B, bootstrap.exactRef(ORIG_HEAD).getObjectId());
+ assertEquals(B, refdb.exactRef(ORIG_HEAD).getObjectId());
+ assertFalse(refdb.getRefs(ALL).containsKey(ORIG_HEAD));
+
+ List<Ref> addl = refdb.getAdditionalRefs();
+ assertEquals(2, addl.size());
+ assertEquals(ORIG_HEAD, addl.get(1).getName());
+ assertEquals(B, addl.get(1).getObjectId());
+ }
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/RefTreeDatabaseTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/RefTreeDatabaseTest.java
new file mode 100644
index 0000000000..e4d0f1d291
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/RefTreeDatabaseTest.java
@@ -0,0 +1,718 @@
+/*
+ * Copyright (C) 2010, 2013, 2016 Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.storage.reftree;
+
+import static org.eclipse.jgit.lib.Constants.HEAD;
+import static org.eclipse.jgit.lib.Constants.ORIG_HEAD;
+import static org.eclipse.jgit.lib.Constants.R_HEADS;
+import static org.eclipse.jgit.lib.Constants.R_TAGS;
+import static org.eclipse.jgit.lib.Ref.Storage.LOOSE;
+import static org.eclipse.jgit.lib.Ref.Storage.PACKED;
+import static org.eclipse.jgit.lib.RefDatabase.ALL;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.LOCK_FAILURE;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_NONFASTFORWARD;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.SymbolicRef;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevTag;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.junit.Before;
+import org.junit.Test;
+
+public class RefTreeDatabaseTest {
+ private InMemRefTreeRepo repo;
+ private RefTreeDatabase refdb;
+ private RefDatabase bootstrap;
+
+ private TestRepository<InMemRefTreeRepo> testRepo;
+ private RevCommit A;
+ private RevCommit B;
+ private RevTag v1_0;
+
+ @Before
+ public void setUp() throws Exception {
+ repo = new InMemRefTreeRepo(new DfsRepositoryDescription("test"));
+ bootstrap = refdb.getBootstrap();
+
+ testRepo = new TestRepository<>(repo);
+ A = testRepo.commit().create();
+ B = testRepo.commit(testRepo.getRevWalk().parseCommit(A));
+ v1_0 = testRepo.tag("v1_0", B);
+ testRepo.getRevWalk().parseBody(v1_0);
+ }
+
+ @Test
+ public void testSupportsAtomic() {
+ assertTrue(refdb.performsAtomicTransactions());
+ }
+
+ @Test
+ public void testGetRefs_EmptyDatabase() throws IOException {
+ assertTrue("no references", refdb.getRefs(ALL).isEmpty());
+ assertTrue("no references", refdb.getRefs(R_HEADS).isEmpty());
+ assertTrue("no references", refdb.getRefs(R_TAGS).isEmpty());
+ assertTrue("no references", refdb.getAdditionalRefs().isEmpty());
+ }
+
+ @Test
+ public void testGetAdditionalRefs() throws IOException {
+ update("refs/heads/master", A);
+
+ List<Ref> addl = refdb.getAdditionalRefs();
+ assertEquals(1, addl.size());
+ assertEquals("refs/txn/committed", addl.get(0).getName());
+ assertEquals(getTxnCommitted(), addl.get(0).getObjectId());
+ }
+
+ @Test
+ public void testGetRefs_HeadOnOneBranch() throws IOException {
+ symref(HEAD, "refs/heads/master");
+ update("refs/heads/master", A);
+
+ Map<String, Ref> all = refdb.getRefs(ALL);
+ assertEquals(2, all.size());
+ assertTrue("has HEAD", all.containsKey(HEAD));
+ assertTrue("has master", all.containsKey("refs/heads/master"));
+
+ Ref head = all.get(HEAD);
+ Ref master = all.get("refs/heads/master");
+
+ assertEquals(HEAD, head.getName());
+ assertTrue(head.isSymbolic());
+ assertSame(LOOSE, head.getStorage());
+ assertSame("uses same ref as target", master, head.getTarget());
+
+ assertEquals("refs/heads/master", master.getName());
+ assertFalse(master.isSymbolic());
+ assertSame(PACKED, master.getStorage());
+ assertEquals(A, master.getObjectId());
+ }
+
+ @Test
+ public void testGetRefs_DetachedHead() throws IOException {
+ update(HEAD, A);
+
+ Map<String, Ref> all = refdb.getRefs(ALL);
+ assertEquals(1, all.size());
+ assertTrue("has HEAD", all.containsKey(HEAD));
+
+ Ref head = all.get(HEAD);
+ assertEquals(HEAD, head.getName());
+ assertFalse(head.isSymbolic());
+ assertSame(PACKED, head.getStorage());
+ assertEquals(A, head.getObjectId());
+ }
+
+ @Test
+ public void testGetRefs_DeeplyNestedBranch() throws IOException {
+ String name = "refs/heads/a/b/c/d/e/f/g/h/i/j/k";
+ update(name, A);
+
+ Map<String, Ref> all = refdb.getRefs(ALL);
+ assertEquals(1, all.size());
+
+ Ref r = all.get(name);
+ assertEquals(name, r.getName());
+ assertFalse(r.isSymbolic());
+ assertSame(PACKED, r.getStorage());
+ assertEquals(A, r.getObjectId());
+ }
+
+ @Test
+ public void testGetRefs_HeadBranchNotBorn() throws IOException {
+ update("refs/heads/A", A);
+ update("refs/heads/B", B);
+
+ Map<String, Ref> all = refdb.getRefs(ALL);
+ assertEquals(2, all.size());
+ assertFalse("no HEAD", all.containsKey(HEAD));
+
+ Ref a = all.get("refs/heads/A");
+ Ref b = all.get("refs/heads/B");
+
+ assertEquals(A, a.getObjectId());
+ assertEquals(B, b.getObjectId());
+
+ assertEquals("refs/heads/A", a.getName());
+ assertEquals("refs/heads/B", b.getName());
+ }
+
+ @Test
+ public void testGetRefs_HeadsOnly() throws IOException {
+ update("refs/heads/A", A);
+ update("refs/heads/B", B);
+ update("refs/tags/v1.0", v1_0);
+
+ Map<String, Ref> heads = refdb.getRefs(R_HEADS);
+ assertEquals(2, heads.size());
+
+ Ref a = heads.get("A");
+ Ref b = heads.get("B");
+
+ assertEquals("refs/heads/A", a.getName());
+ assertEquals("refs/heads/B", b.getName());
+
+ assertEquals(A, a.getObjectId());
+ assertEquals(B, b.getObjectId());
+ }
+
+ @Test
+ public void testGetRefs_TagsOnly() throws IOException {
+ update("refs/heads/A", A);
+ update("refs/heads/B", B);
+ update("refs/tags/v1.0", v1_0);
+
+ Map<String, Ref> tags = refdb.getRefs(R_TAGS);
+ assertEquals(1, tags.size());
+
+ Ref a = tags.get("v1.0");
+ assertEquals("refs/tags/v1.0", a.getName());
+ assertEquals(v1_0, a.getObjectId());
+ assertTrue(a.isPeeled());
+ assertEquals(v1_0.getObject(), a.getPeeledObjectId());
+ }
+
+ @Test
+ public void testGetRefs_HeadsSymref() throws IOException {
+ symref("refs/heads/other", "refs/heads/master");
+ update("refs/heads/master", A);
+
+ Map<String, Ref> heads = refdb.getRefs(R_HEADS);
+ assertEquals(2, heads.size());
+
+ Ref master = heads.get("master");
+ Ref other = heads.get("other");
+
+ assertEquals("refs/heads/master", master.getName());
+ assertEquals(A, master.getObjectId());
+
+ assertEquals("refs/heads/other", other.getName());
+ assertEquals(A, other.getObjectId());
+ assertSame(master, other.getTarget());
+ }
+
+ @Test
+ public void testGetRefs_InvalidPrefixes() throws IOException {
+ update("refs/heads/A", A);
+
+ assertTrue("empty refs/heads", refdb.getRefs("refs/heads").isEmpty());
+ assertTrue("empty objects", refdb.getRefs("objects").isEmpty());
+ assertTrue("empty objects/", refdb.getRefs("objects/").isEmpty());
+ }
+
+ @Test
+ public void testGetRefs_DiscoversNew() throws IOException {
+ update("refs/heads/master", A);
+ Map<String, Ref> orig = refdb.getRefs(ALL);
+
+ update("refs/heads/next", B);
+ Map<String, Ref> next = refdb.getRefs(ALL);
+
+ assertEquals(1, orig.size());
+ assertEquals(2, next.size());
+
+ assertFalse(orig.containsKey("refs/heads/next"));
+ assertTrue(next.containsKey("refs/heads/next"));
+
+ assertEquals(A, next.get("refs/heads/master").getObjectId());
+ assertEquals(B, next.get("refs/heads/next").getObjectId());
+ }
+
+ @Test
+ public void testGetRefs_DiscoversModified() throws IOException {
+ symref(HEAD, "refs/heads/master");
+ update("refs/heads/master", A);
+
+ Map<String, Ref> all = refdb.getRefs(ALL);
+ assertEquals(A, all.get(HEAD).getObjectId());
+
+ update("refs/heads/master", B);
+ all = refdb.getRefs(ALL);
+ assertEquals(B, all.get(HEAD).getObjectId());
+ assertEquals(B, refdb.exactRef(HEAD).getObjectId());
+ }
+
+ @Test
+ public void testGetRefs_CycleInSymbolicRef() throws IOException {
+ symref("refs/1", "refs/2");
+ symref("refs/2", "refs/3");
+ symref("refs/3", "refs/4");
+ symref("refs/4", "refs/5");
+ symref("refs/5", "refs/end");
+ update("refs/end", A);
+
+ Map<String, Ref> all = refdb.getRefs(ALL);
+ Ref r = all.get("refs/1");
+ assertNotNull("has 1", r);
+
+ assertEquals("refs/1", r.getName());
+ assertEquals(A, r.getObjectId());
+ assertTrue(r.isSymbolic());
+
+ r = r.getTarget();
+ assertEquals("refs/2", r.getName());
+ assertEquals(A, r.getObjectId());
+ assertTrue(r.isSymbolic());
+
+ r = r.getTarget();
+ assertEquals("refs/3", r.getName());
+ assertEquals(A, r.getObjectId());
+ assertTrue(r.isSymbolic());
+
+ r = r.getTarget();
+ assertEquals("refs/4", r.getName());
+ assertEquals(A, r.getObjectId());
+ assertTrue(r.isSymbolic());
+
+ r = r.getTarget();
+ assertEquals("refs/5", r.getName());
+ assertEquals(A, r.getObjectId());
+ assertTrue(r.isSymbolic());
+
+ r = r.getTarget();
+ assertEquals("refs/end", r.getName());
+ assertEquals(A, r.getObjectId());
+ assertFalse(r.isSymbolic());
+
+ symref("refs/5", "refs/6");
+ symref("refs/6", "refs/end");
+ all = refdb.getRefs(ALL);
+ assertNull("mising 1 due to cycle", all.get("refs/1"));
+ assertEquals(A, all.get("refs/2").getObjectId());
+ assertEquals(A, all.get("refs/3").getObjectId());
+ assertEquals(A, all.get("refs/4").getObjectId());
+ assertEquals(A, all.get("refs/5").getObjectId());
+ assertEquals(A, all.get("refs/6").getObjectId());
+ assertEquals(A, all.get("refs/end").getObjectId());
+ }
+
+ @Test
+ public void testGetRef_NonExistingBranchConfig() throws IOException {
+ assertNull("find branch config", refdb.getRef("config"));
+ assertNull("find branch config", refdb.getRef("refs/heads/config"));
+ }
+
+ @Test
+ public void testGetRef_FindBranchConfig() throws IOException {
+ update("refs/heads/config", A);
+
+ for (String t : new String[] { "config", "refs/heads/config" }) {
+ Ref r = refdb.getRef(t);
+ assertNotNull("find branch config (" + t + ")", r);
+ assertEquals("for " + t, "refs/heads/config", r.getName());
+ assertEquals("for " + t, A, r.getObjectId());
+ }
+ }
+
+ @Test
+ public void testFirstExactRef() throws IOException {
+ update("refs/heads/A", A);
+ update("refs/tags/v1.0", v1_0);
+
+ Ref a = refdb.firstExactRef("refs/heads/A", "refs/tags/v1.0");
+ Ref one = refdb.firstExactRef("refs/tags/v1.0", "refs/heads/A");
+
+ assertEquals("refs/heads/A", a.getName());
+ assertEquals("refs/tags/v1.0", one.getName());
+
+ assertEquals(A, a.getObjectId());
+ assertEquals(v1_0, one.getObjectId());
+ }
+
+ @Test
+ public void testExactRef_DiscoversModified() throws IOException {
+ symref(HEAD, "refs/heads/master");
+ update("refs/heads/master", A);
+ assertEquals(A, refdb.exactRef(HEAD).getObjectId());
+
+ update("refs/heads/master", B);
+ assertEquals(B, refdb.exactRef(HEAD).getObjectId());
+ }
+
+ @Test
+ public void testIsNameConflicting() throws IOException {
+ update("refs/heads/a/b", A);
+ update("refs/heads/q", B);
+
+ // new references cannot replace an existing container
+ assertTrue(refdb.isNameConflicting("refs"));
+ assertTrue(refdb.isNameConflicting("refs/heads"));
+ assertTrue(refdb.isNameConflicting("refs/heads/a"));
+
+ // existing reference is not conflicting
+ assertFalse(refdb.isNameConflicting("refs/heads/a/b"));
+
+ // new references are not conflicting
+ assertFalse(refdb.isNameConflicting("refs/heads/a/d"));
+ assertFalse(refdb.isNameConflicting("refs/heads/master"));
+
+ // existing reference must not be used as a container
+ assertTrue(refdb.isNameConflicting("refs/heads/a/b/c"));
+ assertTrue(refdb.isNameConflicting("refs/heads/q/master"));
+
+ // refs/txn/ names always conflict.
+ assertTrue(refdb.isNameConflicting(refdb.getTxnCommitted()));
+ assertTrue(refdb.isNameConflicting("refs/txn/foo"));
+ }
+
+ @Test
+ public void testUpdate_RefusesRefsTxnNamespace() throws IOException {
+ ObjectId txnId = getTxnCommitted();
+
+ RefUpdate u = refdb.newUpdate("refs/txn/tmp", false);
+ u.setNewObjectId(B);
+ assertEquals(RefUpdate.Result.LOCK_FAILURE, u.update());
+ assertEquals(txnId, getTxnCommitted());
+
+ ReceiveCommand cmd = command(null, B, "refs/txn/tmp");
+ BatchRefUpdate batch = refdb.newBatchUpdate();
+ batch.addCommand(cmd);
+ batch.execute(new RevWalk(repo), NullProgressMonitor.INSTANCE);
+
+ assertEquals(REJECTED_OTHER_REASON, cmd.getResult());
+ assertEquals(MessageFormat.format(JGitText.get().invalidRefName,
+ "refs/txn/tmp"), cmd.getMessage());
+ assertEquals(txnId, getTxnCommitted());
+ }
+
+ @Test
+ public void testUpdate_RefusesDotLockInRefName() throws IOException {
+ ObjectId txnId = getTxnCommitted();
+
+ RefUpdate u = refdb.newUpdate("refs/heads/pu.lock", false);
+ u.setNewObjectId(B);
+ assertEquals(RefUpdate.Result.REJECTED, u.update());
+ assertEquals(txnId, getTxnCommitted());
+
+ ReceiveCommand cmd = command(null, B, "refs/heads/pu.lock");
+ BatchRefUpdate batch = refdb.newBatchUpdate();
+ batch.addCommand(cmd);
+ batch.execute(new RevWalk(repo), NullProgressMonitor.INSTANCE);
+
+ assertEquals(REJECTED_OTHER_REASON, cmd.getResult());
+ assertEquals(JGitText.get().funnyRefname, cmd.getMessage());
+ assertEquals(txnId, getTxnCommitted());
+ }
+
+ @Test
+ public void testUpdate_RefusesOrigHeadOnBare() throws IOException {
+ assertTrue(refdb.getRepository().isBare());
+ ObjectId txnId = getTxnCommitted();
+
+ RefUpdate orig = refdb.newUpdate(ORIG_HEAD, true);
+ orig.setNewObjectId(B);
+ assertEquals(RefUpdate.Result.LOCK_FAILURE, orig.update());
+ assertEquals(txnId, getTxnCommitted());
+
+ ReceiveCommand cmd = command(null, B, ORIG_HEAD);
+ BatchRefUpdate batch = refdb.newBatchUpdate();
+ batch.addCommand(cmd);
+ batch.execute(new RevWalk(repo), NullProgressMonitor.INSTANCE);
+ assertEquals(REJECTED_OTHER_REASON, cmd.getResult());
+ assertEquals(
+ MessageFormat.format(JGitText.get().invalidRefName, ORIG_HEAD),
+ cmd.getMessage());
+ assertEquals(txnId, getTxnCommitted());
+ }
+
+ @Test
+ public void testBatchRefUpdate_NonFastForwardAborts() throws IOException {
+ update("refs/heads/master", A);
+ update("refs/heads/masters", B);
+ ObjectId txnId = getTxnCommitted();
+
+ List<ReceiveCommand> commands = Arrays.asList(
+ command(A, B, "refs/heads/master"),
+ command(B, A, "refs/heads/masters"));
+ BatchRefUpdate batchUpdate = refdb.newBatchUpdate();
+ batchUpdate.addCommand(commands);
+ batchUpdate.execute(new RevWalk(repo), NullProgressMonitor.INSTANCE);
+ assertEquals(txnId, getTxnCommitted());
+
+ assertEquals(REJECTED_NONFASTFORWARD,
+ commands.get(1).getResult());
+ assertEquals(REJECTED_OTHER_REASON,
+ commands.get(0).getResult());
+ assertEquals(JGitText.get().transactionAborted,
+ commands.get(0).getMessage());
+ }
+
+ @Test
+ public void testBatchRefUpdate_ForceUpdate() throws IOException {
+ update("refs/heads/master", A);
+ update("refs/heads/masters", B);
+ ObjectId txnId = getTxnCommitted();
+
+ List<ReceiveCommand> commands = Arrays.asList(
+ command(A, B, "refs/heads/master"),
+ command(B, A, "refs/heads/masters"));
+ BatchRefUpdate batchUpdate = refdb.newBatchUpdate();
+ batchUpdate.setAllowNonFastForwards(true);
+ batchUpdate.addCommand(commands);
+ batchUpdate.execute(new RevWalk(repo), NullProgressMonitor.INSTANCE);
+ assertNotEquals(txnId, getTxnCommitted());
+
+ Map<String, Ref> refs = refdb.getRefs(ALL);
+ assertEquals(OK, commands.get(0).getResult());
+ assertEquals(OK, commands.get(1).getResult());
+ assertEquals(
+ "[refs/heads/master, refs/heads/masters]",
+ refs.keySet().toString());
+ assertEquals(B.getId(), refs.get("refs/heads/master").getObjectId());
+ assertEquals(A.getId(), refs.get("refs/heads/masters").getObjectId());
+ }
+
+ @Test
+ public void testBatchRefUpdate_NonFastForwardDoesNotDoExpensiveMergeCheck()
+ throws IOException {
+ update("refs/heads/master", B);
+ ObjectId txnId = getTxnCommitted();
+
+ List<ReceiveCommand> commands = Arrays.asList(
+ command(B, A, "refs/heads/master"));
+ BatchRefUpdate batchUpdate = refdb.newBatchUpdate();
+ batchUpdate.setAllowNonFastForwards(true);
+ batchUpdate.addCommand(commands);
+ batchUpdate.execute(new RevWalk(repo) {
+ @Override
+ public boolean isMergedInto(RevCommit base, RevCommit tip) {
+ fail("isMergedInto() should not be called");
+ return false;
+ }
+ }, NullProgressMonitor.INSTANCE);
+ assertNotEquals(txnId, getTxnCommitted());
+
+ Map<String, Ref> refs = refdb.getRefs(ALL);
+ assertEquals(OK, commands.get(0).getResult());
+ assertEquals(A.getId(), refs.get("refs/heads/master").getObjectId());
+ }
+
+ @Test
+ public void testBatchRefUpdate_ConflictCausesAbort() throws IOException {
+ update("refs/heads/master", A);
+ update("refs/heads/masters", B);
+ ObjectId txnId = getTxnCommitted();
+
+ List<ReceiveCommand> commands = Arrays.asList(
+ command(A, B, "refs/heads/master"),
+ command(null, A, "refs/heads/master/x"),
+ command(null, A, "refs/heads"));
+ BatchRefUpdate batchUpdate = refdb.newBatchUpdate();
+ batchUpdate.setAllowNonFastForwards(true);
+ batchUpdate.addCommand(commands);
+ batchUpdate.execute(new RevWalk(repo), NullProgressMonitor.INSTANCE);
+ assertEquals(txnId, getTxnCommitted());
+
+ assertEquals(LOCK_FAILURE, commands.get(0).getResult());
+
+ assertEquals(REJECTED_OTHER_REASON, commands.get(1).getResult());
+ assertEquals(JGitText.get().transactionAborted,
+ commands.get(1).getMessage());
+
+ assertEquals(REJECTED_OTHER_REASON, commands.get(2).getResult());
+ assertEquals(JGitText.get().transactionAborted,
+ commands.get(2).getMessage());
+ }
+
+ @Test
+ public void testBatchRefUpdate_NoConflictIfDeleted() throws IOException {
+ update("refs/heads/master", A);
+ update("refs/heads/masters", B);
+ ObjectId txnId = getTxnCommitted();
+
+ List<ReceiveCommand> commands = Arrays.asList(
+ command(A, B, "refs/heads/master"),
+ command(null, A, "refs/heads/masters/x"),
+ command(B, null, "refs/heads/masters"));
+ BatchRefUpdate batchUpdate = refdb.newBatchUpdate();
+ batchUpdate.setAllowNonFastForwards(true);
+ batchUpdate.addCommand(commands);
+ batchUpdate.execute(new RevWalk(repo), NullProgressMonitor.INSTANCE);
+ assertNotEquals(txnId, getTxnCommitted());
+
+ assertEquals(OK, commands.get(0).getResult());
+ assertEquals(OK, commands.get(1).getResult());
+ assertEquals(OK, commands.get(2).getResult());
+
+ Map<String, Ref> refs = refdb.getRefs(ALL);
+ assertEquals(
+ "[refs/heads/master, refs/heads/masters/x]",
+ refs.keySet().toString());
+ assertEquals(A.getId(), refs.get("refs/heads/masters/x").getObjectId());
+ }
+
+ private ObjectId getTxnCommitted() throws IOException {
+ Ref r = bootstrap.exactRef(refdb.getTxnCommitted());
+ if (r != null && r.getObjectId() != null) {
+ return r.getObjectId();
+ }
+ return ObjectId.zeroId();
+ }
+
+ private static ReceiveCommand command(AnyObjectId a, AnyObjectId b,
+ String name) {
+ return new ReceiveCommand(
+ a != null ? a.copy() : ObjectId.zeroId(),
+ b != null ? b.copy() : ObjectId.zeroId(),
+ name);
+ }
+
+ private void symref(final String name, final String dst)
+ throws IOException {
+ commit(new Function() {
+ @Override
+ public boolean apply(ObjectReader reader, RefTree tree)
+ throws IOException {
+ Ref old = tree.exactRef(reader, name);
+ Command n = new Command(
+ old,
+ new SymbolicRef(
+ name,
+ new ObjectIdRef.Unpeeled(Ref.Storage.NEW, dst, null)));
+ return tree.apply(Collections.singleton(n));
+ }
+ });
+ }
+
+ private void update(final String name, final ObjectId id)
+ throws IOException {
+ commit(new Function() {
+ @Override
+ public boolean apply(ObjectReader reader, RefTree tree)
+ throws IOException {
+ Ref old = tree.exactRef(reader, name);
+ Command n;
+ try (RevWalk rw = new RevWalk(repo)) {
+ n = new Command(old, Command.toRef(rw, id, name, true));
+ }
+ return tree.apply(Collections.singleton(n));
+ }
+ });
+ }
+
+ interface Function {
+ boolean apply(ObjectReader reader, RefTree tree) throws IOException;
+ }
+
+ private void commit(Function fun) throws IOException {
+ try (ObjectReader reader = repo.newObjectReader();
+ ObjectInserter inserter = repo.newObjectInserter();
+ RevWalk rw = new RevWalk(reader)) {
+ RefUpdate u = bootstrap.newUpdate(refdb.getTxnCommitted(), false);
+ CommitBuilder cb = new CommitBuilder();
+ testRepo.setAuthorAndCommitter(cb);
+
+ Ref ref = bootstrap.exactRef(refdb.getTxnCommitted());
+ RefTree tree;
+ if (ref != null && ref.getObjectId() != null) {
+ tree = RefTree.read(reader, rw.parseTree(ref.getObjectId()));
+ cb.setParentId(ref.getObjectId());
+ u.setExpectedOldObjectId(ref.getObjectId());
+ } else {
+ tree = RefTree.newEmptyTree();
+ u.setExpectedOldObjectId(ObjectId.zeroId());
+ }
+
+ assertTrue(fun.apply(reader, tree));
+ cb.setTreeId(tree.writeTree(inserter));
+ u.setNewObjectId(inserter.insert(cb));
+ inserter.flush();
+ switch (u.update(rw)) {
+ case NEW:
+ case FAST_FORWARD:
+ break;
+ default:
+ fail("Expected " + u.getName() + " to update");
+ }
+ }
+ }
+
+ private class InMemRefTreeRepo extends InMemoryRepository {
+ private final RefTreeDatabase refs;
+
+ InMemRefTreeRepo(DfsRepositoryDescription repoDesc) {
+ super(repoDesc);
+ refs = new RefTreeDatabase(this, super.getRefDatabase(),
+ "refs/txn/committed");
+ RefTreeDatabaseTest.this.refdb = refs;
+ }
+
+ public RefDatabase getRefDatabase() {
+ return refs;
+ }
+ }
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/RefTreeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/RefTreeTest.java
new file mode 100644
index 0000000000..8e0f38c69a
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/RefTreeTest.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.storage.reftree;
+
+import static org.eclipse.jgit.lib.Constants.HEAD;
+import static org.eclipse.jgit.lib.Constants.R_HEADS;
+import static org.eclipse.jgit.lib.Constants.R_TAGS;
+import static org.eclipse.jgit.lib.Ref.Storage.LOOSE;
+import static org.eclipse.jgit.lib.Ref.Storage.NEW;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.LOCK_FAILURE;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.SymbolicRef;
+import org.eclipse.jgit.revwalk.RevBlob;
+import org.eclipse.jgit.revwalk.RevTag;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.junit.Before;
+import org.junit.Test;
+
+public class RefTreeTest {
+ private static final String R_MASTER = R_HEADS + "master";
+ private InMemoryRepository repo;
+ private TestRepository<InMemoryRepository> git;
+
+ @Before
+ public void setUp() throws IOException {
+ repo = new InMemoryRepository(new DfsRepositoryDescription("RefTree"));
+ git = new TestRepository<>(repo);
+ }
+
+ @Test
+ public void testEmptyTree() throws IOException {
+ RefTree tree = RefTree.newEmptyTree();
+ try (ObjectReader reader = repo.newObjectReader()) {
+ assertNull(HEAD, tree.exactRef(reader, HEAD));
+ assertNull("master", tree.exactRef(reader, R_MASTER));
+ }
+ }
+
+ @Test
+ public void testApplyThenReadMaster() throws Exception {
+ RefTree tree = RefTree.newEmptyTree();
+ RevBlob id = git.blob("A");
+ Command cmd = new Command(null, ref(R_MASTER, id));
+ assertTrue(tree.apply(Collections.singletonList(cmd)));
+ assertSame(NOT_ATTEMPTED, cmd.getResult());
+
+ try (ObjectReader reader = repo.newObjectReader()) {
+ Ref m = tree.exactRef(reader, R_MASTER);
+ assertNotNull(R_MASTER, m);
+ assertEquals(R_MASTER, m.getName());
+ assertEquals(id, m.getObjectId());
+ assertTrue("peeled", m.isPeeled());
+ }
+ }
+
+ @Test
+ public void testUpdateMaster() throws Exception {
+ RefTree tree = RefTree.newEmptyTree();
+ RevBlob id1 = git.blob("A");
+ Command cmd1 = new Command(null, ref(R_MASTER, id1));
+ assertTrue(tree.apply(Collections.singletonList(cmd1)));
+ assertSame(NOT_ATTEMPTED, cmd1.getResult());
+
+ RevBlob id2 = git.blob("B");
+ Command cmd2 = new Command(ref(R_MASTER, id1), ref(R_MASTER, id2));
+ assertTrue(tree.apply(Collections.singletonList(cmd2)));
+ assertSame(NOT_ATTEMPTED, cmd2.getResult());
+
+ try (ObjectReader reader = repo.newObjectReader()) {
+ Ref m = tree.exactRef(reader, R_MASTER);
+ assertNotNull(R_MASTER, m);
+ assertEquals(R_MASTER, m.getName());
+ assertEquals(id2, m.getObjectId());
+ assertTrue("peeled", m.isPeeled());
+ }
+ }
+
+ @Test
+ public void testHeadSymref() throws Exception {
+ RefTree tree = RefTree.newEmptyTree();
+ RevBlob id = git.blob("A");
+ Command cmd1 = new Command(null, ref(R_MASTER, id));
+ Command cmd2 = new Command(null, symref(HEAD, R_MASTER));
+ assertTrue(tree.apply(Arrays.asList(new Command[] { cmd1, cmd2 })));
+ assertSame(NOT_ATTEMPTED, cmd1.getResult());
+ assertSame(NOT_ATTEMPTED, cmd2.getResult());
+
+ try (ObjectReader reader = repo.newObjectReader()) {
+ Ref m = tree.exactRef(reader, HEAD);
+ assertNotNull(HEAD, m);
+ assertEquals(HEAD, m.getName());
+ assertTrue("symbolic", m.isSymbolic());
+ assertNotNull(m.getTarget());
+ assertEquals(R_MASTER, m.getTarget().getName());
+ assertEquals(id, m.getTarget().getObjectId());
+ }
+
+ // Writing flushes some buffers, re-read from blob.
+ ObjectId newId = write(tree);
+ try (ObjectReader reader = repo.newObjectReader();
+ RevWalk rw = new RevWalk(reader)) {
+ tree = RefTree.read(reader, rw.parseTree(newId));
+ Ref m = tree.exactRef(reader, HEAD);
+ assertEquals(R_MASTER, m.getTarget().getName());
+ }
+ }
+
+ @Test
+ public void testTagIsPeeled() throws Exception {
+ String name = "v1.0";
+ RefTree tree = RefTree.newEmptyTree();
+ RevBlob id = git.blob("A");
+ RevTag tag = git.tag(name, id);
+
+ String ref = R_TAGS + name;
+ Command cmd = create(ref, tag);
+ assertTrue(tree.apply(Collections.singletonList(cmd)));
+ assertSame(NOT_ATTEMPTED, cmd.getResult());
+
+ try (ObjectReader reader = repo.newObjectReader()) {
+ Ref m = tree.exactRef(reader, ref);
+ assertNotNull(ref, m);
+ assertEquals(ref, m.getName());
+ assertEquals(tag, m.getObjectId());
+ assertTrue("peeled", m.isPeeled());
+ assertEquals(id, m.getPeeledObjectId());
+ }
+ }
+
+ @Test
+ public void testApplyAlreadyExists() throws Exception {
+ RefTree tree = RefTree.newEmptyTree();
+ RevBlob a = git.blob("A");
+ Command cmd = new Command(null, ref(R_MASTER, a));
+ assertTrue(tree.apply(Collections.singletonList(cmd)));
+ ObjectId treeId = write(tree);
+
+ RevBlob b = git.blob("B");
+ Command cmd1 = create(R_MASTER, b);
+ Command cmd2 = create(R_MASTER, b);
+ assertFalse(tree.apply(Arrays.asList(new Command[] { cmd1, cmd2 })));
+ assertSame(LOCK_FAILURE, cmd1.getResult());
+ assertSame(REJECTED_OTHER_REASON, cmd2.getResult());
+ assertEquals(JGitText.get().transactionAborted, cmd2.getMessage());
+ assertEquals(treeId, write(tree));
+ }
+
+ @Test
+ public void testApplyWrongOldId() throws Exception {
+ RefTree tree = RefTree.newEmptyTree();
+ RevBlob a = git.blob("A");
+ Command cmd = new Command(null, ref(R_MASTER, a));
+ assertTrue(tree.apply(Collections.singletonList(cmd)));
+ ObjectId treeId = write(tree);
+
+ RevBlob b = git.blob("B");
+ RevBlob c = git.blob("C");
+ Command cmd1 = update(R_MASTER, b, c);
+ Command cmd2 = create(R_MASTER, b);
+ assertFalse(tree.apply(Arrays.asList(new Command[] { cmd1, cmd2 })));
+ assertSame(LOCK_FAILURE, cmd1.getResult());
+ assertSame(REJECTED_OTHER_REASON, cmd2.getResult());
+ assertEquals(JGitText.get().transactionAborted, cmd2.getMessage());
+ assertEquals(treeId, write(tree));
+ }
+
+ @Test
+ public void testApplyWrongOldIdButAlreadyCurrentIsNoOp() throws Exception {
+ RefTree tree = RefTree.newEmptyTree();
+ RevBlob a = git.blob("A");
+ Command cmd = new Command(null, ref(R_MASTER, a));
+ assertTrue(tree.apply(Collections.singletonList(cmd)));
+ ObjectId treeId = write(tree);
+
+ RevBlob b = git.blob("B");
+ cmd = update(R_MASTER, b, a);
+ assertTrue(tree.apply(Collections.singletonList(cmd)));
+ assertEquals(treeId, write(tree));
+ }
+
+ @Test
+ public void testApplyCannotCreateSubdirectory() throws Exception {
+ RefTree tree = RefTree.newEmptyTree();
+ RevBlob a = git.blob("A");
+ Command cmd = new Command(null, ref(R_MASTER, a));
+ assertTrue(tree.apply(Collections.singletonList(cmd)));
+ ObjectId treeId = write(tree);
+
+ RevBlob b = git.blob("B");
+ Command cmd1 = create(R_MASTER + "/fail", b);
+ assertFalse(tree.apply(Collections.singletonList(cmd1)));
+ assertSame(LOCK_FAILURE, cmd1.getResult());
+ assertEquals(treeId, write(tree));
+ }
+
+ @Test
+ public void testApplyCannotCreateParentRef() throws Exception {
+ RefTree tree = RefTree.newEmptyTree();
+ RevBlob a = git.blob("A");
+ Command cmd = new Command(null, ref(R_MASTER, a));
+ assertTrue(tree.apply(Collections.singletonList(cmd)));
+ ObjectId treeId = write(tree);
+
+ RevBlob b = git.blob("B");
+ Command cmd1 = create("refs/heads", b);
+ assertFalse(tree.apply(Collections.singletonList(cmd1)));
+ assertSame(LOCK_FAILURE, cmd1.getResult());
+ assertEquals(treeId, write(tree));
+ }
+
+ private static Ref ref(String name, ObjectId id) {
+ return new ObjectIdRef.PeeledNonTag(LOOSE, name, id);
+ }
+
+ private static Ref symref(String name, String dest) {
+ Ref d = new ObjectIdRef.PeeledNonTag(NEW, dest, null);
+ return new SymbolicRef(name, d);
+ }
+
+ private Command create(String name, ObjectId id)
+ throws MissingObjectException, IOException {
+ return update(name, ObjectId.zeroId(), id);
+ }
+
+ private Command update(String name, ObjectId oldId, ObjectId newId)
+ throws MissingObjectException, IOException {
+ try (RevWalk rw = new RevWalk(repo)) {
+ return new Command(rw, new ReceiveCommand(oldId, newId, name));
+ }
+ }
+
+ private ObjectId write(RefTree tree) throws IOException {
+ try (ObjectInserter ins = repo.newObjectInserter()) {
+ ObjectId id = tree.writeTree(ins);
+ ins.flush();
+ return id;
+ }
+ }
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutTest.java
index d768e0fa0b..92901f826b 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutTest.java
@@ -1084,7 +1084,7 @@ public class DirCacheCheckoutTest extends RepositoryTestCase {
assertWorkDir(mkmap(linkName, "a", fname, "a"));
Status st = git.status().call();
- assertFalse(st.isClean());
+ assertTrue(st.isClean());
}
@Test
@@ -1213,9 +1213,7 @@ public class DirCacheCheckoutTest extends RepositoryTestCase {
assertWorkDir(mkmap(fname, "a"));
Status st = git.status().call();
- assertFalse(st.isClean());
- assertEquals(1, st.getAdded().size());
- assertTrue(st.getAdded().contains(fname + "/dir/file1"));
+ assertTrue(st.isClean());
}
@Test
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexDiffTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexDiffTest.java
index a5cd7b5c0b..7fcee3dc82 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexDiffTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexDiffTest.java
@@ -99,8 +99,7 @@ public class IndexDiffTest extends RepositoryTestCase {
public void testAdded() throws IOException {
writeTrashFile("file1", "file1");
writeTrashFile("dir/subfile", "dir/subfile");
- Tree tree = new Tree(db);
- tree.setId(insertTree(tree));
+ ObjectId tree = insertTree(new TreeFormatter());
DirCache index = db.lockDirCache();
DirCacheEditor editor = index.editor();
@@ -108,7 +107,7 @@ public class IndexDiffTest extends RepositoryTestCase {
editor.add(add(db, trash, "dir/subfile"));
editor.commit();
FileTreeIterator iterator = new FileTreeIterator(db);
- IndexDiff diff = new IndexDiff(db, tree.getId(), iterator);
+ IndexDiff diff = new IndexDiff(db, tree, iterator);
diff.diff();
assertEquals(2, diff.getAdded().size());
assertTrue(diff.getAdded().contains("file1"));
@@ -124,18 +123,16 @@ public class IndexDiffTest extends RepositoryTestCase {
writeTrashFile("file2", "file2");
writeTrashFile("dir/file3", "dir/file3");
- Tree tree = new Tree(db);
- tree.addFile("file2");
- tree.addFile("dir/file3");
- assertEquals(2, tree.memberCount());
- tree.findBlobMember("file2").setId(ObjectId.fromString("30d67d4672d5c05833b7192cc77a79eaafb5c7ad"));
- Tree tree2 = (Tree) tree.findTreeMember("dir");
- tree2.findBlobMember("file3").setId(ObjectId.fromString("873fb8d667d05436d728c52b1d7a09528e6eb59b"));
- tree2.setId(insertTree(tree2));
- tree.setId(insertTree(tree));
+ TreeFormatter dir = new TreeFormatter();
+ dir.append("file3", FileMode.REGULAR_FILE, ObjectId.fromString("873fb8d667d05436d728c52b1d7a09528e6eb59b"));
+
+ TreeFormatter tree = new TreeFormatter();
+ tree.append("file2", FileMode.REGULAR_FILE, ObjectId.fromString("30d67d4672d5c05833b7192cc77a79eaafb5c7ad"));
+ tree.append("dir", FileMode.TREE, insertTree(dir));
+ ObjectId treeId = insertTree(tree);
FileTreeIterator iterator = new FileTreeIterator(db);
- IndexDiff diff = new IndexDiff(db, tree.getId(), iterator);
+ IndexDiff diff = new IndexDiff(db, treeId, iterator);
diff.diff();
assertEquals(2, diff.getRemoved().size());
assertTrue(diff.getRemoved().contains("file2"));
@@ -157,16 +154,16 @@ public class IndexDiffTest extends RepositoryTestCase {
writeTrashFile("dir/file3", "changed");
- Tree tree = new Tree(db);
- tree.addFile("file2").setId(ObjectId.fromString("0123456789012345678901234567890123456789"));
- tree.addFile("dir/file3").setId(ObjectId.fromString("0123456789012345678901234567890123456789"));
- assertEquals(2, tree.memberCount());
+ TreeFormatter dir = new TreeFormatter();
+ dir.append("file3", FileMode.REGULAR_FILE, ObjectId.fromString("0123456789012345678901234567890123456789"));
+
+ TreeFormatter tree = new TreeFormatter();
+ tree.append("dir", FileMode.TREE, insertTree(dir));
+ tree.append("file2", FileMode.REGULAR_FILE, ObjectId.fromString("0123456789012345678901234567890123456789"));
+ ObjectId treeId = insertTree(tree);
- Tree tree2 = (Tree) tree.findTreeMember("dir");
- tree2.setId(insertTree(tree2));
- tree.setId(insertTree(tree));
FileTreeIterator iterator = new FileTreeIterator(db);
- IndexDiff diff = new IndexDiff(db, tree.getId(), iterator);
+ IndexDiff diff = new IndexDiff(db, treeId, iterator);
diff.diff();
assertEquals(2, diff.getChanged().size());
assertTrue(diff.getChanged().contains("file2"));
@@ -314,17 +311,16 @@ public class IndexDiffTest extends RepositoryTestCase {
git.add().addFilepattern("a=c").call();
git.add().addFilepattern("a=d").call();
- Tree tree = new Tree(db);
+ TreeFormatter tree = new TreeFormatter();
// got the hash id'd from the data using echo -n a.b|git hash-object -t blob --stdin
- tree.addFile("a.b").setId(ObjectId.fromString("f6f28df96c2b40c951164286e08be7c38ec74851"));
- tree.addFile("a.c").setId(ObjectId.fromString("6bc0e647512d2a0bef4f26111e484dc87df7f5ca"));
- tree.addFile("a=c").setId(ObjectId.fromString("06022365ddbd7fb126761319633bf73517770714"));
- tree.addFile("a=d").setId(ObjectId.fromString("fa6414df3da87840700e9eeb7fc261dd77ccd5c2"));
-
- tree.setId(insertTree(tree));
+ tree.append("a.b", FileMode.REGULAR_FILE, ObjectId.fromString("f6f28df96c2b40c951164286e08be7c38ec74851"));
+ tree.append("a.c", FileMode.REGULAR_FILE, ObjectId.fromString("6bc0e647512d2a0bef4f26111e484dc87df7f5ca"));
+ tree.append("a=c", FileMode.REGULAR_FILE, ObjectId.fromString("06022365ddbd7fb126761319633bf73517770714"));
+ tree.append("a=d", FileMode.REGULAR_FILE, ObjectId.fromString("fa6414df3da87840700e9eeb7fc261dd77ccd5c2"));
+ ObjectId treeId = insertTree(tree);
FileTreeIterator iterator = new FileTreeIterator(db);
- IndexDiff diff = new IndexDiff(db, tree.getId(), iterator);
+ IndexDiff diff = new IndexDiff(db, treeId, iterator);
diff.diff();
assertEquals(0, diff.getChanged().size());
assertEquals(0, diff.getAdded().size());
@@ -356,24 +352,27 @@ public class IndexDiffTest extends RepositoryTestCase {
.addFilepattern("a/c").addFilepattern("a=c")
.addFilepattern("a=d").call();
- Tree tree = new Tree(db);
+
// got the hash id'd from the data using echo -n a.b|git hash-object -t blob --stdin
- tree.addFile("a.b").setId(ObjectId.fromString("f6f28df96c2b40c951164286e08be7c38ec74851"));
- tree.addFile("a.c").setId(ObjectId.fromString("6bc0e647512d2a0bef4f26111e484dc87df7f5ca"));
- tree.addFile("a/b.b/b").setId(ObjectId.fromString("8d840bd4e2f3a48ff417c8e927d94996849933fd"));
- tree.addFile("a/b").setId(ObjectId.fromString("db89c972fc57862eae378f45b74aca228037d415"));
- tree.addFile("a/c").setId(ObjectId.fromString("52ad142a008aeb39694bafff8e8f1be75ed7f007"));
- tree.addFile("a=c").setId(ObjectId.fromString("06022365ddbd7fb126761319633bf73517770714"));
- tree.addFile("a=d").setId(ObjectId.fromString("fa6414df3da87840700e9eeb7fc261dd77ccd5c2"));
-
- Tree tree3 = (Tree) tree.findTreeMember("a/b.b");
- tree3.setId(insertTree(tree3));
- Tree tree2 = (Tree) tree.findTreeMember("a");
- tree2.setId(insertTree(tree2));
- tree.setId(insertTree(tree));
+ TreeFormatter bb = new TreeFormatter();
+ bb.append("b", FileMode.REGULAR_FILE, ObjectId.fromString("8d840bd4e2f3a48ff417c8e927d94996849933fd"));
+
+ TreeFormatter a = new TreeFormatter();
+ a.append("b", FileMode.REGULAR_FILE, ObjectId
+ .fromString("db89c972fc57862eae378f45b74aca228037d415"));
+ a.append("b.b", FileMode.TREE, insertTree(bb));
+ a.append("c", FileMode.REGULAR_FILE, ObjectId.fromString("52ad142a008aeb39694bafff8e8f1be75ed7f007"));
+
+ TreeFormatter tree = new TreeFormatter();
+ tree.append("a.b", FileMode.REGULAR_FILE, ObjectId.fromString("f6f28df96c2b40c951164286e08be7c38ec74851"));
+ tree.append("a.c", FileMode.REGULAR_FILE, ObjectId.fromString("6bc0e647512d2a0bef4f26111e484dc87df7f5ca"));
+ tree.append("a", FileMode.TREE, insertTree(a));
+ tree.append("a=c", FileMode.REGULAR_FILE, ObjectId.fromString("06022365ddbd7fb126761319633bf73517770714"));
+ tree.append("a=d", FileMode.REGULAR_FILE, ObjectId.fromString("fa6414df3da87840700e9eeb7fc261dd77ccd5c2"));
+ ObjectId treeId = insertTree(tree);
FileTreeIterator iterator = new FileTreeIterator(db);
- IndexDiff diff = new IndexDiff(db, tree.getId(), iterator);
+ IndexDiff diff = new IndexDiff(db, treeId, iterator);
diff.diff();
assertEquals(0, diff.getChanged().size());
assertEquals(0, diff.getAdded().size());
@@ -383,9 +382,9 @@ public class IndexDiffTest extends RepositoryTestCase {
assertEquals(Collections.EMPTY_SET, diff.getUntrackedFolders());
}
- private ObjectId insertTree(Tree tree) throws IOException {
+ private ObjectId insertTree(TreeFormatter tree) throws IOException {
try (ObjectInserter oi = db.newObjectInserter()) {
- ObjectId id = oi.insert(Constants.OBJ_TREE, tree.format());
+ ObjectId id = oi.insert(tree);
oi.flush();
return id;
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectCheckerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectCheckerTest.java
index 3abe81cf85..43160fb115 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectCheckerTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectCheckerTest.java
@@ -45,8 +45,25 @@
package org.eclipse.jgit.lib;
import static java.lang.Integer.valueOf;
-import static java.lang.Long.valueOf;
+import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH;
+import static org.eclipse.jgit.lib.Constants.OBJ_BAD;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT;
+import static org.eclipse.jgit.lib.Constants.OBJ_TAG;
+import static org.eclipse.jgit.lib.Constants.OBJ_TREE;
+import static org.eclipse.jgit.lib.Constants.encode;
+import static org.eclipse.jgit.lib.Constants.encodeASCII;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.DUPLICATE_ENTRIES;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.EMPTY_NAME;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.FULL_PATHNAME;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.HAS_DOT;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.HAS_DOTDOT;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.HAS_DOTGIT;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.NULL_SHA1;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.TREE_NOT_SORTED;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.ZERO_PADDED_FILEMODE;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
import static org.junit.Assert.fail;
import java.io.UnsupportedEncodingException;
@@ -67,15 +84,10 @@ public class ObjectCheckerTest {
@Test
public void testInvalidType() {
- try {
- checker.check(Constants.OBJ_BAD, new byte[0]);
- fail("Did not throw CorruptObjectException");
- } catch (CorruptObjectException e) {
- final String m = e.getMessage();
- assertEquals(MessageFormat.format(
- JGitText.get().corruptObjectInvalidType2,
- valueOf(Constants.OBJ_BAD)), m);
- }
+ String msg = MessageFormat.format(
+ JGitText.get().corruptObjectInvalidType2,
+ valueOf(OBJ_BAD));
+ assertCorrupt(msg, OBJ_BAD, new byte[0]);
}
@Test
@@ -84,13 +96,13 @@ public class ObjectCheckerTest {
checker.checkBlob(new byte[0]);
checker.checkBlob(new byte[1]);
- checker.check(Constants.OBJ_BLOB, new byte[0]);
- checker.check(Constants.OBJ_BLOB, new byte[1]);
+ checker.check(OBJ_BLOB, new byte[0]);
+ checker.check(OBJ_BLOB, new byte[1]);
}
@Test
public void testValidCommitNoParent() throws CorruptObjectException {
- final StringBuilder b = new StringBuilder();
+ StringBuilder b = new StringBuilder();
b.append("tree ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
@@ -99,14 +111,14 @@ public class ObjectCheckerTest {
b.append("author A. U. Thor <author@localhost> 1 +0000\n");
b.append("committer A. U. Thor <author@localhost> 1 +0000\n");
- final byte[] data = Constants.encodeASCII(b.toString());
+ byte[] data = encodeASCII(b.toString());
checker.checkCommit(data);
- checker.check(Constants.OBJ_COMMIT, data);
+ checker.check(OBJ_COMMIT, data);
}
@Test
public void testValidCommitBlankAuthor() throws CorruptObjectException {
- final StringBuilder b = new StringBuilder();
+ StringBuilder b = new StringBuilder();
b.append("tree ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
@@ -115,9 +127,9 @@ public class ObjectCheckerTest {
b.append("author <> 0 +0000\n");
b.append("committer <> 0 +0000\n");
- final byte[] data = Constants.encodeASCII(b.toString());
+ byte[] data = encodeASCII(b.toString());
checker.checkCommit(data);
- checker.check(Constants.OBJ_COMMIT, data);
+ checker.check(OBJ_COMMIT, data);
}
@Test
@@ -127,15 +139,13 @@ public class ObjectCheckerTest {
b.append("author b <b@c> <b@c> 0 +0000\n");
b.append("committer <> 0 +0000\n");
- byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkCommit(data);
- fail("Did not catch corrupt object");
- } catch (CorruptObjectException e) {
- assertEquals("invalid author", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("bad date", OBJ_COMMIT, data);
checker.setAllowInvalidPersonIdent(true);
checker.checkCommit(data);
+
+ checker.setAllowInvalidPersonIdent(false);
+ assertSkipListAccepts(OBJ_COMMIT, data);
}
@Test
@@ -145,20 +155,18 @@ public class ObjectCheckerTest {
b.append("author <> 0 +0000\n");
b.append("committer b <b@c> <b@c> 0 +0000\n");
- byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkCommit(data);
- fail("Did not catch corrupt object");
- } catch (CorruptObjectException e) {
- assertEquals("invalid committer", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("bad date", OBJ_COMMIT, data);
checker.setAllowInvalidPersonIdent(true);
checker.checkCommit(data);
+
+ checker.setAllowInvalidPersonIdent(false);
+ assertSkipListAccepts(OBJ_COMMIT, data);
}
@Test
public void testValidCommit1Parent() throws CorruptObjectException {
- final StringBuilder b = new StringBuilder();
+ StringBuilder b = new StringBuilder();
b.append("tree ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
@@ -171,14 +179,14 @@ public class ObjectCheckerTest {
b.append("author A. U. Thor <author@localhost> 1 +0000\n");
b.append("committer A. U. Thor <author@localhost> 1 +0000\n");
- final byte[] data = Constants.encodeASCII(b.toString());
+ byte[] data = encodeASCII(b.toString());
checker.checkCommit(data);
- checker.check(Constants.OBJ_COMMIT, data);
+ checker.check(OBJ_COMMIT, data);
}
@Test
public void testValidCommit2Parent() throws CorruptObjectException {
- final StringBuilder b = new StringBuilder();
+ StringBuilder b = new StringBuilder();
b.append("tree ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
@@ -195,14 +203,14 @@ public class ObjectCheckerTest {
b.append("author A. U. Thor <author@localhost> 1 +0000\n");
b.append("committer A. U. Thor <author@localhost> 1 +0000\n");
- final byte[] data = Constants.encodeASCII(b.toString());
+ byte[] data = encodeASCII(b.toString());
checker.checkCommit(data);
- checker.check(Constants.OBJ_COMMIT, data);
+ checker.check(OBJ_COMMIT, data);
}
@Test
public void testValidCommit128Parent() throws CorruptObjectException {
- final StringBuilder b = new StringBuilder();
+ StringBuilder b = new StringBuilder();
b.append("tree ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
@@ -217,15 +225,15 @@ public class ObjectCheckerTest {
b.append("author A. U. Thor <author@localhost> 1 +0000\n");
b.append("committer A. U. Thor <author@localhost> 1 +0000\n");
- final byte[] data = Constants.encodeASCII(b.toString());
+ byte[] data = encodeASCII(b.toString());
checker.checkCommit(data);
- checker.check(Constants.OBJ_COMMIT, data);
+ checker.check(OBJ_COMMIT, data);
}
@Test
public void testValidCommitNormalTime() throws CorruptObjectException {
- final StringBuilder b = new StringBuilder();
- final String when = "1222757360 -0730";
+ StringBuilder b = new StringBuilder();
+ String when = "1222757360 -0730";
b.append("tree ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
@@ -234,843 +242,539 @@ public class ObjectCheckerTest {
b.append("author A. U. Thor <author@localhost> " + when + "\n");
b.append("committer A. U. Thor <author@localhost> " + when + "\n");
- final byte[] data = Constants.encodeASCII(b.toString());
+ byte[] data = encodeASCII(b.toString());
checker.checkCommit(data);
- checker.check(Constants.OBJ_COMMIT, data);
+ checker.check(OBJ_COMMIT, data);
}
@Test
public void testInvalidCommitNoTree1() {
- final StringBuilder b = new StringBuilder();
-
+ StringBuilder b = new StringBuilder();
b.append("parent ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append('\n');
-
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkCommit(data);
- fail("Did not catch corrupt object");
- } catch (CorruptObjectException e) {
- assertEquals("no tree header", e.getMessage());
- }
+ assertCorrupt("no tree header", OBJ_COMMIT, b);
}
@Test
public void testInvalidCommitNoTree2() {
- final StringBuilder b = new StringBuilder();
-
+ StringBuilder b = new StringBuilder();
b.append("trie ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append('\n');
-
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkCommit(data);
- fail("Did not catch corrupt object");
- } catch (CorruptObjectException e) {
- assertEquals("no tree header", e.getMessage());
- }
+ assertCorrupt("no tree header", OBJ_COMMIT, b);
}
@Test
public void testInvalidCommitNoTree3() {
- final StringBuilder b = new StringBuilder();
-
+ StringBuilder b = new StringBuilder();
b.append("tree");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append('\n');
-
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkCommit(data);
- fail("Did not catch corrupt object");
- } catch (CorruptObjectException e) {
- assertEquals("no tree header", e.getMessage());
- }
+ assertCorrupt("no tree header", OBJ_COMMIT, b);
}
@Test
public void testInvalidCommitNoTree4() {
- final StringBuilder b = new StringBuilder();
-
+ StringBuilder b = new StringBuilder();
b.append("tree\t");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append('\n');
-
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkCommit(data);
- fail("Did not catch corrupt object");
- } catch (CorruptObjectException e) {
- assertEquals("no tree header", e.getMessage());
- }
+ assertCorrupt("no tree header", OBJ_COMMIT, b);
}
@Test
public void testInvalidCommitInvalidTree1() {
- final StringBuilder b = new StringBuilder();
-
+ StringBuilder b = new StringBuilder();
b.append("tree ");
b.append("zzzzfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append('\n');
-
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkCommit(data);
- fail("Did not catch corrupt object");
- } catch (CorruptObjectException e) {
- assertEquals("invalid tree", e.getMessage());
- }
+ assertCorrupt("invalid tree", OBJ_COMMIT, b);
}
@Test
public void testInvalidCommitInvalidTree2() {
- final StringBuilder b = new StringBuilder();
-
+ StringBuilder b = new StringBuilder();
b.append("tree ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append("z\n");
-
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkCommit(data);
- fail("Did not catch corrupt object");
- } catch (CorruptObjectException e) {
- assertEquals("invalid tree", e.getMessage());
- }
+ assertCorrupt("invalid tree", OBJ_COMMIT, b);
}
@Test
public void testInvalidCommitInvalidTree3() {
- final StringBuilder b = new StringBuilder();
-
+ StringBuilder b = new StringBuilder();
b.append("tree ");
b.append("be9b");
b.append("\n");
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkCommit(data);
- fail("Did not catch corrupt object");
- } catch (CorruptObjectException e) {
- assertEquals("invalid tree", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("invalid tree", OBJ_COMMIT, data);
}
@Test
public void testInvalidCommitInvalidTree4() {
- final StringBuilder b = new StringBuilder();
-
+ StringBuilder b = new StringBuilder();
b.append("tree ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append('\n');
-
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkCommit(data);
- fail("Did not catch corrupt object");
- } catch (CorruptObjectException e) {
- assertEquals("invalid tree", e.getMessage());
- }
+ assertCorrupt("invalid tree", OBJ_COMMIT, b);
}
@Test
public void testInvalidCommitInvalidParent1() {
- final StringBuilder b = new StringBuilder();
-
+ StringBuilder b = new StringBuilder();
b.append("tree ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append('\n');
-
b.append("parent ");
b.append("\n");
-
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkCommit(data);
- fail("Did not catch corrupt object");
- } catch (CorruptObjectException e) {
- assertEquals("invalid parent", e.getMessage());
- }
+ assertCorrupt("invalid parent", OBJ_COMMIT, b);
}
@Test
public void testInvalidCommitInvalidParent2() {
- final StringBuilder b = new StringBuilder();
-
+ StringBuilder b = new StringBuilder();
b.append("tree ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append('\n');
-
b.append("parent ");
b.append("zzzzfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append("\n");
-
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkCommit(data);
- fail("Did not catch corrupt object");
- } catch (CorruptObjectException e) {
- assertEquals("invalid parent", e.getMessage());
- }
+ assertCorrupt("invalid parent", OBJ_COMMIT, b);
}
@Test
public void testInvalidCommitInvalidParent3() {
- final StringBuilder b = new StringBuilder();
-
+ StringBuilder b = new StringBuilder();
b.append("tree ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append('\n');
-
b.append("parent ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append("\n");
-
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkCommit(data);
- fail("Did not catch corrupt object");
- } catch (CorruptObjectException e) {
- assertEquals("invalid parent", e.getMessage());
- }
+ assertCorrupt("invalid parent", OBJ_COMMIT, b);
}
@Test
public void testInvalidCommitInvalidParent4() {
- final StringBuilder b = new StringBuilder();
-
+ StringBuilder b = new StringBuilder();
b.append("tree ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append('\n');
-
b.append("parent ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append("z\n");
-
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkCommit(data);
- fail("Did not catch corrupt object");
- } catch (CorruptObjectException e) {
- assertEquals("invalid parent", e.getMessage());
- }
+ assertCorrupt("invalid parent", OBJ_COMMIT, b);
}
@Test
public void testInvalidCommitInvalidParent5() {
- final StringBuilder b = new StringBuilder();
-
+ StringBuilder b = new StringBuilder();
b.append("tree ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append('\n');
-
b.append("parent\t");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append("\n");
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkCommit(data);
- fail("Did not catch corrupt object");
- } catch (CorruptObjectException e) {
- // Yes, really, we complain about author not being
- // found as the invalid parent line wasn't consumed.
- assertEquals("no author", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ // Yes, really, we complain about author not being
+ // found as the invalid parent line wasn't consumed.
+ assertCorrupt("no author", OBJ_COMMIT, data);
}
@Test
- public void testInvalidCommitNoAuthor() {
- final StringBuilder b = new StringBuilder();
-
+ public void testInvalidCommitNoAuthor() throws CorruptObjectException {
+ StringBuilder b = new StringBuilder();
b.append("tree ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append('\n');
-
b.append("committer A. U. Thor <author@localhost> 1 +0000\n");
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkCommit(data);
- fail("Did not catch corrupt object");
- } catch (CorruptObjectException e) {
- // Yes, really, we complain about author not being
- // found as the invalid parent line wasn't consumed.
- assertEquals("no author", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("no author", OBJ_COMMIT, data);
+ assertSkipListAccepts(OBJ_COMMIT, data);
}
@Test
- public void testInvalidCommitNoCommitter1() {
- final StringBuilder b = new StringBuilder();
-
+ public void testInvalidCommitNoCommitter1() throws CorruptObjectException {
+ StringBuilder b = new StringBuilder();
b.append("tree ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append('\n');
-
b.append("author A. U. Thor <author@localhost> 1 +0000\n");
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkCommit(data);
- fail("Did not catch corrupt object");
- } catch (CorruptObjectException e) {
- // Yes, really, we complain about author not being
- // found as the invalid parent line wasn't consumed.
- assertEquals("no committer", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("no committer", OBJ_COMMIT, data);
+ assertSkipListAccepts(OBJ_COMMIT, data);
}
@Test
- public void testInvalidCommitNoCommitter2() {
- final StringBuilder b = new StringBuilder();
-
+ public void testInvalidCommitNoCommitter2() throws CorruptObjectException {
+ StringBuilder b = new StringBuilder();
b.append("tree ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append('\n');
-
b.append("author A. U. Thor <author@localhost> 1 +0000\n");
b.append("\n");
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkCommit(data);
- fail("Did not catch corrupt object");
- } catch (CorruptObjectException e) {
- // Yes, really, we complain about author not being
- // found as the invalid parent line wasn't consumed.
- assertEquals("no committer", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("no committer", OBJ_COMMIT, data);
+ assertSkipListAccepts(OBJ_COMMIT, data);
}
@Test
- public void testInvalidCommitInvalidAuthor1() {
- final StringBuilder b = new StringBuilder();
-
+ public void testInvalidCommitInvalidAuthor1()
+ throws CorruptObjectException {
+ StringBuilder b = new StringBuilder();
b.append("tree ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append('\n');
-
b.append("author A. U. Thor <foo 1 +0000\n");
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkCommit(data);
- fail("Did not catch corrupt object");
- } catch (CorruptObjectException e) {
- // Yes, really, we complain about author not being
- // found as the invalid parent line wasn't consumed.
- assertEquals("invalid author", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("bad email", OBJ_COMMIT, data);
+ assertSkipListAccepts(OBJ_COMMIT, data);
}
@Test
- public void testInvalidCommitInvalidAuthor2() {
- final StringBuilder b = new StringBuilder();
-
+ public void testInvalidCommitInvalidAuthor2()
+ throws CorruptObjectException {
+ StringBuilder b = new StringBuilder();
b.append("tree ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append('\n');
-
b.append("author A. U. Thor foo> 1 +0000\n");
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkCommit(data);
- fail("Did not catch corrupt object");
- } catch (CorruptObjectException e) {
- // Yes, really, we complain about author not being
- // found as the invalid parent line wasn't consumed.
- assertEquals("invalid author", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("missing email", OBJ_COMMIT, data);
+ assertSkipListAccepts(OBJ_COMMIT, data);
}
@Test
- public void testInvalidCommitInvalidAuthor3() {
- final StringBuilder b = new StringBuilder();
-
+ public void testInvalidCommitInvalidAuthor3()
+ throws CorruptObjectException {
+ StringBuilder b = new StringBuilder();
b.append("tree ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append('\n');
-
b.append("author 1 +0000\n");
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkCommit(data);
- fail("Did not catch corrupt object");
- } catch (CorruptObjectException e) {
- // Yes, really, we complain about author not being
- // found as the invalid parent line wasn't consumed.
- assertEquals("invalid author", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("missing email", OBJ_COMMIT, data);
+ assertSkipListAccepts(OBJ_COMMIT, data);
}
@Test
- public void testInvalidCommitInvalidAuthor4() {
- final StringBuilder b = new StringBuilder();
-
+ public void testInvalidCommitInvalidAuthor4()
+ throws CorruptObjectException {
+ StringBuilder b = new StringBuilder();
b.append("tree ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append('\n');
-
b.append("author a <b> +0000\n");
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkCommit(data);
- fail("Did not catch corrupt object");
- } catch (CorruptObjectException e) {
- // Yes, really, we complain about author not being
- // found as the invalid parent line wasn't consumed.
- assertEquals("invalid author", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("bad date", OBJ_COMMIT, data);
+ assertSkipListAccepts(OBJ_COMMIT, data);
}
@Test
- public void testInvalidCommitInvalidAuthor5() {
- final StringBuilder b = new StringBuilder();
-
+ public void testInvalidCommitInvalidAuthor5()
+ throws CorruptObjectException {
+ StringBuilder b = new StringBuilder();
b.append("tree ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append('\n');
-
b.append("author a <b>\n");
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkCommit(data);
- fail("Did not catch corrupt object");
- } catch (CorruptObjectException e) {
- // Yes, really, we complain about author not being
- // found as the invalid parent line wasn't consumed.
- assertEquals("invalid author", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("bad date", OBJ_COMMIT, data);
+ assertSkipListAccepts(OBJ_COMMIT, data);
}
@Test
- public void testInvalidCommitInvalidAuthor6() {
- final StringBuilder b = new StringBuilder();
-
+ public void testInvalidCommitInvalidAuthor6()
+ throws CorruptObjectException {
+ StringBuilder b = new StringBuilder();
b.append("tree ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append('\n');
-
b.append("author a <b> z");
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkCommit(data);
- fail("Did not catch corrupt object");
- } catch (CorruptObjectException e) {
- // Yes, really, we complain about author not being
- // found as the invalid parent line wasn't consumed.
- assertEquals("invalid author", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("bad date", OBJ_COMMIT, data);
+ assertSkipListAccepts(OBJ_COMMIT, data);
}
@Test
- public void testInvalidCommitInvalidAuthor7() {
- final StringBuilder b = new StringBuilder();
-
+ public void testInvalidCommitInvalidAuthor7()
+ throws CorruptObjectException {
+ StringBuilder b = new StringBuilder();
b.append("tree ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append('\n');
-
b.append("author a <b> 1 z");
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkCommit(data);
- fail("Did not catch corrupt object");
- } catch (CorruptObjectException e) {
- // Yes, really, we complain about author not being
- // found as the invalid parent line wasn't consumed.
- assertEquals("invalid author", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("bad time zone", OBJ_COMMIT, data);
+ assertSkipListAccepts(OBJ_COMMIT, data);
}
@Test
- public void testInvalidCommitInvalidCommitter() {
- final StringBuilder b = new StringBuilder();
-
+ public void testInvalidCommitInvalidCommitter()
+ throws CorruptObjectException {
+ StringBuilder b = new StringBuilder();
b.append("tree ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append('\n');
-
b.append("author a <b> 1 +0000\n");
b.append("committer a <");
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkCommit(data);
- fail("Did not catch corrupt object");
- } catch (CorruptObjectException e) {
- // Yes, really, we complain about author not being
- // found as the invalid parent line wasn't consumed.
- assertEquals("invalid committer", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("bad email", OBJ_COMMIT, data);
+ assertSkipListAccepts(OBJ_COMMIT, data);
}
@Test
public void testValidTag() throws CorruptObjectException {
- final StringBuilder b = new StringBuilder();
-
+ StringBuilder b = new StringBuilder();
b.append("object ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append('\n');
-
b.append("type commit\n");
b.append("tag test-tag\n");
b.append("tagger A. U. Thor <author@localhost> 1 +0000\n");
- final byte[] data = Constants.encodeASCII(b.toString());
+ byte[] data = encodeASCII(b.toString());
checker.checkTag(data);
- checker.check(Constants.OBJ_TAG, data);
+ checker.check(OBJ_TAG, data);
}
@Test
public void testInvalidTagNoObject1() {
- final StringBuilder b = new StringBuilder();
-
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTag(data);
- fail("incorrectly accepted invalid tag");
- } catch (CorruptObjectException e) {
- assertEquals("no object header", e.getMessage());
- }
+ assertCorrupt("no object header", OBJ_TAG, new byte[0]);
}
@Test
public void testInvalidTagNoObject2() {
- final StringBuilder b = new StringBuilder();
-
+ StringBuilder b = new StringBuilder();
b.append("object\t");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append('\n');
-
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTag(data);
- fail("incorrectly accepted invalid tag");
- } catch (CorruptObjectException e) {
- assertEquals("no object header", e.getMessage());
- }
+ assertCorrupt("no object header", OBJ_TAG, b);
}
@Test
public void testInvalidTagNoObject3() {
- final StringBuilder b = new StringBuilder();
-
+ StringBuilder b = new StringBuilder();
b.append("obejct ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append('\n');
-
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTag(data);
- fail("incorrectly accepted invalid tag");
- } catch (CorruptObjectException e) {
- assertEquals("no object header", e.getMessage());
- }
+ assertCorrupt("no object header", OBJ_TAG, b);
}
@Test
public void testInvalidTagNoObject4() {
- final StringBuilder b = new StringBuilder();
-
+ StringBuilder b = new StringBuilder();
b.append("object ");
b.append("zz9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append('\n');
-
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTag(data);
- fail("incorrectly accepted invalid tag");
- } catch (CorruptObjectException e) {
- assertEquals("invalid object", e.getMessage());
- }
+ assertCorrupt("invalid object", OBJ_TAG, b);
}
@Test
public void testInvalidTagNoObject5() {
- final StringBuilder b = new StringBuilder();
-
+ StringBuilder b = new StringBuilder();
b.append("object ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append(" \n");
-
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTag(data);
- fail("incorrectly accepted invalid tag");
- } catch (CorruptObjectException e) {
- assertEquals("invalid object", e.getMessage());
- }
+ assertCorrupt("invalid object", OBJ_TAG, b);
}
@Test
public void testInvalidTagNoObject6() {
- final StringBuilder b = new StringBuilder();
-
+ StringBuilder b = new StringBuilder();
b.append("object ");
b.append("be9");
-
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTag(data);
- fail("incorrectly accepted invalid tag");
- } catch (CorruptObjectException e) {
- assertEquals("invalid object", e.getMessage());
- }
+ assertCorrupt("invalid object", OBJ_TAG, b);
}
@Test
public void testInvalidTagNoType1() {
- final StringBuilder b = new StringBuilder();
-
+ StringBuilder b = new StringBuilder();
b.append("object ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append('\n');
-
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTag(data);
- fail("incorrectly accepted invalid tag");
- } catch (CorruptObjectException e) {
- assertEquals("no type header", e.getMessage());
- }
+ assertCorrupt("no type header", OBJ_TAG, b);
}
@Test
public void testInvalidTagNoType2() {
- final StringBuilder b = new StringBuilder();
-
+ StringBuilder b = new StringBuilder();
b.append("object ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append('\n');
-
b.append("type\tcommit\n");
-
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTag(data);
- fail("incorrectly accepted invalid tag");
- } catch (CorruptObjectException e) {
- assertEquals("no type header", e.getMessage());
- }
+ assertCorrupt("no type header", OBJ_TAG, b);
}
@Test
public void testInvalidTagNoType3() {
- final StringBuilder b = new StringBuilder();
-
+ StringBuilder b = new StringBuilder();
b.append("object ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append('\n');
-
b.append("tpye commit\n");
-
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTag(data);
- fail("incorrectly accepted invalid tag");
- } catch (CorruptObjectException e) {
- assertEquals("no type header", e.getMessage());
- }
+ assertCorrupt("no type header", OBJ_TAG, b);
}
@Test
public void testInvalidTagNoType4() {
- final StringBuilder b = new StringBuilder();
-
+ StringBuilder b = new StringBuilder();
b.append("object ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append('\n');
-
b.append("type commit");
-
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTag(data);
- fail("incorrectly accepted invalid tag");
- } catch (CorruptObjectException e) {
- assertEquals("no tag header", e.getMessage());
- }
+ assertCorrupt("no tag header", OBJ_TAG, b);
}
@Test
public void testInvalidTagNoTagHeader1() {
- final StringBuilder b = new StringBuilder();
-
+ StringBuilder b = new StringBuilder();
b.append("object ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append('\n');
-
b.append("type commit\n");
-
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTag(data);
- fail("incorrectly accepted invalid tag");
- } catch (CorruptObjectException e) {
- assertEquals("no tag header", e.getMessage());
- }
+ assertCorrupt("no tag header", OBJ_TAG, b);
}
@Test
public void testInvalidTagNoTagHeader2() {
- final StringBuilder b = new StringBuilder();
-
+ StringBuilder b = new StringBuilder();
b.append("object ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append('\n');
-
b.append("type commit\n");
b.append("tag\tfoo\n");
-
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTag(data);
- fail("incorrectly accepted invalid tag");
- } catch (CorruptObjectException e) {
- assertEquals("no tag header", e.getMessage());
- }
+ assertCorrupt("no tag header", OBJ_TAG, b);
}
@Test
public void testInvalidTagNoTagHeader3() {
- final StringBuilder b = new StringBuilder();
-
+ StringBuilder b = new StringBuilder();
b.append("object ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append('\n');
-
b.append("type commit\n");
b.append("tga foo\n");
-
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTag(data);
- fail("incorrectly accepted invalid tag");
- } catch (CorruptObjectException e) {
- assertEquals("no tag header", e.getMessage());
- }
+ assertCorrupt("no tag header", OBJ_TAG, b);
}
@Test
public void testValidTagHasNoTaggerHeader() throws CorruptObjectException {
- final StringBuilder b = new StringBuilder();
-
+ StringBuilder b = new StringBuilder();
b.append("object ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append('\n');
-
b.append("type commit\n");
b.append("tag foo\n");
-
- checker.checkTag(Constants.encodeASCII(b.toString()));
+ checker.checkTag(encodeASCII(b.toString()));
}
@Test
public void testInvalidTagInvalidTaggerHeader1()
throws CorruptObjectException {
- final StringBuilder b = new StringBuilder();
-
+ StringBuilder b = new StringBuilder();
b.append("object ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append('\n');
-
b.append("type commit\n");
b.append("tag foo\n");
b.append("tagger \n");
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTag(data);
- fail("incorrectly accepted invalid tag");
- } catch (CorruptObjectException e) {
- assertEquals("invalid tagger", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("missing email", OBJ_TAG, data);
checker.setAllowInvalidPersonIdent(true);
checker.checkTag(data);
+
+ checker.setAllowInvalidPersonIdent(false);
+ assertSkipListAccepts(OBJ_TAG, data);
}
@Test
- public void testInvalidTagInvalidTaggerHeader3() {
- final StringBuilder b = new StringBuilder();
-
+ public void testInvalidTagInvalidTaggerHeader3()
+ throws CorruptObjectException {
+ StringBuilder b = new StringBuilder();
b.append("object ");
b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
b.append('\n');
-
b.append("type commit\n");
b.append("tag foo\n");
b.append("tagger a < 1 +000\n");
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTag(data);
- fail("incorrectly accepted invalid tag");
- } catch (CorruptObjectException e) {
- assertEquals("invalid tagger", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("bad email", OBJ_TAG, data);
+ assertSkipListAccepts(OBJ_TAG, data);
}
@Test
public void testValidEmptyTree() throws CorruptObjectException {
checker.checkTree(new byte[0]);
- checker.check(Constants.OBJ_TREE, new byte[0]);
+ checker.check(OBJ_TREE, new byte[0]);
}
@Test
public void testValidTree1() throws CorruptObjectException {
- final StringBuilder b = new StringBuilder();
+ StringBuilder b = new StringBuilder();
entry(b, "100644 regular-file");
- final byte[] data = Constants.encodeASCII(b.toString());
- checker.checkTree(data);
+ checker.checkTree(encodeASCII(b.toString()));
}
@Test
public void testValidTree2() throws CorruptObjectException {
- final StringBuilder b = new StringBuilder();
+ StringBuilder b = new StringBuilder();
entry(b, "100755 executable");
- final byte[] data = Constants.encodeASCII(b.toString());
- checker.checkTree(data);
+ checker.checkTree(encodeASCII(b.toString()));
}
@Test
public void testValidTree3() throws CorruptObjectException {
- final StringBuilder b = new StringBuilder();
+ StringBuilder b = new StringBuilder();
entry(b, "40000 tree");
- final byte[] data = Constants.encodeASCII(b.toString());
- checker.checkTree(data);
+ checker.checkTree(encodeASCII(b.toString()));
}
@Test
public void testValidTree4() throws CorruptObjectException {
- final StringBuilder b = new StringBuilder();
+ StringBuilder b = new StringBuilder();
entry(b, "120000 symlink");
- final byte[] data = Constants.encodeASCII(b.toString());
- checker.checkTree(data);
+ checker.checkTree(encodeASCII(b.toString()));
}
@Test
public void testValidTree5() throws CorruptObjectException {
- final StringBuilder b = new StringBuilder();
+ StringBuilder b = new StringBuilder();
entry(b, "160000 git link");
- final byte[] data = Constants.encodeASCII(b.toString());
- checker.checkTree(data);
+ checker.checkTree(encodeASCII(b.toString()));
}
@Test
public void testValidTree6() throws CorruptObjectException {
- final StringBuilder b = new StringBuilder();
+ StringBuilder b = new StringBuilder();
entry(b, "100644 .a");
- final byte[] data = Constants.encodeASCII(b.toString());
+ checker.checkTree(encodeASCII(b.toString()));
+ }
+
+ @Test
+ public void testNullSha1InTreeEntry() throws CorruptObjectException {
+ byte[] data = concat(
+ encodeASCII("100644 A"), new byte[] { '\0' },
+ new byte[OBJECT_ID_LENGTH]);
+ assertCorrupt("entry points to null SHA-1", OBJ_TREE, data);
+ assertSkipListAccepts(OBJ_TREE, data);
+ checker.setIgnore(NULL_SHA1, true);
checker.checkTree(data);
}
@@ -1084,357 +788,326 @@ public class ObjectCheckerTest {
@Test
public void testValidTreeSorting1() throws CorruptObjectException {
- final StringBuilder b = new StringBuilder();
+ StringBuilder b = new StringBuilder();
entry(b, "100644 fooaaa");
entry(b, "100755 foobar");
- final byte[] data = Constants.encodeASCII(b.toString());
- checker.checkTree(data);
+ checker.checkTree(encodeASCII(b.toString()));
}
@Test
public void testValidTreeSorting2() throws CorruptObjectException {
- final StringBuilder b = new StringBuilder();
+ StringBuilder b = new StringBuilder();
entry(b, "100755 fooaaa");
entry(b, "100644 foobar");
- final byte[] data = Constants.encodeASCII(b.toString());
- checker.checkTree(data);
+ checker.checkTree(encodeASCII(b.toString()));
}
@Test
public void testValidTreeSorting3() throws CorruptObjectException {
- final StringBuilder b = new StringBuilder();
+ StringBuilder b = new StringBuilder();
entry(b, "40000 a");
entry(b, "100644 b");
- final byte[] data = Constants.encodeASCII(b.toString());
- checker.checkTree(data);
+ checker.checkTree(encodeASCII(b.toString()));
}
@Test
public void testValidTreeSorting4() throws CorruptObjectException {
- final StringBuilder b = new StringBuilder();
+ StringBuilder b = new StringBuilder();
entry(b, "100644 a");
entry(b, "40000 b");
- final byte[] data = Constants.encodeASCII(b.toString());
- checker.checkTree(data);
+ checker.checkTree(encodeASCII(b.toString()));
}
@Test
public void testValidTreeSorting5() throws CorruptObjectException {
- final StringBuilder b = new StringBuilder();
+ StringBuilder b = new StringBuilder();
entry(b, "100644 a.c");
entry(b, "40000 a");
entry(b, "100644 a0c");
- final byte[] data = Constants.encodeASCII(b.toString());
- checker.checkTree(data);
+ checker.checkTree(encodeASCII(b.toString()));
}
@Test
public void testValidTreeSorting6() throws CorruptObjectException {
- final StringBuilder b = new StringBuilder();
+ StringBuilder b = new StringBuilder();
entry(b, "40000 a");
entry(b, "100644 apple");
- final byte[] data = Constants.encodeASCII(b.toString());
- checker.checkTree(data);
+ checker.checkTree(encodeASCII(b.toString()));
}
@Test
public void testValidTreeSorting7() throws CorruptObjectException {
- final StringBuilder b = new StringBuilder();
+ StringBuilder b = new StringBuilder();
entry(b, "40000 an orang");
entry(b, "40000 an orange");
- final byte[] data = Constants.encodeASCII(b.toString());
- checker.checkTree(data);
+ checker.checkTree(encodeASCII(b.toString()));
}
@Test
public void testValidTreeSorting8() throws CorruptObjectException {
- final StringBuilder b = new StringBuilder();
+ StringBuilder b = new StringBuilder();
entry(b, "100644 a");
entry(b, "100644 a0c");
entry(b, "100644 b");
- final byte[] data = Constants.encodeASCII(b.toString());
- checker.checkTree(data);
+ checker.checkTree(encodeASCII(b.toString()));
}
@Test
public void testAcceptTreeModeWithZero() throws CorruptObjectException {
StringBuilder b = new StringBuilder();
entry(b, "040000 a");
+ byte[] data = encodeASCII(b.toString());
checker.setAllowLeadingZeroFileMode(true);
- checker.checkTree(Constants.encodeASCII(b.toString()));
+ checker.checkTree(data);
+
+ checker.setAllowLeadingZeroFileMode(false);
+ assertSkipListAccepts(OBJ_TREE, data);
+
+ checker.setIgnore(ZERO_PADDED_FILEMODE, true);
+ checker.checkTree(data);
}
@Test
public void testInvalidTreeModeStartsWithZero1() {
- final StringBuilder b = new StringBuilder();
+ StringBuilder b = new StringBuilder();
entry(b, "0 a");
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTree(data);
- fail("incorrectly accepted an invalid tree");
- } catch (CorruptObjectException e) {
- assertEquals("mode starts with '0'", e.getMessage());
- }
+ assertCorrupt("mode starts with '0'", OBJ_TREE, b);
}
@Test
public void testInvalidTreeModeStartsWithZero2() {
- final StringBuilder b = new StringBuilder();
+ StringBuilder b = new StringBuilder();
entry(b, "0100644 a");
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTree(data);
- fail("incorrectly accepted an invalid tree");
- } catch (CorruptObjectException e) {
- assertEquals("mode starts with '0'", e.getMessage());
- }
+ assertCorrupt("mode starts with '0'", OBJ_TREE, b);
}
@Test
public void testInvalidTreeModeStartsWithZero3() {
- final StringBuilder b = new StringBuilder();
+ StringBuilder b = new StringBuilder();
entry(b, "040000 a");
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTree(data);
- fail("incorrectly accepted an invalid tree");
- } catch (CorruptObjectException e) {
- assertEquals("mode starts with '0'", e.getMessage());
- }
+ assertCorrupt("mode starts with '0'", OBJ_TREE, b);
}
@Test
public void testInvalidTreeModeNotOctal1() {
- final StringBuilder b = new StringBuilder();
+ StringBuilder b = new StringBuilder();
entry(b, "8 a");
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTree(data);
- fail("incorrectly accepted an invalid tree");
- } catch (CorruptObjectException e) {
- assertEquals("invalid mode character", e.getMessage());
- }
+ assertCorrupt("invalid mode character", OBJ_TREE, b);
}
@Test
public void testInvalidTreeModeNotOctal2() {
- final StringBuilder b = new StringBuilder();
+ StringBuilder b = new StringBuilder();
entry(b, "Z a");
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTree(data);
- fail("incorrectly accepted an invalid tree");
- } catch (CorruptObjectException e) {
- assertEquals("invalid mode character", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("invalid mode character", OBJ_TREE, data);
+ assertSkipListRejects("invalid mode character", OBJ_TREE, data);
}
@Test
public void testInvalidTreeModeNotSupportedMode1() {
- final StringBuilder b = new StringBuilder();
+ StringBuilder b = new StringBuilder();
entry(b, "1 a");
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTree(data);
- fail("incorrectly accepted an invalid tree");
- } catch (CorruptObjectException e) {
- assertEquals("invalid mode 1", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("invalid mode 1", OBJ_TREE, data);
+ assertSkipListRejects("invalid mode 1", OBJ_TREE, data);
}
@Test
public void testInvalidTreeModeNotSupportedMode2() {
- final StringBuilder b = new StringBuilder();
+ StringBuilder b = new StringBuilder();
entry(b, "170000 a");
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTree(data);
- fail("incorrectly accepted an invalid tree");
- } catch (CorruptObjectException e) {
- assertEquals("invalid mode " + 0170000, e.getMessage());
- }
+ assertCorrupt("invalid mode " + 0170000, OBJ_TREE, b);
}
@Test
public void testInvalidTreeModeMissingName() {
- final StringBuilder b = new StringBuilder();
+ StringBuilder b = new StringBuilder();
b.append("100644");
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTree(data);
- fail("incorrectly accepted an invalid tree");
- } catch (CorruptObjectException e) {
- assertEquals("truncated in mode", e.getMessage());
- }
+ assertCorrupt("truncated in mode", OBJ_TREE, b);
}
@Test
- public void testInvalidTreeNameContainsSlash() {
- final StringBuilder b = new StringBuilder();
+ public void testInvalidTreeNameContainsSlash()
+ throws CorruptObjectException {
+ StringBuilder b = new StringBuilder();
entry(b, "100644 a/b");
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTree(data);
- fail("incorrectly accepted an invalid tree");
- } catch (CorruptObjectException e) {
- assertEquals("name contains '/'", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("name contains '/'", OBJ_TREE, data);
+ assertSkipListAccepts(OBJ_TREE, data);
+ checker.setIgnore(FULL_PATHNAME, true);
+ checker.checkTree(data);
}
@Test
- public void testInvalidTreeNameIsEmpty() {
- final StringBuilder b = new StringBuilder();
+ public void testInvalidTreeNameIsEmpty() throws CorruptObjectException {
+ StringBuilder b = new StringBuilder();
entry(b, "100644 ");
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTree(data);
- fail("incorrectly accepted an invalid tree");
- } catch (CorruptObjectException e) {
- assertEquals("zero length name", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("zero length name", OBJ_TREE, data);
+ assertSkipListAccepts(OBJ_TREE, data);
+ checker.setIgnore(EMPTY_NAME, true);
+ checker.checkTree(data);
}
@Test
- public void testInvalidTreeNameIsDot() {
- final StringBuilder b = new StringBuilder();
+ public void testInvalidTreeNameIsDot() throws CorruptObjectException {
+ StringBuilder b = new StringBuilder();
entry(b, "100644 .");
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTree(data);
- fail("incorrectly accepted an invalid tree");
- } catch (CorruptObjectException e) {
- assertEquals("invalid name '.'", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("invalid name '.'", OBJ_TREE, data);
+ assertSkipListAccepts(OBJ_TREE, data);
+ checker.setIgnore(HAS_DOT, true);
+ checker.checkTree(data);
}
@Test
- public void testInvalidTreeNameIsDotDot() {
- final StringBuilder b = new StringBuilder();
+ public void testInvalidTreeNameIsDotDot() throws CorruptObjectException {
+ StringBuilder b = new StringBuilder();
entry(b, "100644 ..");
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTree(data);
- fail("incorrectly accepted an invalid tree");
- } catch (CorruptObjectException e) {
- assertEquals("invalid name '..'", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("invalid name '..'", OBJ_TREE, data);
+ assertSkipListAccepts(OBJ_TREE, data);
+ checker.setIgnore(HAS_DOTDOT, true);
+ checker.checkTree(data);
}
@Test
- public void testInvalidTreeNameIsGit() {
+ public void testInvalidTreeNameIsGit() throws CorruptObjectException {
StringBuilder b = new StringBuilder();
entry(b, "100644 .git");
- byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTree(data);
- fail("incorrectly accepted an invalid tree");
- } catch (CorruptObjectException e) {
- assertEquals("invalid name '.git'", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("invalid name '.git'", OBJ_TREE, data);
+ assertSkipListAccepts(OBJ_TREE, data);
+ checker.setIgnore(HAS_DOTGIT, true);
+ checker.checkTree(data);
}
@Test
- public void testInvalidTreeNameIsMixedCaseGit() {
+ public void testInvalidTreeNameIsMixedCaseGit()
+ throws CorruptObjectException {
StringBuilder b = new StringBuilder();
entry(b, "100644 .GiT");
- byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTree(data);
- fail("incorrectly accepted an invalid tree");
- } catch (CorruptObjectException e) {
- assertEquals("invalid name '.GiT'", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("invalid name '.GiT'", OBJ_TREE, data);
+ assertSkipListAccepts(OBJ_TREE, data);
+ checker.setIgnore(HAS_DOTGIT, true);
+ checker.checkTree(data);
}
@Test
- public void testInvalidTreeNameIsMacHFSGit() {
+ public void testInvalidTreeNameIsMacHFSGit() throws CorruptObjectException {
StringBuilder b = new StringBuilder();
entry(b, "100644 .gi\u200Ct");
- byte[] data = Constants.encode(b.toString());
- try {
- checker.setSafeForMacOS(true);
- checker.checkTree(data);
- fail("incorrectly accepted an invalid tree");
- } catch (CorruptObjectException e) {
- assertEquals(
- "invalid name '.gi\u200Ct' contains ignorable Unicode characters",
- e.getMessage());
- }
+ byte[] data = encode(b.toString());
+
+ // Fine on POSIX.
+ checker.checkTree(data);
+
+ // Rejected on Mac OS.
+ checker.setSafeForMacOS(true);
+ assertCorrupt(
+ "invalid name '.gi\u200Ct' contains ignorable Unicode characters",
+ OBJ_TREE, data);
+ assertSkipListAccepts(OBJ_TREE, data);
+ checker.setIgnore(HAS_DOTGIT, true);
+ checker.checkTree(data);
}
@Test
- public void testInvalidTreeNameIsMacHFSGit2() {
+ public void testInvalidTreeNameIsMacHFSGit2()
+ throws CorruptObjectException {
StringBuilder b = new StringBuilder();
entry(b, "100644 \u206B.git");
- byte[] data = Constants.encode(b.toString());
- try {
- checker.setSafeForMacOS(true);
- checker.checkTree(data);
- fail("incorrectly accepted an invalid tree");
- } catch (CorruptObjectException e) {
- assertEquals(
- "invalid name '\u206B.git' contains ignorable Unicode characters",
- e.getMessage());
- }
+ byte[] data = encode(b.toString());
+
+ // Fine on POSIX.
+ checker.checkTree(data);
+
+ // Rejected on Mac OS.
+ checker.setSafeForMacOS(true);
+ assertCorrupt(
+ "invalid name '\u206B.git' contains ignorable Unicode characters",
+ OBJ_TREE, data);
+ assertSkipListAccepts(OBJ_TREE, data);
+ checker.setIgnore(HAS_DOTGIT, true);
+ checker.checkTree(data);
}
@Test
- public void testInvalidTreeNameIsMacHFSGit3() {
+ public void testInvalidTreeNameIsMacHFSGit3()
+ throws CorruptObjectException {
StringBuilder b = new StringBuilder();
entry(b, "100644 .git\uFEFF");
- byte[] data = Constants.encode(b.toString());
- try {
- checker.setSafeForMacOS(true);
- checker.checkTree(data);
- fail("incorrectly accepted an invalid tree");
- } catch (CorruptObjectException e) {
- assertEquals(
- "invalid name '.git\uFEFF' contains ignorable Unicode characters",
- e.getMessage());
- }
+ byte[] data = encode(b.toString());
+
+ // Fine on POSIX.
+ checker.checkTree(data);
+
+ // Rejected on Mac OS.
+ checker.setSafeForMacOS(true);
+ assertCorrupt(
+ "invalid name '.git\uFEFF' contains ignorable Unicode characters",
+ OBJ_TREE, data);
+ assertSkipListAccepts(OBJ_TREE, data);
+ checker.setIgnore(HAS_DOTGIT, true);
+ checker.checkTree(data);
}
- private static byte[] concat(byte[] b1, byte[] b2) {
- byte[] data = new byte[b1.length + b2.length];
- System.arraycopy(b1, 0, data, 0, b1.length);
- System.arraycopy(b2, 0, data, b1.length, b2.length);
+ private static byte[] concat(byte[]... b) {
+ int n = 0;
+ for (byte[] a : b) {
+ n += a.length;
+ }
+
+ byte[] data = new byte[n];
+ n = 0;
+ for (byte[] a : b) {
+ System.arraycopy(a, 0, data, n, a.length);
+ n += a.length;
+ }
return data;
}
@Test
- public void testInvalidTreeNameIsMacHFSGitCorruptUTF8AtEnd() {
- byte[] data = concat(Constants.encode("100644 .git"),
+ public void testInvalidTreeNameIsMacHFSGitCorruptUTF8AtEnd()
+ throws CorruptObjectException {
+ byte[] data = concat(encode("100644 .git"),
new byte[] { (byte) 0xef });
StringBuilder b = new StringBuilder();
entry(b, "");
- data = concat(data, Constants.encode(b.toString()));
- try {
- checker.setSafeForMacOS(true);
- checker.checkTree(data);
- fail("incorrectly accepted an invalid tree");
- } catch (CorruptObjectException e) {
- assertEquals(
- "invalid name contains byte sequence '0xef' which is not a valid UTF-8 character",
- e.getMessage());
- }
+ data = concat(data, encode(b.toString()));
+
+ // Fine on POSIX.
+ checker.checkTree(data);
+
+ // Rejected on Mac OS.
+ checker.setSafeForMacOS(true);
+ assertCorrupt(
+ "invalid name contains byte sequence '0xef' which is not a valid UTF-8 character",
+ OBJ_TREE, data);
+ assertSkipListAccepts(OBJ_TREE, data);
}
@Test
- public void testInvalidTreeNameIsMacHFSGitCorruptUTF8AtEnd2() {
- byte[] data = concat(Constants.encode("100644 .git"), new byte[] {
+ public void testInvalidTreeNameIsMacHFSGitCorruptUTF8AtEnd2()
+ throws CorruptObjectException {
+ byte[] data = concat(encode("100644 .git"),
+ new byte[] {
(byte) 0xe2, (byte) 0xab });
StringBuilder b = new StringBuilder();
entry(b, "");
- data = concat(data, Constants.encode(b.toString()));
- try {
- checker.setSafeForMacOS(true);
- checker.checkTree(data);
- fail("incorrectly accepted an invalid tree");
- } catch (CorruptObjectException e) {
- assertEquals(
- "invalid name contains byte sequence '0xe2ab' which is not a valid UTF-8 character",
- e.getMessage());
- }
+ data = concat(data, encode(b.toString()));
+
+ // Fine on POSIX.
+ checker.checkTree(data);
+
+ // Rejected on Mac OS.
+ checker.setSafeForMacOS(true);
+ assertCorrupt(
+ "invalid name contains byte sequence '0xe2ab' which is not a valid UTF-8 character",
+ OBJ_TREE, data);
+ assertSkipListAccepts(OBJ_TREE, data);
}
@Test
@@ -1442,7 +1115,7 @@ public class ObjectCheckerTest {
throws CorruptObjectException {
StringBuilder b = new StringBuilder();
entry(b, "100644 .git\u200Cx");
- byte[] data = Constants.encode(b.toString());
+ byte[] data = encode(b.toString());
checker.setSafeForMacOS(true);
checker.checkTree(data);
}
@@ -1452,7 +1125,7 @@ public class ObjectCheckerTest {
throws CorruptObjectException {
StringBuilder b = new StringBuilder();
entry(b, "100644 .kit\u200C");
- byte[] data = Constants.encode(b.toString());
+ byte[] data = encode(b.toString());
checker.setSafeForMacOS(true);
checker.checkTree(data);
}
@@ -1462,21 +1135,19 @@ public class ObjectCheckerTest {
throws CorruptObjectException {
StringBuilder b = new StringBuilder();
entry(b, "100644 .git\u200C");
- byte[] data = Constants.encode(b.toString());
+ byte[] data = encode(b.toString());
checker.checkTree(data);
}
@Test
- public void testInvalidTreeNameIsDotGitDot() {
+ public void testInvalidTreeNameIsDotGitDot() throws CorruptObjectException {
StringBuilder b = new StringBuilder();
entry(b, "100644 .git.");
- byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTree(data);
- fail("incorrectly accepted an invalid tree");
- } catch (CorruptObjectException e) {
- assertEquals("invalid name '.git.'", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("invalid name '.git.'", OBJ_TREE, data);
+ assertSkipListAccepts(OBJ_TREE, data);
+ checker.setIgnore(HAS_DOTGIT, true);
+ checker.checkTree(data);
}
@Test
@@ -1484,20 +1155,19 @@ public class ObjectCheckerTest {
throws CorruptObjectException {
StringBuilder b = new StringBuilder();
entry(b, "100644 .git..");
- checker.checkTree(Constants.encodeASCII(b.toString()));
+ checker.checkTree(encodeASCII(b.toString()));
}
@Test
- public void testInvalidTreeNameIsDotGitSpace() {
+ public void testInvalidTreeNameIsDotGitSpace()
+ throws CorruptObjectException {
StringBuilder b = new StringBuilder();
entry(b, "100644 .git ");
- byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTree(data);
- fail("incorrectly accepted an invalid tree");
- } catch (CorruptObjectException e) {
- assertEquals("invalid name '.git '", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("invalid name '.git '", OBJ_TREE, data);
+ assertSkipListAccepts(OBJ_TREE, data);
+ checker.setIgnore(HAS_DOTGIT, true);
+ checker.checkTree(data);
}
@Test
@@ -1505,7 +1175,7 @@ public class ObjectCheckerTest {
throws CorruptObjectException {
StringBuilder b = new StringBuilder();
entry(b, "100644 .gitfoobar");
- byte[] data = Constants.encodeASCII(b.toString());
+ byte[] data = encodeASCII(b.toString());
checker.checkTree(data);
}
@@ -1514,7 +1184,7 @@ public class ObjectCheckerTest {
throws CorruptObjectException {
StringBuilder b = new StringBuilder();
entry(b, "100644 .gitfoo bar");
- byte[] data = Constants.encodeASCII(b.toString());
+ byte[] data = encodeASCII(b.toString());
checker.checkTree(data);
}
@@ -1523,7 +1193,7 @@ public class ObjectCheckerTest {
throws CorruptObjectException {
StringBuilder b = new StringBuilder();
entry(b, "100644 .gitfoobar.");
- byte[] data = Constants.encodeASCII(b.toString());
+ byte[] data = encodeASCII(b.toString());
checker.checkTree(data);
}
@@ -1532,251 +1202,236 @@ public class ObjectCheckerTest {
throws CorruptObjectException {
StringBuilder b = new StringBuilder();
entry(b, "100644 .gitfoobar..");
- byte[] data = Constants.encodeASCII(b.toString());
+ byte[] data = encodeASCII(b.toString());
checker.checkTree(data);
}
@Test
- public void testInvalidTreeNameIsDotGitDotSpace() {
+ public void testInvalidTreeNameIsDotGitDotSpace()
+ throws CorruptObjectException {
StringBuilder b = new StringBuilder();
entry(b, "100644 .git. ");
- byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTree(data);
- fail("incorrectly accepted an invalid tree");
- } catch (CorruptObjectException e) {
- assertEquals("invalid name '.git. '", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("invalid name '.git. '", OBJ_TREE, data);
+ assertSkipListAccepts(OBJ_TREE, data);
+ checker.setIgnore(HAS_DOTGIT, true);
+ checker.checkTree(data);
}
@Test
- public void testInvalidTreeNameIsDotGitSpaceDot() {
+ public void testInvalidTreeNameIsDotGitSpaceDot()
+ throws CorruptObjectException {
StringBuilder b = new StringBuilder();
entry(b, "100644 .git . ");
- byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTree(data);
- fail("incorrectly accepted an invalid tree");
- } catch (CorruptObjectException e) {
- assertEquals("invalid name '.git . '", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("invalid name '.git . '", OBJ_TREE, data);
+ assertSkipListAccepts(OBJ_TREE, data);
+ checker.setIgnore(HAS_DOTGIT, true);
+ checker.checkTree(data);
}
@Test
- public void testInvalidTreeNameIsGITTilde1() {
+ public void testInvalidTreeNameIsGITTilde1() throws CorruptObjectException {
StringBuilder b = new StringBuilder();
entry(b, "100644 GIT~1");
- byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTree(data);
- fail("incorrectly accepted an invalid tree");
- } catch (CorruptObjectException e) {
- assertEquals("invalid name 'GIT~1'", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("invalid name 'GIT~1'", OBJ_TREE, data);
+ assertSkipListAccepts(OBJ_TREE, data);
+ checker.setIgnore(HAS_DOTGIT, true);
+ checker.checkTree(data);
}
@Test
- public void testInvalidTreeNameIsGiTTilde1() {
+ public void testInvalidTreeNameIsGiTTilde1() throws CorruptObjectException {
StringBuilder b = new StringBuilder();
entry(b, "100644 GiT~1");
- byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTree(data);
- fail("incorrectly accepted an invalid tree");
- } catch (CorruptObjectException e) {
- assertEquals("invalid name 'GiT~1'", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("invalid name 'GiT~1'", OBJ_TREE, data);
+ assertSkipListAccepts(OBJ_TREE, data);
+ checker.setIgnore(HAS_DOTGIT, true);
+ checker.checkTree(data);
}
@Test
public void testValidTreeNameIsGitTilde11() throws CorruptObjectException {
StringBuilder b = new StringBuilder();
entry(b, "100644 GIT~11");
- byte[] data = Constants.encodeASCII(b.toString());
+ byte[] data = encodeASCII(b.toString());
checker.checkTree(data);
}
@Test
public void testInvalidTreeTruncatedInName() {
- final StringBuilder b = new StringBuilder();
+ StringBuilder b = new StringBuilder();
b.append("100644 b");
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTree(data);
- fail("incorrectly accepted an invalid tree");
- } catch (CorruptObjectException e) {
- assertEquals("truncated in name", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("truncated in name", OBJ_TREE, data);
+ assertSkipListRejects("truncated in name", OBJ_TREE, data);
}
@Test
public void testInvalidTreeTruncatedInObjectId() {
- final StringBuilder b = new StringBuilder();
+ StringBuilder b = new StringBuilder();
b.append("100644 b\0\1\2");
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTree(data);
- fail("incorrectly accepted an invalid tree");
- } catch (CorruptObjectException e) {
- assertEquals("truncated in object id", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("truncated in object id", OBJ_TREE, data);
+ assertSkipListRejects("truncated in object id", OBJ_TREE, data);
}
@Test
- public void testInvalidTreeBadSorting1() {
- final StringBuilder b = new StringBuilder();
+ public void testInvalidTreeBadSorting1() throws CorruptObjectException {
+ StringBuilder b = new StringBuilder();
entry(b, "100644 foobar");
entry(b, "100644 fooaaa");
- final byte[] data = Constants.encodeASCII(b.toString());
+ byte[] data = encodeASCII(b.toString());
+
+ assertCorrupt("incorrectly sorted", OBJ_TREE, data);
+
+ ObjectId id = idFor(OBJ_TREE, data);
try {
- checker.checkTree(data);
- fail("incorrectly accepted an invalid tree");
+ checker.check(id, OBJ_TREE, data);
+ fail("Did not throw CorruptObjectException");
} catch (CorruptObjectException e) {
- assertEquals("incorrectly sorted", e.getMessage());
+ assertSame(TREE_NOT_SORTED, e.getErrorType());
+ assertEquals("treeNotSorted: object " + id.name()
+ + ": incorrectly sorted", e.getMessage());
}
+
+ assertSkipListAccepts(OBJ_TREE, data);
+ checker.setIgnore(TREE_NOT_SORTED, true);
+ checker.checkTree(data);
}
@Test
- public void testInvalidTreeBadSorting2() {
- final StringBuilder b = new StringBuilder();
+ public void testInvalidTreeBadSorting2() throws CorruptObjectException {
+ StringBuilder b = new StringBuilder();
entry(b, "40000 a");
entry(b, "100644 a.c");
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTree(data);
- fail("incorrectly accepted an invalid tree");
- } catch (CorruptObjectException e) {
- assertEquals("incorrectly sorted", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("incorrectly sorted", OBJ_TREE, data);
+ assertSkipListAccepts(OBJ_TREE, data);
+ checker.setIgnore(TREE_NOT_SORTED, true);
+ checker.checkTree(data);
}
@Test
- public void testInvalidTreeBadSorting3() {
- final StringBuilder b = new StringBuilder();
+ public void testInvalidTreeBadSorting3() throws CorruptObjectException {
+ StringBuilder b = new StringBuilder();
entry(b, "100644 a0c");
entry(b, "40000 a");
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTree(data);
- fail("incorrectly accepted an invalid tree");
- } catch (CorruptObjectException e) {
- assertEquals("incorrectly sorted", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("incorrectly sorted", OBJ_TREE, data);
+ assertSkipListAccepts(OBJ_TREE, data);
+ checker.setIgnore(TREE_NOT_SORTED, true);
+ checker.checkTree(data);
}
@Test
- public void testInvalidTreeDuplicateNames1() {
- final StringBuilder b = new StringBuilder();
+ public void testInvalidTreeDuplicateNames1_File()
+ throws CorruptObjectException {
+ StringBuilder b = new StringBuilder();
entry(b, "100644 a");
entry(b, "100644 a");
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTree(data);
- fail("incorrectly accepted an invalid tree");
- } catch (CorruptObjectException e) {
- assertEquals("duplicate entry names", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("duplicate entry names", OBJ_TREE, data);
+ assertSkipListAccepts(OBJ_TREE, data);
+ checker.setIgnore(DUPLICATE_ENTRIES, true);
+ checker.checkTree(data);
+ }
+
+ @Test
+ public void testInvalidTreeDuplicateNames1_Tree()
+ throws CorruptObjectException {
+ StringBuilder b = new StringBuilder();
+ entry(b, "40000 a");
+ entry(b, "40000 a");
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("duplicate entry names", OBJ_TREE, data);
+ assertSkipListAccepts(OBJ_TREE, data);
+ checker.setIgnore(DUPLICATE_ENTRIES, true);
+ checker.checkTree(data);
}
@Test
- public void testInvalidTreeDuplicateNames2() {
- final StringBuilder b = new StringBuilder();
+ public void testInvalidTreeDuplicateNames2() throws CorruptObjectException {
+ StringBuilder b = new StringBuilder();
entry(b, "100644 a");
entry(b, "100755 a");
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTree(data);
- fail("incorrectly accepted an invalid tree");
- } catch (CorruptObjectException e) {
- assertEquals("duplicate entry names", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("duplicate entry names", OBJ_TREE, data);
+ assertSkipListAccepts(OBJ_TREE, data);
+ checker.setIgnore(DUPLICATE_ENTRIES, true);
+ checker.checkTree(data);
}
@Test
- public void testInvalidTreeDuplicateNames3() {
- final StringBuilder b = new StringBuilder();
+ public void testInvalidTreeDuplicateNames3() throws CorruptObjectException {
+ StringBuilder b = new StringBuilder();
entry(b, "100644 a");
entry(b, "40000 a");
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTree(data);
- fail("incorrectly accepted an invalid tree");
- } catch (CorruptObjectException e) {
- assertEquals("duplicate entry names", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("duplicate entry names", OBJ_TREE, data);
+ assertSkipListAccepts(OBJ_TREE, data);
+ checker.setIgnore(DUPLICATE_ENTRIES, true);
+ checker.checkTree(data);
}
@Test
- public void testInvalidTreeDuplicateNames4() {
- final StringBuilder b = new StringBuilder();
+ public void testInvalidTreeDuplicateNames4() throws CorruptObjectException {
+ StringBuilder b = new StringBuilder();
entry(b, "100644 a");
entry(b, "100644 a.c");
entry(b, "100644 a.d");
entry(b, "100644 a.e");
entry(b, "40000 a");
entry(b, "100644 zoo");
- final byte[] data = Constants.encodeASCII(b.toString());
- try {
- checker.checkTree(data);
- fail("incorrectly accepted an invalid tree");
- } catch (CorruptObjectException e) {
- assertEquals("duplicate entry names", e.getMessage());
- }
+ byte[] data = encodeASCII(b.toString());
+ assertCorrupt("duplicate entry names", OBJ_TREE, data);
+ assertSkipListAccepts(OBJ_TREE, data);
+ checker.setIgnore(DUPLICATE_ENTRIES, true);
+ checker.checkTree(data);
}
@Test
public void testInvalidTreeDuplicateNames5()
- throws UnsupportedEncodingException {
+ throws UnsupportedEncodingException, CorruptObjectException {
StringBuilder b = new StringBuilder();
- entry(b, "100644 a");
entry(b, "100644 A");
+ entry(b, "100644 a");
byte[] data = b.toString().getBytes("UTF-8");
- try {
- checker.setSafeForWindows(true);
- checker.checkTree(data);
- fail("incorrectly accepted an invalid tree");
- } catch (CorruptObjectException e) {
- assertEquals("duplicate entry names", e.getMessage());
- }
+ checker.setSafeForWindows(true);
+ assertCorrupt("duplicate entry names", OBJ_TREE, data);
+ assertSkipListAccepts(OBJ_TREE, data);
+ checker.setIgnore(DUPLICATE_ENTRIES, true);
+ checker.checkTree(data);
}
@Test
public void testInvalidTreeDuplicateNames6()
- throws UnsupportedEncodingException {
+ throws UnsupportedEncodingException, CorruptObjectException {
StringBuilder b = new StringBuilder();
- entry(b, "100644 a");
entry(b, "100644 A");
+ entry(b, "100644 a");
byte[] data = b.toString().getBytes("UTF-8");
- try {
- checker.setSafeForMacOS(true);
- checker.checkTree(data);
- fail("incorrectly accepted an invalid tree");
- } catch (CorruptObjectException e) {
- assertEquals("duplicate entry names", e.getMessage());
- }
+ checker.setSafeForMacOS(true);
+ assertCorrupt("duplicate entry names", OBJ_TREE, data);
+ assertSkipListAccepts(OBJ_TREE, data);
+ checker.setIgnore(DUPLICATE_ENTRIES, true);
+ checker.checkTree(data);
}
@Test
public void testInvalidTreeDuplicateNames7()
- throws UnsupportedEncodingException {
- try {
- Class.forName("java.text.Normalizer");
- } catch (ClassNotFoundException e) {
- // Ignore this test on Java 5 platform.
- return;
- }
-
+ throws UnsupportedEncodingException, CorruptObjectException {
StringBuilder b = new StringBuilder();
entry(b, "100644 \u0065\u0301");
entry(b, "100644 \u00e9");
byte[] data = b.toString().getBytes("UTF-8");
- try {
- checker.setSafeForMacOS(true);
- checker.checkTree(data);
- fail("incorrectly accepted an invalid tree");
- } catch (CorruptObjectException e) {
- assertEquals("duplicate entry names", e.getMessage());
- }
+ checker.setSafeForMacOS(true);
+ assertCorrupt("duplicate entry names", OBJ_TREE, data);
+ assertSkipListAccepts(OBJ_TREE, data);
+ checker.setIgnore(DUPLICATE_ENTRIES, true);
+ checker.checkTree(data);
}
@Test
@@ -1791,7 +1446,7 @@ public class ObjectCheckerTest {
@Test
public void testRejectNulInPathSegment() {
try {
- checker.checkPathSegment(Constants.encodeASCII("a\u0000b"), 0, 3);
+ checker.checkPathSegment(encodeASCII("a\u0000b"), 0, 3);
fail("incorrectly accepted NUL in middle of name");
} catch (CorruptObjectException e) {
assertEquals("name contains byte 0x00", e.getMessage());
@@ -1893,13 +1548,65 @@ public class ObjectCheckerTest {
private void checkOneName(String name) throws CorruptObjectException {
StringBuilder b = new StringBuilder();
entry(b, "100644 " + name);
- checker.checkTree(Constants.encodeASCII(b.toString()));
+ checker.checkTree(encodeASCII(b.toString()));
}
- private static void entry(final StringBuilder b, final String modeName) {
+ private static void entry(StringBuilder b, final String modeName) {
b.append(modeName);
b.append('\0');
- for (int i = 0; i < Constants.OBJECT_ID_LENGTH; i++)
+ for (int i = 0; i < OBJECT_ID_LENGTH; i++)
b.append((char) i);
}
+
+ private void assertCorrupt(String msg, int type, StringBuilder b) {
+ assertCorrupt(msg, type, encodeASCII(b.toString()));
+ }
+
+ private void assertCorrupt(String msg, int type, byte[] data) {
+ try {
+ checker.check(type, data);
+ fail("Did not throw CorruptObjectException");
+ } catch (CorruptObjectException e) {
+ assertEquals(msg, e.getMessage());
+ }
+ }
+
+ private void assertSkipListAccepts(int type, byte[] data)
+ throws CorruptObjectException {
+ ObjectId id = idFor(type, data);
+ checker.setSkipList(set(id));
+ checker.check(id, type, data);
+ checker.setSkipList(null);
+ }
+
+ private void assertSkipListRejects(String msg, int type, byte[] data) {
+ ObjectId id = idFor(type, data);
+ checker.setSkipList(set(id));
+ try {
+ checker.check(id, type, data);
+ fail("Did not throw CorruptObjectException");
+ } catch (CorruptObjectException e) {
+ assertEquals(msg, e.getMessage());
+ }
+ checker.setSkipList(null);
+ }
+
+ private static ObjectIdSet set(final ObjectId... ids) {
+ return new ObjectIdSet() {
+ @Override
+ public boolean contains(AnyObjectId objectId) {
+ for (ObjectId id : ids) {
+ if (id.equals(objectId)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ };
+ }
+
+ @SuppressWarnings("resource")
+ private static ObjectId idFor(int type, byte[] raw) {
+ return new ObjectInserter.Formatter().idFor(type, raw);
+ }
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/T0002_TreeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/T0002_TreeTest.java
deleted file mode 100644
index 651e62c9ca..0000000000
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/T0002_TreeTest.java
+++ /dev/null
@@ -1,319 +0,0 @@
-/*
- * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org>
- * and other copyright owners as documented in the project's IP log.
- *
- * This program and the accompanying materials are made available
- * under the terms of the Eclipse Distribution License v1.0 which
- * accompanies this distribution, is reproduced below, and is
- * available at http://www.eclipse.org/org/documents/edl-v10.php
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or
- * without modification, are permitted provided that the following
- * conditions are met:
- *
- * - Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * - Redistributions in binary form must reproduce the above
- * copyright notice, this list of conditions and the following
- * disclaimer in the documentation and/or other materials provided
- * with the distribution.
- *
- * - Neither the name of the Eclipse Foundation, Inc. nor the
- * names of its contributors may be used to endorse or promote
- * products derived from this software without specific prior
- * written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
- * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
- * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
- * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
- * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
- * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
- * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
- * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package org.eclipse.jgit.lib;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.util.ArrayList;
-import java.util.List;
-
-import org.eclipse.jgit.test.resources.SampleDataRepositoryTestCase;
-import org.junit.Test;
-
-@SuppressWarnings("deprecation")
-public class T0002_TreeTest extends SampleDataRepositoryTestCase {
- private static final ObjectId SOME_FAKE_ID = ObjectId.fromString(
- "0123456789abcdef0123456789abcdef01234567");
-
- private static int compareNamesUsingSpecialCompare(String a, String b)
- throws UnsupportedEncodingException {
- char lasta = '\0';
- byte[] abytes;
- if (a.length() > 0 && a.charAt(a.length()-1) == '/') {
- lasta = '/';
- a = a.substring(0, a.length() - 1);
- }
- abytes = a.getBytes("ISO-8859-1");
- char lastb = '\0';
- byte[] bbytes;
- if (b.length() > 0 && b.charAt(b.length()-1) == '/') {
- lastb = '/';
- b = b.substring(0, b.length() - 1);
- }
- bbytes = b.getBytes("ISO-8859-1");
- return Tree.compareNames(abytes, bbytes, lasta, lastb);
- }
-
- @Test
- public void test000_sort_01() throws UnsupportedEncodingException {
- assertEquals(0, compareNamesUsingSpecialCompare("a","a"));
- }
-
- @Test
- public void test000_sort_02() throws UnsupportedEncodingException {
- assertEquals(-1, compareNamesUsingSpecialCompare("a","b"));
- assertEquals(1, compareNamesUsingSpecialCompare("b","a"));
- }
-
- @Test
- public void test000_sort_03() throws UnsupportedEncodingException {
- assertEquals(1, compareNamesUsingSpecialCompare("a:","a"));
- assertEquals(1, compareNamesUsingSpecialCompare("a/","a"));
- assertEquals(-1, compareNamesUsingSpecialCompare("a","a/"));
- assertEquals(-1, compareNamesUsingSpecialCompare("a","a:"));
- assertEquals(1, compareNamesUsingSpecialCompare("a:","a/"));
- assertEquals(-1, compareNamesUsingSpecialCompare("a/","a:"));
- }
-
- @Test
- public void test000_sort_04() throws UnsupportedEncodingException {
- assertEquals(-1, compareNamesUsingSpecialCompare("a.a","a/a"));
- assertEquals(1, compareNamesUsingSpecialCompare("a/a","a.a"));
- }
-
- @Test
- public void test000_sort_05() throws UnsupportedEncodingException {
- assertEquals(-1, compareNamesUsingSpecialCompare("a.","a/"));
- assertEquals(1, compareNamesUsingSpecialCompare("a/","a."));
-
- }
-
- @Test
- public void test001_createEmpty() throws IOException {
- final Tree t = new Tree(db);
- assertTrue("isLoaded", t.isLoaded());
- assertTrue("isModified", t.isModified());
- assertTrue("no parent", t.getParent() == null);
- assertTrue("isRoot", t.isRoot());
- assertTrue("no name", t.getName() == null);
- assertTrue("no nameUTF8", t.getNameUTF8() == null);
- assertTrue("has entries array", t.members() != null);
- assertEquals("entries is empty", 0, t.members().length);
- assertEquals("full name is empty", "", t.getFullName());
- assertTrue("no id", t.getId() == null);
- assertTrue("database is r", t.getRepository() == db);
- assertTrue("no foo child", t.findTreeMember("foo") == null);
- assertTrue("no foo child", t.findBlobMember("foo") == null);
- }
-
- @Test
- public void test002_addFile() throws IOException {
- final Tree t = new Tree(db);
- t.setId(SOME_FAKE_ID);
- assertTrue("has id", t.getId() != null);
- assertFalse("not modified", t.isModified());
-
- final String n = "bob";
- final FileTreeEntry f = t.addFile(n);
- assertNotNull("have file", f);
- assertEquals("name matches", n, f.getName());
- assertEquals("name matches", f.getName(), new String(f.getNameUTF8(),
- "UTF-8"));
- assertEquals("full name matches", n, f.getFullName());
- assertTrue("no id", f.getId() == null);
- assertTrue("is modified", t.isModified());
- assertTrue("has no id", t.getId() == null);
- assertTrue("found bob", t.findBlobMember(f.getName()) == f);
-
- final TreeEntry[] i = t.members();
- assertNotNull("members array not null", i);
- assertTrue("iterator is not empty", i != null && i.length > 0);
- assertTrue("iterator returns file", i != null && i[0] == f);
- assertTrue("iterator is empty", i != null && i.length == 1);
- }
-
- @Test
- public void test004_addTree() throws IOException {
- final Tree t = new Tree(db);
- t.setId(SOME_FAKE_ID);
- assertTrue("has id", t.getId() != null);
- assertFalse("not modified", t.isModified());
-
- final String n = "bob";
- final Tree f = t.addTree(n);
- assertNotNull("have tree", f);
- assertEquals("name matches", n, f.getName());
- assertEquals("name matches", f.getName(), new String(f.getNameUTF8(),
- "UTF-8"));
- assertEquals("full name matches", n, f.getFullName());
- assertTrue("no id", f.getId() == null);
- assertTrue("parent matches", f.getParent() == t);
- assertTrue("repository matches", f.getRepository() == db);
- assertTrue("isLoaded", f.isLoaded());
- assertFalse("has items", f.members().length > 0);
- assertFalse("is root", f.isRoot());
- assertTrue("parent is modified", t.isModified());
- assertTrue("parent has no id", t.getId() == null);
- assertTrue("found bob child", t.findTreeMember(f.getName()) == f);
-
- final TreeEntry[] i = t.members();
- assertTrue("iterator is not empty", i.length > 0);
- assertTrue("iterator returns file", i[0] == f);
- assertEquals("iterator is empty", 1, i.length);
- }
-
- @Test
- public void test005_addRecursiveFile() throws IOException {
- final Tree t = new Tree(db);
- final FileTreeEntry f = t.addFile("a/b/c");
- assertNotNull("created f", f);
- assertEquals("c", f.getName());
- assertEquals("b", f.getParent().getName());
- assertEquals("a", f.getParent().getParent().getName());
- assertTrue("t is great-grandparent", t == f.getParent().getParent()
- .getParent());
- }
-
- @Test
- public void test005_addRecursiveTree() throws IOException {
- final Tree t = new Tree(db);
- final Tree f = t.addTree("a/b/c");
- assertNotNull("created f", f);
- assertEquals("c", f.getName());
- assertEquals("b", f.getParent().getName());
- assertEquals("a", f.getParent().getParent().getName());
- assertTrue("t is great-grandparent", t == f.getParent().getParent()
- .getParent());
- }
-
- @Test
- public void test006_addDeepTree() throws IOException {
- final Tree t = new Tree(db);
-
- final Tree e = t.addTree("e");
- assertNotNull("have e", e);
- assertTrue("e.parent == t", e.getParent() == t);
- final Tree f = t.addTree("f");
- assertNotNull("have f", f);
- assertTrue("f.parent == t", f.getParent() == t);
- final Tree g = f.addTree("g");
- assertNotNull("have g", g);
- assertTrue("g.parent == f", g.getParent() == f);
- final Tree h = g.addTree("h");
- assertNotNull("have h", h);
- assertTrue("h.parent = g", h.getParent() == g);
-
- h.setId(SOME_FAKE_ID);
- assertTrue("h not modified", !h.isModified());
- g.setId(SOME_FAKE_ID);
- assertTrue("g not modified", !g.isModified());
- f.setId(SOME_FAKE_ID);
- assertTrue("f not modified", !f.isModified());
- e.setId(SOME_FAKE_ID);
- assertTrue("e not modified", !e.isModified());
- t.setId(SOME_FAKE_ID);
- assertTrue("t not modified.", !t.isModified());
-
- assertEquals("full path of h ok", "f/g/h", h.getFullName());
- assertTrue("Can find h", t.findTreeMember(h.getFullName()) == h);
- assertTrue("Can't find f/z", t.findBlobMember("f/z") == null);
- assertTrue("Can't find y/z", t.findBlobMember("y/z") == null);
-
- final FileTreeEntry i = h.addFile("i");
- assertNotNull(i);
- assertEquals("full path of i ok", "f/g/h/i", i.getFullName());
- assertTrue("Can find i", t.findBlobMember(i.getFullName()) == i);
- assertTrue("h modified", h.isModified());
- assertTrue("g modified", g.isModified());
- assertTrue("f modified", f.isModified());
- assertTrue("e not modified", !e.isModified());
- assertTrue("t modified", t.isModified());
-
- assertTrue("h no id", h.getId() == null);
- assertTrue("g no id", g.getId() == null);
- assertTrue("f no id", f.getId() == null);
- assertTrue("e has id", e.getId() != null);
- assertTrue("t no id", t.getId() == null);
- }
-
- @Test
- public void test007_manyFileLookup() throws IOException {
- final Tree t = new Tree(db);
- final List<FileTreeEntry> files = new ArrayList<FileTreeEntry>(26 * 26);
- for (char level1 = 'a'; level1 <= 'z'; level1++) {
- for (char level2 = 'a'; level2 <= 'z'; level2++) {
- final String n = "." + level1 + level2 + "9";
- final FileTreeEntry f = t.addFile(n);
- assertNotNull("File " + n + " added.", f);
- assertEquals(n, f.getName());
- files.add(f);
- }
- }
- assertEquals(files.size(), t.memberCount());
- final TreeEntry[] ents = t.members();
- assertNotNull(ents);
- assertEquals(files.size(), ents.length);
- for (int k = 0; k < ents.length; k++) {
- assertTrue("File " + files.get(k).getName()
- + " is at " + k + ".", files.get(k) == ents[k]);
- }
- }
-
- @Test
- public void test008_SubtreeInternalSorting() throws IOException {
- final Tree t = new Tree(db);
- final FileTreeEntry e0 = t.addFile("a-b");
- final FileTreeEntry e1 = t.addFile("a-");
- final FileTreeEntry e2 = t.addFile("a=b");
- final Tree e3 = t.addTree("a");
- final FileTreeEntry e4 = t.addFile("a=");
-
- final TreeEntry[] ents = t.members();
- assertSame(e1, ents[0]);
- assertSame(e0, ents[1]);
- assertSame(e3, ents[2]);
- assertSame(e4, ents[3]);
- assertSame(e2, ents[4]);
- }
-
- @Test
- public void test009_SymlinkAndGitlink() throws IOException {
- final Tree symlinkTree = mapTree("symlink");
- assertTrue("Symlink entry exists", symlinkTree.existsBlob("symlink.txt"));
- final Tree gitlinkTree = mapTree("gitlink");
- assertTrue("Gitlink entry exists", gitlinkTree.existsBlob("submodule"));
- }
-
- private Tree mapTree(String name) throws IOException {
- ObjectId id = db.resolve(name + "^{tree}");
- return new Tree(db, id, db.open(id).getCachedBytes());
- }
-}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/ObjectWalkTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/ObjectWalkTest.java
index 2a59f58c66..9c9edc1476 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/ObjectWalkTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/ObjectWalkTest.java
@@ -47,11 +47,10 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.FileTreeEntry;
+import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.Tree;
+import org.eclipse.jgit.lib.TreeFormatter;
import org.junit.Test;
@SuppressWarnings("deprecation")
@@ -220,28 +219,24 @@ public class ObjectWalkTest extends RevWalkTestCase {
.fromString("abbbfafe3129f85747aba7bfac992af77134c607");
final RevTree tree_root, tree_A, tree_AB;
final RevCommit b;
- {
- Tree root = new Tree(db);
- Tree A = root.addTree("A");
- FileTreeEntry B = root.addFile("B");
- B.setId(bId);
-
- Tree A_A = A.addTree("A");
- Tree A_B = A.addTree("B");
-
- try (final ObjectInserter inserter = db.newObjectInserter()) {
- A_A.setId(inserter.insert(Constants.OBJ_TREE, A_A.format()));
- A_B.setId(inserter.insert(Constants.OBJ_TREE, A_B.format()));
- A.setId(inserter.insert(Constants.OBJ_TREE, A.format()));
- root.setId(inserter.insert(Constants.OBJ_TREE, root.format()));
- inserter.flush();
- }
-
- tree_root = rw.parseTree(root.getId());
- tree_A = rw.parseTree(A.getId());
- tree_AB = rw.parseTree(A_A.getId());
- assertSame(tree_AB, rw.parseTree(A_B.getId()));
- b = commit(rw.parseTree(root.getId()));
+ try (ObjectInserter inserter = db.newObjectInserter()) {
+ ObjectId empty = inserter.insert(new TreeFormatter());
+
+ TreeFormatter A = new TreeFormatter();
+ A.append("A", FileMode.TREE, empty);
+ A.append("B", FileMode.TREE, empty);
+ ObjectId idA = inserter.insert(A);
+
+ TreeFormatter root = new TreeFormatter();
+ root.append("A", FileMode.TREE, idA);
+ root.append("B", FileMode.REGULAR_FILE, bId);
+ ObjectId idRoot = inserter.insert(root);
+ inserter.flush();
+
+ tree_root = objw.parseTree(idRoot);
+ tree_A = objw.parseTree(idA);
+ tree_AB = objw.parseTree(empty);
+ b = commit(tree_root);
}
markStart(b);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevCommitParseTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevCommitParseTest.java
index beda2a7b97..885c1b5b2d 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevCommitParseTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevCommitParseTest.java
@@ -43,13 +43,18 @@
package org.eclipse.jgit.revwalk;
+import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
+import java.nio.charset.IllegalCharsetNameException;
+import java.nio.charset.UnsupportedCharsetException;
import java.util.TimeZone;
import org.eclipse.jgit.junit.RepositoryTestCase;
@@ -304,6 +309,86 @@ public class RevCommitParseTest extends RepositoryTestCase {
}
@Test
+ public void testParse_incorrectUtf8Name() throws Exception {
+ ByteArrayOutputStream b = new ByteArrayOutputStream();
+ b.write("tree 9788669ad918b6fcce64af8882fc9a81cb6aba67\n"
+ .getBytes(UTF_8));
+ b.write("author au <a@example.com> 1218123387 +0700\n".getBytes(UTF_8));
+ b.write("committer co <c@example.com> 1218123390 -0500\n"
+ .getBytes(UTF_8));
+ b.write("encoding 'utf8'\n".getBytes(UTF_8));
+ b.write("\n".getBytes(UTF_8));
+ b.write("Sm\u00f6rg\u00e5sbord\n".getBytes(UTF_8));
+
+ RevCommit c = new RevCommit(
+ id("9473095c4cb2f12aefe1db8a355fe3fafba42f67"));
+ c.parseCanonical(new RevWalk(db), b.toByteArray());
+ assertEquals("'utf8'", c.getEncodingName());
+ assertEquals("Sm\u00f6rg\u00e5sbord\n", c.getFullMessage());
+
+ try {
+ c.getEncoding();
+ fail("Expected " + IllegalCharsetNameException.class);
+ } catch (IllegalCharsetNameException badName) {
+ assertEquals("'utf8'", badName.getMessage());
+ }
+ }
+
+ @Test
+ public void testParse_illegalEncoding() throws Exception {
+ ByteArrayOutputStream b = new ByteArrayOutputStream();
+ b.write("tree 9788669ad918b6fcce64af8882fc9a81cb6aba67\n".getBytes(UTF_8));
+ b.write("author au <a@example.com> 1218123387 +0700\n".getBytes(UTF_8));
+ b.write("committer co <c@example.com> 1218123390 -0500\n".getBytes(UTF_8));
+ b.write("encoding utf-8logoutputencoding=gbk\n".getBytes(UTF_8));
+ b.write("\n".getBytes(UTF_8));
+ b.write("message\n".getBytes(UTF_8));
+
+ RevCommit c = new RevCommit(
+ id("9473095c4cb2f12aefe1db8a355fe3fafba42f67"));
+ c.parseCanonical(new RevWalk(db), b.toByteArray());
+ assertEquals("utf-8logoutputencoding=gbk", c.getEncodingName());
+ assertEquals("message\n", c.getFullMessage());
+ assertEquals("message", c.getShortMessage());
+ assertTrue(c.getFooterLines().isEmpty());
+ assertEquals("au", c.getAuthorIdent().getName());
+
+ try {
+ c.getEncoding();
+ fail("Expected " + IllegalCharsetNameException.class);
+ } catch (IllegalCharsetNameException badName) {
+ assertEquals("utf-8logoutputencoding=gbk", badName.getMessage());
+ }
+ }
+
+ @Test
+ public void testParse_unsupportedEncoding() throws Exception {
+ ByteArrayOutputStream b = new ByteArrayOutputStream();
+ b.write("tree 9788669ad918b6fcce64af8882fc9a81cb6aba67\n".getBytes(UTF_8));
+ b.write("author au <a@example.com> 1218123387 +0700\n".getBytes(UTF_8));
+ b.write("committer co <c@example.com> 1218123390 -0500\n".getBytes(UTF_8));
+ b.write("encoding it_IT.UTF8\n".getBytes(UTF_8));
+ b.write("\n".getBytes(UTF_8));
+ b.write("message\n".getBytes(UTF_8));
+
+ RevCommit c = new RevCommit(
+ id("9473095c4cb2f12aefe1db8a355fe3fafba42f67"));
+ c.parseCanonical(new RevWalk(db), b.toByteArray());
+ assertEquals("it_IT.UTF8", c.getEncodingName());
+ assertEquals("message\n", c.getFullMessage());
+ assertEquals("message", c.getShortMessage());
+ assertTrue(c.getFooterLines().isEmpty());
+ assertEquals("au", c.getAuthorIdent().getName());
+
+ try {
+ c.getEncoding();
+ fail("Expected " + UnsupportedCharsetException.class);
+ } catch (UnsupportedCharsetException badName) {
+ assertEquals("it_IT.UTF8", badName.getMessage());
+ }
+ }
+
+ @Test
public void testParse_NoMessage() throws Exception {
final String msg = "";
final RevCommit c = create(msg);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevTagParseTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevTagParseTest.java
index 614f49bf03..82505caf22 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevTagParseTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevTagParseTest.java
@@ -43,6 +43,7 @@
package org.eclipse.jgit.revwalk;
+import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
@@ -362,6 +363,44 @@ public class RevTagParseTest extends RepositoryTestCase {
}
@Test
+ public void testParse_illegalEncoding() throws Exception {
+ ByteArrayOutputStream b = new ByteArrayOutputStream();
+ b.write("object 9788669ad918b6fcce64af8882fc9a81cb6aba67\n".getBytes(UTF_8));
+ b.write("type tree\n".getBytes(UTF_8));
+ b.write("tag v1.0\n".getBytes(UTF_8));
+ b.write("tagger t <t@example.com> 1218123387 +0700\n".getBytes(UTF_8));
+ b.write("encoding utf-8logoutputencoding=gbk\n".getBytes(UTF_8));
+ b.write("\n".getBytes(UTF_8));
+ b.write("message\n".getBytes(UTF_8));
+
+ RevTag t = new RevTag(id("9473095c4cb2f12aefe1db8a355fe3fafba42f67"));
+ t.parseCanonical(new RevWalk(db), b.toByteArray());
+
+ assertEquals("t", t.getTaggerIdent().getName());
+ assertEquals("message", t.getShortMessage());
+ assertEquals("message\n", t.getFullMessage());
+ }
+
+ @Test
+ public void testParse_unsupportedEncoding() throws Exception {
+ ByteArrayOutputStream b = new ByteArrayOutputStream();
+ b.write("object 9788669ad918b6fcce64af8882fc9a81cb6aba67\n".getBytes(UTF_8));
+ b.write("type tree\n".getBytes(UTF_8));
+ b.write("tag v1.0\n".getBytes(UTF_8));
+ b.write("tagger t <t@example.com> 1218123387 +0700\n".getBytes(UTF_8));
+ b.write("encoding it_IT.UTF8\n".getBytes(UTF_8));
+ b.write("\n".getBytes(UTF_8));
+ b.write("message\n".getBytes(UTF_8));
+
+ RevTag t = new RevTag(id("9473095c4cb2f12aefe1db8a355fe3fafba42f67"));
+ t.parseCanonical(new RevWalk(db), b.toByteArray());
+
+ assertEquals("t", t.getTaggerIdent().getName());
+ assertEquals("message", t.getShortMessage());
+ assertEquals("message\n", t.getFullMessage());
+ }
+
+ @Test
public void testParse_NoMessage() throws Exception {
final String msg = "";
final RevTag c = create(msg);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/AtomicPushTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/AtomicPushTest.java
index 782e414b62..c1e078d10d 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/AtomicPushTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/AtomicPushTest.java
@@ -112,12 +112,9 @@ public class AtomicPushTest {
public void pushNonAtomic() throws Exception {
PushResult r;
server.setPerformsAtomicTransactions(false);
- Transport tn = testProtocol.open(uri, client, "server");
- try {
+ try (Transport tn = testProtocol.open(uri, client, "server")) {
tn.setPushAtomic(false);
r = tn.push(NullProgressMonitor.INSTANCE, commands());
- } finally {
- tn.close();
}
RemoteRefUpdate one = r.getRemoteUpdate("refs/heads/one");
@@ -131,12 +128,9 @@ public class AtomicPushTest {
@Test
public void pushAtomicClientGivesUpEarly() throws Exception {
PushResult r;
- Transport tn = testProtocol.open(uri, client, "server");
- try {
+ try (Transport tn = testProtocol.open(uri, client, "server")) {
tn.setPushAtomic(true);
r = tn.push(NullProgressMonitor.INSTANCE, commands());
- } finally {
- tn.close();
}
RemoteRefUpdate one = r.getRemoteUpdate("refs/heads/one");
@@ -167,8 +161,7 @@ public class AtomicPushTest {
ObjectId.zeroId()));
server.setPerformsAtomicTransactions(false);
- Transport tn = testProtocol.open(uri, client, "server");
- try {
+ try (Transport tn = testProtocol.open(uri, client, "server")) {
tn.setPushAtomic(true);
tn.push(NullProgressMonitor.INSTANCE, cmds);
fail("did not throw TransportException");
@@ -176,8 +169,6 @@ public class AtomicPushTest {
assertEquals(
uri + ": " + JGitText.get().atomicPushNotSupported,
e.getMessage());
- } finally {
- tn.close();
}
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BundleWriterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BundleWriterTest.java
index 461530896d..a83a993330 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BundleWriterTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BundleWriterTest.java
@@ -168,8 +168,10 @@ public class BundleWriterTest extends SampleDataRepositoryTestCase {
final ByteArrayInputStream in = new ByteArrayInputStream(bundle);
final RefSpec rs = new RefSpec("refs/heads/*:refs/heads/*");
final Set<RefSpec> refs = Collections.singleton(rs);
- return new TransportBundleStream(newRepo, uri, in).fetch(
- NullProgressMonitor.INSTANCE, refs);
+ try (TransportBundleStream transport = new TransportBundleStream(
+ newRepo, uri, in)) {
+ return transport.fetch(NullProgressMonitor.INSTANCE, refs);
+ }
}
private byte[] makeBundle(final String name,
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ReceivePackAdvertiseRefsHookTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ReceivePackAdvertiseRefsHookTest.java
index aa5914fe03..94bc383db7 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ReceivePackAdvertiseRefsHookTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ReceivePackAdvertiseRefsHookTest.java
@@ -116,12 +116,9 @@ public class ReceivePackAdvertiseRefsHookTest extends LocalDiskRepositoryTestCas
// Clone from dst into src
//
- Transport t = Transport.open(src, uriOf(dst));
- try {
+ try (Transport t = Transport.open(src, uriOf(dst))) {
t.fetch(PM, Collections.singleton(new RefSpec("+refs/*:refs/*")));
assertEquals(B, src.resolve(R_MASTER));
- } finally {
- t.close();
}
// Now put private stuff into dst.
@@ -144,7 +141,8 @@ public class ReceivePackAdvertiseRefsHookTest extends LocalDiskRepositoryTestCas
@Test
public void testFilterHidesPrivate() throws Exception {
Map<String, Ref> refs;
- TransportLocal t = new TransportLocal(src, uriOf(dst), dst.getDirectory()) {
+ try (TransportLocal t = new TransportLocal(src, uriOf(dst),
+ dst.getDirectory()) {
@Override
ReceivePack createReceivePack(final Repository db) {
db.close();
@@ -154,16 +152,10 @@ public class ReceivePackAdvertiseRefsHookTest extends LocalDiskRepositoryTestCas
rp.setAdvertiseRefsHook(new HidePrivateHook());
return rp;
}
- };
- try {
- PushConnection c = t.openPush();
- try {
+ }) {
+ try (PushConnection c = t.openPush()) {
refs = c.getRefsMap();
- } finally {
- c.close();
}
- } finally {
- t.close();
}
assertNotNull(refs);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RefSpecTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RefSpecTest.java
index 3f5fcbbf07..4f833509d9 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RefSpecTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RefSpecTest.java
@@ -341,6 +341,41 @@ public class RefSpecTest {
}
@Test
+ public void testWildcardAfterText1() {
+ RefSpec a = new RefSpec("refs/heads/*/for-linus:refs/remotes/mine/*-blah");
+ assertTrue(a.isWildcard());
+ assertTrue(a.matchDestination("refs/remotes/mine/x-blah"));
+ assertTrue(a.matchDestination("refs/remotes/mine/foo-blah"));
+ assertTrue(a.matchDestination("refs/remotes/mine/foo/x-blah"));
+ assertFalse(a.matchDestination("refs/remotes/origin/foo/x-blah"));
+
+ RefSpec b = a.expandFromSource("refs/heads/foo/for-linus");
+ assertEquals("refs/remotes/mine/foo-blah", b.getDestination());
+ RefSpec c = a.expandFromDestination("refs/remotes/mine/foo-blah");
+ assertEquals("refs/heads/foo/for-linus", c.getSource());
+ }
+
+ @Test
+ public void testWildcardAfterText2() {
+ RefSpec a = new RefSpec("refs/heads*/for-linus:refs/remotes/mine/*");
+ assertTrue(a.isWildcard());
+ assertTrue(a.matchSource("refs/headsx/for-linus"));
+ assertTrue(a.matchSource("refs/headsfoo/for-linus"));
+ assertTrue(a.matchSource("refs/headsx/foo/for-linus"));
+ assertFalse(a.matchSource("refs/headx/for-linus"));
+
+ RefSpec b = a.expandFromSource("refs/headsx/for-linus");
+ assertEquals("refs/remotes/mine/x", b.getDestination());
+ RefSpec c = a.expandFromDestination("refs/remotes/mine/x");
+ assertEquals("refs/headsx/for-linus", c.getSource());
+
+ RefSpec d = a.expandFromSource("refs/headsx/foo/for-linus");
+ assertEquals("refs/remotes/mine/x/foo", d.getDestination());
+ RefSpec e = a.expandFromDestination("refs/remotes/mine/x/foo");
+ assertEquals("refs/headsx/foo/for-linus", e.getSource());
+ }
+
+ @Test
public void testWildcardMirror() {
RefSpec a = new RefSpec("*:*");
assertTrue(a.isWildcard());
@@ -404,21 +439,6 @@ public class RefSpecTest {
}
@Test(expected = IllegalArgumentException.class)
- public void invalidWhenWildcardAfterText() {
- assertNotNull(new RefSpec("refs/heads/wrong*:refs/heads/right/*"));
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void invalidWhenWildcardBeforeText() {
- assertNotNull(new RefSpec("*wrong:right/*"));
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void invalidWhenWildcardBeforeTextAtEnd() {
- assertNotNull(new RefSpec("refs/heads/*wrong:right/*"));
- }
-
- @Test(expected = IllegalArgumentException.class)
public void invalidSourceDoubleSlashes() {
assertNotNull(new RefSpec("refs/heads//wrong"));
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportTest.java
index 55e1e44206..5519f61ac2 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportTest.java
@@ -61,13 +61,10 @@ import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.test.resources.SampleDataRepositoryTestCase;
-import org.junit.After;
import org.junit.Before;
import org.junit.Test;
public class TransportTest extends SampleDataRepositoryTestCase {
- private Transport transport;
-
private RemoteConfig remoteConfig;
@Override
@@ -77,17 +74,6 @@ public class TransportTest extends SampleDataRepositoryTestCase {
final Config config = db.getConfig();
remoteConfig = new RemoteConfig(config, "test");
remoteConfig.addURI(new URIish("http://everyones.loves.git/u/2"));
- transport = null;
- }
-
- @Override
- @After
- public void tearDown() throws Exception {
- if (transport != null) {
- transport.close();
- transport = null;
- }
- super.tearDown();
}
/**
@@ -99,10 +85,11 @@ public class TransportTest extends SampleDataRepositoryTestCase {
@Test
public void testFindRemoteRefUpdatesNoWildcardNoTracking()
throws IOException {
- transport = Transport.open(db, remoteConfig);
- final Collection<RemoteRefUpdate> result = transport
- .findRemoteRefUpdatesFor(Collections.nCopies(1, new RefSpec(
- "refs/heads/master:refs/heads/x")));
+ Collection<RemoteRefUpdate> result;
+ try (Transport transport = Transport.open(db, remoteConfig)) {
+ result = transport.findRemoteRefUpdatesFor(Collections.nCopies(1,
+ new RefSpec("refs/heads/master:refs/heads/x")));
+ }
assertEquals(1, result.size());
final RemoteRefUpdate rru = result.iterator().next();
@@ -122,10 +109,11 @@ public class TransportTest extends SampleDataRepositoryTestCase {
@Test
public void testFindRemoteRefUpdatesNoWildcardNoDestination()
throws IOException {
- transport = Transport.open(db, remoteConfig);
- final Collection<RemoteRefUpdate> result = transport
- .findRemoteRefUpdatesFor(Collections.nCopies(1, new RefSpec(
- "+refs/heads/master")));
+ Collection<RemoteRefUpdate> result;
+ try (Transport transport = Transport.open(db, remoteConfig)) {
+ result = transport.findRemoteRefUpdatesFor(
+ Collections.nCopies(1, new RefSpec("+refs/heads/master")));
+ }
assertEquals(1, result.size());
final RemoteRefUpdate rru = result.iterator().next();
@@ -143,10 +131,11 @@ public class TransportTest extends SampleDataRepositoryTestCase {
*/
@Test
public void testFindRemoteRefUpdatesWildcardNoTracking() throws IOException {
- transport = Transport.open(db, remoteConfig);
- final Collection<RemoteRefUpdate> result = transport
- .findRemoteRefUpdatesFor(Collections.nCopies(1, new RefSpec(
- "+refs/heads/*:refs/heads/test/*")));
+ Collection<RemoteRefUpdate> result;
+ try (Transport transport = Transport.open(db, remoteConfig)) {
+ result = transport.findRemoteRefUpdatesFor(Collections.nCopies(1,
+ new RefSpec("+refs/heads/*:refs/heads/test/*")));
+ }
assertEquals(12, result.size());
boolean foundA = false;
@@ -171,12 +160,14 @@ public class TransportTest extends SampleDataRepositoryTestCase {
*/
@Test
public void testFindRemoteRefUpdatesTwoRefSpecs() throws IOException {
- transport = Transport.open(db, remoteConfig);
final RefSpec specA = new RefSpec("+refs/heads/a:refs/heads/b");
final RefSpec specC = new RefSpec("+refs/heads/c:refs/heads/d");
final Collection<RefSpec> specs = Arrays.asList(specA, specC);
- final Collection<RemoteRefUpdate> result = transport
- .findRemoteRefUpdatesFor(specs);
+
+ Collection<RemoteRefUpdate> result;
+ try (Transport transport = Transport.open(db, remoteConfig)) {
+ result = transport.findRemoteRefUpdatesFor(specs);
+ }
assertEquals(2, result.size());
boolean foundA = false;
@@ -202,10 +193,12 @@ public class TransportTest extends SampleDataRepositoryTestCase {
public void testFindRemoteRefUpdatesTrackingRef() throws IOException {
remoteConfig.addFetchRefSpec(new RefSpec(
"refs/heads/*:refs/remotes/test/*"));
- transport = Transport.open(db, remoteConfig);
- final Collection<RemoteRefUpdate> result = transport
- .findRemoteRefUpdatesFor(Collections.nCopies(1, new RefSpec(
- "+refs/heads/a:refs/heads/a")));
+
+ Collection<RemoteRefUpdate> result;
+ try (Transport transport = Transport.open(db, remoteConfig)) {
+ result = transport.findRemoteRefUpdatesFor(Collections.nCopies(1,
+ new RefSpec("+refs/heads/a:refs/heads/a")));
+ }
assertEquals(1, result.size());
final TrackingRefUpdate tru = result.iterator().next()
@@ -225,20 +218,18 @@ public class TransportTest extends SampleDataRepositoryTestCase {
config.addURI(new URIish("../" + otherDir));
// Should not throw NoRemoteRepositoryException
- transport = Transport.open(db, config);
+ Transport.open(db, config).close();
}
@Test
public void testLocalTransportFetchWithoutLocalRepository()
throws Exception {
URIish uri = new URIish("file://" + db.getWorkTree().getAbsolutePath());
- transport = Transport.open(uri);
- FetchConnection fetchConnection = transport.openFetch();
- try {
- Ref head = fetchConnection.getRef(Constants.HEAD);
- assertNotNull(head);
- } finally {
- fetchConnection.close();
+ try (Transport transport = Transport.open(uri)) {
+ try (FetchConnection fetchConnection = transport.openFetch()) {
+ Ref head = fetchConnection.getRef(Constants.HEAD);
+ assertNotNull(head);
+ }
}
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/URIishTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/URIishTest.java
index 2078dd337b..e55d373347 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/URIishTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/URIishTest.java
@@ -460,6 +460,48 @@ public class URIishTest {
}
@Test
+ public void testSshProtoWithEmailUserAndPort() throws Exception {
+ final String str = "ssh://user.name@email.com@example.com:33/some/p ath";
+ URIish u = new URIish(str);
+ assertEquals("ssh", u.getScheme());
+ assertTrue(u.isRemote());
+ assertEquals("/some/p ath", u.getRawPath());
+ assertEquals("/some/p ath", u.getPath());
+ assertEquals("example.com", u.getHost());
+ assertEquals("user.name@email.com", u.getUser());
+ assertNull(u.getPass());
+ assertEquals(33, u.getPort());
+ assertEquals("ssh://user.name%40email.com@example.com:33/some/p ath",
+ u.toPrivateString());
+ assertEquals("ssh://user.name%40email.com@example.com:33/some/p%20ath",
+ u.toPrivateASCIIString());
+ assertEquals(u.setPass(null).toPrivateString(), u.toString());
+ assertEquals(u.setPass(null).toPrivateASCIIString(), u.toASCIIString());
+ assertEquals(u, new URIish(str));
+ }
+
+ @Test
+ public void testSshProtoWithEmailUserPassAndPort() throws Exception {
+ final String str = "ssh://user.name@email.com:pass@wor:d@example.com:33/some/p ath";
+ URIish u = new URIish(str);
+ assertEquals("ssh", u.getScheme());
+ assertTrue(u.isRemote());
+ assertEquals("/some/p ath", u.getRawPath());
+ assertEquals("/some/p ath", u.getPath());
+ assertEquals("example.com", u.getHost());
+ assertEquals("user.name@email.com", u.getUser());
+ assertEquals("pass@wor:d", u.getPass());
+ assertEquals(33, u.getPort());
+ assertEquals("ssh://user.name%40email.com:pass%40wor%3ad@example.com:33/some/p ath",
+ u.toPrivateString());
+ assertEquals("ssh://user.name%40email.com:pass%40wor%3ad@example.com:33/some/p%20ath",
+ u.toPrivateASCIIString());
+ assertEquals(u.setPass(null).toPrivateString(), u.toString());
+ assertEquals(u.setPass(null).toPrivateASCIIString(), u.toASCIIString());
+ assertEquals(u, new URIish(str));
+ }
+
+ @Test
public void testSshProtoWithADUserPassAndPort() throws Exception {
final String str = "ssh://DOMAIN\\user:pass@example.com:33/some/p ath";
URIish u = new URIish(str);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/TreeWalkBasicDiffTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/TreeWalkBasicDiffTest.java
index aca7c80fd7..c3ff7df8f2 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/TreeWalkBasicDiffTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/TreeWalkBasicDiffTest.java
@@ -44,7 +44,6 @@
package org.eclipse.jgit.treewalk;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
-import static org.eclipse.jgit.lib.Constants.OBJ_TREE;
import static org.eclipse.jgit.lib.Constants.encode;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -54,11 +53,10 @@ import org.eclipse.jgit.junit.RepositoryTestCase;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.Tree;
+import org.eclipse.jgit.lib.TreeFormatter;
import org.eclipse.jgit.treewalk.filter.TreeFilter;
import org.junit.Test;
-@SuppressWarnings("deprecation")
public class TreeWalkBasicDiffTest extends RepositoryTestCase {
@Test
public void testMissingSubtree_DetectFileAdded_FileModified()
@@ -72,62 +70,63 @@ public class TreeWalkBasicDiffTest extends RepositoryTestCase {
// Create sub-a/empty, sub-c/empty = hello.
{
- final Tree root = new Tree(db);
+ TreeFormatter root = new TreeFormatter();
{
- final Tree subA = root.addTree("sub-a");
- subA.addFile("empty").setId(aFileId);
- subA.setId(inserter.insert(OBJ_TREE, subA.format()));
+ TreeFormatter subA = new TreeFormatter();
+ subA.append("empty", FileMode.REGULAR_FILE, aFileId);
+ root.append("sub-a", FileMode.TREE, inserter.insert(subA));
}
{
- final Tree subC = root.addTree("sub-c");
- subC.addFile("empty").setId(cFileId1);
- subC.setId(inserter.insert(OBJ_TREE, subC.format()));
+ TreeFormatter subC = new TreeFormatter();
+ subC.append("empty", FileMode.REGULAR_FILE, cFileId1);
+ root.append("sub-c", FileMode.TREE, inserter.insert(subC));
}
- oldTree = inserter.insert(OBJ_TREE, root.format());
+ oldTree = inserter.insert(root);
}
// Create sub-a/empty, sub-b/empty, sub-c/empty.
{
- final Tree root = new Tree(db);
+ TreeFormatter root = new TreeFormatter();
{
- final Tree subA = root.addTree("sub-a");
- subA.addFile("empty").setId(aFileId);
- subA.setId(inserter.insert(OBJ_TREE, subA.format()));
+ TreeFormatter subA = new TreeFormatter();
+ subA.append("empty", FileMode.REGULAR_FILE, aFileId);
+ root.append("sub-a", FileMode.TREE, inserter.insert(subA));
}
{
- final Tree subB = root.addTree("sub-b");
- subB.addFile("empty").setId(bFileId);
- subB.setId(inserter.insert(OBJ_TREE, subB.format()));
+ TreeFormatter subB = new TreeFormatter();
+ subB.append("empty", FileMode.REGULAR_FILE, bFileId);
+ root.append("sub-b", FileMode.TREE, inserter.insert(subB));
}
{
- final Tree subC = root.addTree("sub-c");
- subC.addFile("empty").setId(cFileId2);
- subC.setId(inserter.insert(OBJ_TREE, subC.format()));
+ TreeFormatter subC = new TreeFormatter();
+ subC.append("empty", FileMode.REGULAR_FILE, cFileId2);
+ root.append("sub-c", FileMode.TREE, inserter.insert(subC));
}
- newTree = inserter.insert(OBJ_TREE, root.format());
+ newTree = inserter.insert(root);
}
inserter.flush();
}
- final TreeWalk tw = new TreeWalk(db);
- tw.reset(oldTree, newTree);
- tw.setRecursive(true);
- tw.setFilter(TreeFilter.ANY_DIFF);
+ try (TreeWalk tw = new TreeWalk(db)) {
+ tw.reset(oldTree, newTree);
+ tw.setRecursive(true);
+ tw.setFilter(TreeFilter.ANY_DIFF);
- assertTrue(tw.next());
- assertEquals("sub-b/empty", tw.getPathString());
- assertEquals(FileMode.MISSING, tw.getFileMode(0));
- assertEquals(FileMode.REGULAR_FILE, tw.getFileMode(1));
- assertEquals(ObjectId.zeroId(), tw.getObjectId(0));
- assertEquals(bFileId, tw.getObjectId(1));
+ assertTrue(tw.next());
+ assertEquals("sub-b/empty", tw.getPathString());
+ assertEquals(FileMode.MISSING, tw.getFileMode(0));
+ assertEquals(FileMode.REGULAR_FILE, tw.getFileMode(1));
+ assertEquals(ObjectId.zeroId(), tw.getObjectId(0));
+ assertEquals(bFileId, tw.getObjectId(1));
- assertTrue(tw.next());
- assertEquals("sub-c/empty", tw.getPathString());
- assertEquals(FileMode.REGULAR_FILE, tw.getFileMode(0));
- assertEquals(FileMode.REGULAR_FILE, tw.getFileMode(1));
- assertEquals(cFileId1, tw.getObjectId(0));
- assertEquals(cFileId2, tw.getObjectId(1));
+ assertTrue(tw.next());
+ assertEquals("sub-c/empty", tw.getPathString());
+ assertEquals(FileMode.REGULAR_FILE, tw.getFileMode(0));
+ assertEquals(FileMode.REGULAR_FILE, tw.getFileMode(1));
+ assertEquals(cFileId1, tw.getObjectId(0));
+ assertEquals(cFileId2, tw.getObjectId(1));
- assertFalse(tw.next());
+ assertFalse(tw.next());
+ }
}
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/PathFilterGroupTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/PathFilterGroupTest.java
index d0062e1990..5edc1924f2 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/PathFilterGroupTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/PathFilterGroupTest.java
@@ -43,11 +43,18 @@
package org.eclipse.jgit.treewalk.filter;
+import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheEditor;
@@ -58,6 +65,7 @@ import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.errors.StopWalkException;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Sets;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.junit.Before;
import org.junit.Test;
@@ -66,6 +74,8 @@ public class PathFilterGroupTest {
private TreeFilter filter;
+ private Map<String, TreeFilter> singles;
+
@Before
public void setup() {
// @formatter:off
@@ -81,64 +91,75 @@ public class PathFilterGroupTest {
};
// @formatter:on
filter = PathFilterGroup.createFromStrings(paths);
+ singles = new HashMap<>();
+ for (String path : paths) {
+ singles.put(path, PathFilterGroup.createFromStrings(path));
+ }
}
@Test
public void testExact() throws MissingObjectException,
IncorrectObjectTypeException, IOException {
- assertTrue(filter.include(fakeWalk("a")));
- assertTrue(filter.include(fakeWalk("b/c")));
- assertTrue(filter.include(fakeWalk("c/d/e")));
- assertTrue(filter.include(fakeWalk("c/d/f")));
- assertTrue(filter.include(fakeWalk("d/e/f/g")));
- assertTrue(filter.include(fakeWalk("d/e/f/g.x")));
+ assertMatches(Sets.of("a"), fakeWalk("a"));
+ assertMatches(Sets.of("b/c"), fakeWalk("b/c"));
+ assertMatches(Sets.of("c/d/e"), fakeWalk("c/d/e"));
+ assertMatches(Sets.of("c/d/f"), fakeWalk("c/d/f"));
+ assertMatches(Sets.of("d/e/f/g"), fakeWalk("d/e/f/g"));
+ assertMatches(Sets.of("d/e/f/g.x"), fakeWalk("d/e/f/g.x"));
}
@Test
public void testNoMatchButClose() throws MissingObjectException,
IncorrectObjectTypeException, IOException {
- assertFalse(filter.include(fakeWalk("a+")));
- assertFalse(filter.include(fakeWalk("b+/c")));
- assertFalse(filter.include(fakeWalk("c+/d/e")));
- assertFalse(filter.include(fakeWalk("c+/d/f")));
- assertFalse(filter.include(fakeWalk("c/d.a")));
- assertFalse(filter.include(fakeWalk("d+/e/f/g")));
+ assertNoMatches(fakeWalk("a+"));
+ assertNoMatches(fakeWalk("b+/c"));
+ assertNoMatches(fakeWalk("c+/d/e"));
+ assertNoMatches(fakeWalk("c+/d/f"));
+ assertNoMatches(fakeWalk("c/d.a"));
+ assertNoMatches(fakeWalk("d+/e/f/g"));
}
@Test
public void testJustCommonPrefixIsNotMatch() throws MissingObjectException,
IncorrectObjectTypeException, IOException {
- assertFalse(filter.include(fakeWalk("b/a")));
- assertFalse(filter.include(fakeWalk("b/d")));
- assertFalse(filter.include(fakeWalk("c/d/a")));
- assertFalse(filter.include(fakeWalk("d/e/e")));
+ assertNoMatches(fakeWalk("b/a"));
+ assertNoMatches(fakeWalk("b/d"));
+ assertNoMatches(fakeWalk("c/d/a"));
+ assertNoMatches(fakeWalk("d/e/e"));
+ assertNoMatches(fakeWalk("d/e/f/g.y"));
}
@Test
public void testKeyIsPrefixOfFilter() throws MissingObjectException,
IncorrectObjectTypeException, IOException {
- assertTrue(filter.include(fakeWalk("b")));
- assertTrue(filter.include(fakeWalk("c/d")));
- assertTrue(filter.include(fakeWalk("c/d")));
- assertTrue(filter.include(fakeWalk("c")));
- assertTrue(filter.include(fakeWalk("d/e/f")));
- assertTrue(filter.include(fakeWalk("d/e")));
- assertTrue(filter.include(fakeWalk("d")));
+ assertMatches(Sets.of("b/c"), fakeWalkAtSubtree("b"));
+ assertMatches(Sets.of("c/d/e", "c/d/f"), fakeWalkAtSubtree("c/d"));
+ assertMatches(Sets.of("c/d/e", "c/d/f"), fakeWalkAtSubtree("c"));
+ assertMatches(Sets.of("d/e/f/g", "d/e/f/g.x"),
+ fakeWalkAtSubtree("d/e/f"));
+ assertMatches(Sets.of("d/e/f/g", "d/e/f/g.x"),
+ fakeWalkAtSubtree("d/e"));
+ assertMatches(Sets.of("d/e/f/g", "d/e/f/g.x"), fakeWalkAtSubtree("d"));
+
+ assertNoMatches(fakeWalk("b"));
+ assertNoMatches(fakeWalk("c/d"));
+ assertNoMatches(fakeWalk("c"));
+ assertNoMatches(fakeWalk("d/e/f"));
+ assertNoMatches(fakeWalk("d/e"));
+ assertNoMatches(fakeWalk("d"));
+
}
@Test
public void testFilterIsPrefixOfKey() throws MissingObjectException,
IncorrectObjectTypeException, IOException {
- assertTrue(filter.include(fakeWalk("a/b")));
- assertTrue(filter.include(fakeWalk("b/c/d")));
- assertTrue(filter.include(fakeWalk("c/d/e/f")));
- assertTrue(filter.include(fakeWalk("c/d/f/g")));
- assertTrue(filter.include(fakeWalk("d/e/f/g/h")));
- assertTrue(filter.include(fakeWalk("d/e/f/g/y")));
- assertTrue(filter.include(fakeWalk("d/e/f/g.x/h")));
- // listed before g/y, so can't StopWalk here, but it's not included
- // either
- assertFalse(filter.include(fakeWalk("d/e/f/g.y")));
+ assertMatches(Sets.of("a"), fakeWalk("a/b"));
+ assertMatches(Sets.of("b/c"), fakeWalk("b/c/d"));
+ assertMatches(Sets.of("c/d/e"), fakeWalk("c/d/e/f"));
+ assertMatches(Sets.of("c/d/f"), fakeWalk("c/d/f/g"));
+ assertMatches(Sets.of("d/e/f/g"), fakeWalk("d/e/f/g/h"));
+ assertMatches(Sets.of("d/e/f/g"), fakeWalk("d/e/f/g/y"));
+ assertMatches(Sets.of("d/e/f/g.x"), fakeWalk("d/e/f/g.x/h"));
}
@Test
@@ -182,6 +203,10 @@ public class PathFilterGroupTest {
// less obvious #2 due to git sorting order
filter.include(fakeWalk("d/e/f/g/h.txt"));
+ // listed before g/y, so can't StopWalk here
+ filter.include(fakeWalk("d/e/f/g.y"));
+ singles.get("d/e/f/g").include(fakeWalk("d/e/f/g.y"));
+
// non-ascii
try {
filter.include(fakeWalk("\u00C0"));
@@ -191,6 +216,44 @@ public class PathFilterGroupTest {
}
}
+ private void assertNoMatches(TreeWalk tw) throws MissingObjectException,
+ IncorrectObjectTypeException, IOException {
+ assertMatches(Sets.<String> of(), tw);
+ }
+
+ private void assertMatches(Set<String> expect, TreeWalk tw)
+ throws MissingObjectException, IncorrectObjectTypeException,
+ IOException {
+ List<String> actual = new ArrayList<>();
+ for (String path : singles.keySet()) {
+ if (includes(singles.get(path), tw)) {
+ actual.add(path);
+ }
+ }
+
+ String[] e = expect.toArray(new String[expect.size()]);
+ String[] a = actual.toArray(new String[actual.size()]);
+ Arrays.sort(e);
+ Arrays.sort(a);
+ assertArrayEquals(e, a);
+
+ if (expect.isEmpty()) {
+ assertFalse(includes(filter, tw));
+ } else {
+ assertTrue(includes(filter, tw));
+ }
+ }
+
+ private static boolean includes(TreeFilter f, TreeWalk tw)
+ throws MissingObjectException, IncorrectObjectTypeException,
+ IOException {
+ try {
+ return f.include(tw);
+ } catch (StopWalkException e) {
+ return false;
+ }
+ }
+
TreeWalk fakeWalk(final String path) throws IOException {
DirCache dc = DirCache.newInCore();
DirCacheEditor dce = dc.editor();
@@ -210,4 +273,25 @@ public class PathFilterGroupTest {
return ret;
}
+ TreeWalk fakeWalkAtSubtree(final String path) throws IOException {
+ DirCache dc = DirCache.newInCore();
+ DirCacheEditor dce = dc.editor();
+ dce.add(new DirCacheEditor.PathEdit(path + "/README") {
+ public void apply(DirCacheEntry ent) {
+ ent.setFileMode(FileMode.REGULAR_FILE);
+ }
+ });
+ dce.finish();
+
+ TreeWalk ret = new TreeWalk((ObjectReader) null);
+ ret.addTree(new DirCacheIterator(dc));
+ ret.next();
+ while (!path.equals(ret.getPathString())) {
+ if (ret.isSubtree()) {
+ ret.enterSubtree();
+ }
+ ret.next();
+ }
+ return ret;
+ }
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/ChangeIdUtilTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/ChangeIdUtilTest.java
index 7273cdbabc..aaeb79c64a 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/ChangeIdUtilTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/ChangeIdUtilTest.java
@@ -45,7 +45,6 @@ package org.eclipse.jgit.util;
import static org.junit.Assert.assertEquals;
-import java.io.IOException;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.junit.MockSystemReader;
@@ -113,7 +112,7 @@ public class ChangeIdUtilTest {
}
@Test
- public void testId() throws IOException {
+ public void testId() {
String msg = "A\nMessage\n";
ObjectId id = ChangeIdUtil.computeChangeId(treeId, parentId, p, q, msg);
assertEquals("73f3751208ac92cbb76f9a26ac4a0d9d472e381b", ObjectId
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FileUtilTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FileUtilTest.java
index 0d7d31b3ad..1f78e02087 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FileUtilTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FileUtilTest.java
@@ -54,6 +54,7 @@ import java.util.regex.Matcher;
import org.eclipse.jgit.junit.JGitTestUtil;
import org.junit.After;
+import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
@@ -424,19 +425,28 @@ public class FileUtilTest {
@Test
public void testCreateSymlink() throws IOException {
FS fs = FS.DETECTED;
- try {
- fs.createSymLink(new File(trash, "x"), "y");
- } catch (IOException e) {
- if (fs.supportsSymlinks())
- fail("FS claims to support symlinks but attempt to create symlink failed");
- return;
- }
- assertTrue(fs.supportsSymlinks());
+ // show test as ignored if the FS doesn't support symlinks
+ Assume.assumeTrue(fs.supportsSymlinks());
+ fs.createSymLink(new File(trash, "x"), "y");
String target = fs.readSymLink(new File(trash, "x"));
assertEquals("y", target);
}
@Test
+ public void testCreateSymlinkOverrideExisting() throws IOException {
+ FS fs = FS.DETECTED;
+ // show test as ignored if the FS doesn't support symlinks
+ Assume.assumeTrue(fs.supportsSymlinks());
+ File file = new File(trash, "x");
+ fs.createSymLink(file, "y");
+ String target = fs.readSymLink(file);
+ assertEquals("y", target);
+ fs.createSymLink(file, "z");
+ target = fs.readSymLink(file);
+ assertEquals("z", target);
+ }
+
+ @Test
public void testRelativize_doc() {
// This is the javadoc example
String base = toOSPathString("c:\\Users\\jdoe\\eclipse\\git\\project");
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/PathsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/PathsTest.java
new file mode 100644
index 0000000000..7542ec8910
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/PathsTest.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.util;
+
+import static org.eclipse.jgit.util.Paths.compare;
+import static org.eclipse.jgit.util.Paths.compareSameName;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.FileMode;
+import org.junit.Test;
+
+public class PathsTest {
+ @Test
+ public void testStripTrailingSeparator() {
+ assertNull(Paths.stripTrailingSeparator(null));
+ assertEquals("", Paths.stripTrailingSeparator(""));
+ assertEquals("a", Paths.stripTrailingSeparator("a"));
+ assertEquals("a/boo", Paths.stripTrailingSeparator("a/boo"));
+ assertEquals("a/boo", Paths.stripTrailingSeparator("a/boo/"));
+ assertEquals("a/boo", Paths.stripTrailingSeparator("a/boo//"));
+ assertEquals("a/boo", Paths.stripTrailingSeparator("a/boo///"));
+ }
+
+ @Test
+ public void testPathCompare() {
+ byte[] a = Constants.encode("afoo/bar.c");
+ byte[] b = Constants.encode("bfoo/bar.c");
+
+ assertEquals(0, compare(a, 1, a.length, 0, b, 1, b.length, 0));
+ assertEquals(-1, compare(a, 0, a.length, 0, b, 0, b.length, 0));
+ assertEquals(1, compare(b, 0, b.length, 0, a, 0, a.length, 0));
+
+ a = Constants.encode("a");
+ b = Constants.encode("aa");
+ assertEquals(-97, compare(a, 0, a.length, 0, b, 0, b.length, 0));
+ assertEquals(0, compare(a, 0, a.length, 0, b, 0, 1, 0));
+ assertEquals(0, compare(a, 0, a.length, 0, b, 1, 2, 0));
+ assertEquals(0, compareSameName(a, 0, a.length, b, 1, b.length, 0));
+ assertEquals(0, compareSameName(a, 0, a.length, b, 0, 1, 0));
+ assertEquals(-50, compareSameName(a, 0, a.length, b, 0, b.length, 0));
+ assertEquals(97, compareSameName(b, 0, b.length, a, 0, a.length, 0));
+
+ a = Constants.encode("a");
+ b = Constants.encode("a");
+ assertEquals(0, compare(
+ a, 0, a.length, FileMode.TREE.getBits(),
+ b, 0, b.length, FileMode.TREE.getBits()));
+ assertEquals(0, compare(
+ a, 0, a.length, FileMode.REGULAR_FILE.getBits(),
+ b, 0, b.length, FileMode.REGULAR_FILE.getBits()));
+ assertEquals(-47, compare(
+ a, 0, a.length, FileMode.REGULAR_FILE.getBits(),
+ b, 0, b.length, FileMode.TREE.getBits()));
+ assertEquals(47, compare(
+ a, 0, a.length, FileMode.TREE.getBits(),
+ b, 0, b.length, FileMode.REGULAR_FILE.getBits()));
+
+ assertEquals(0, compareSameName(
+ a, 0, a.length,
+ b, 0, b.length, FileMode.TREE.getBits()));
+ assertEquals(0, compareSameName(
+ a, 0, a.length,
+ b, 0, b.length, FileMode.REGULAR_FILE.getBits()));
+
+ a = Constants.encode("a.c");
+ b = Constants.encode("a");
+ byte[] c = Constants.encode("a0c");
+ assertEquals(-1, compare(
+ a, 0, a.length, FileMode.REGULAR_FILE.getBits(),
+ b, 0, b.length, FileMode.TREE.getBits()));
+ assertEquals(-1, compare(
+ b, 0, b.length, FileMode.TREE.getBits(),
+ c, 0, c.length, FileMode.REGULAR_FILE.getBits()));
+ }
+}
diff --git a/org.eclipse.jgit.ui/BUCK b/org.eclipse.jgit.ui/BUCK
new file mode 100644
index 0000000000..fcd87cf9aa
--- /dev/null
+++ b/org.eclipse.jgit.ui/BUCK
@@ -0,0 +1,7 @@
+java_library(
+ name = 'ui',
+ srcs = glob(['src/**']),
+ resources = glob(['resources/**']),
+ deps = ['//org.eclipse.jgit:jgit'],
+ visibility = ['PUBLIC'],
+)
diff --git a/org.eclipse.jgit.ui/src/org/eclipse/jgit/awtui/AwtCredentialsProvider.java b/org.eclipse.jgit.ui/src/org/eclipse/jgit/awtui/AwtCredentialsProvider.java
index fd26bfa7f9..a9967ae49e 100644
--- a/org.eclipse.jgit.ui/src/org/eclipse/jgit/awtui/AwtCredentialsProvider.java
+++ b/org.eclipse.jgit.ui/src/org/eclipse/jgit/awtui/AwtCredentialsProvider.java
@@ -56,15 +56,20 @@ import javax.swing.JPasswordField;
import javax.swing.JTextField;
import org.eclipse.jgit.errors.UnsupportedCredentialItem;
+import org.eclipse.jgit.transport.ChainingCredentialsProvider;
import org.eclipse.jgit.transport.CredentialItem;
import org.eclipse.jgit.transport.CredentialsProvider;
+import org.eclipse.jgit.transport.NetRCCredentialsProvider;
import org.eclipse.jgit.transport.URIish;
/** Interacts with the user during authentication by using AWT/Swing dialogs. */
public class AwtCredentialsProvider extends CredentialsProvider {
/** Install this implementation as the default. */
public static void install() {
- CredentialsProvider.setDefault(new AwtCredentialsProvider());
+ final AwtCredentialsProvider c = new AwtCredentialsProvider();
+ CredentialsProvider cp = new ChainingCredentialsProvider(
+ new NetRCCredentialsProvider(), c);
+ CredentialsProvider.setDefault(cp);
}
@Override
diff --git a/org.eclipse.jgit/.settings/.api_filters b/org.eclipse.jgit/.settings/.api_filters
index b2a8f677f3..36041f8144 100644
--- a/org.eclipse.jgit/.settings/.api_filters
+++ b/org.eclipse.jgit/.settings/.api_filters
@@ -1,5 +1,45 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<component id="org.eclipse.jgit" version="2">
+ <resource path="META-INF/MANIFEST.MF" type="org.eclipse.jgit.lib.FileTreeEntry">
+ <filter id="305324134">
+ <message_arguments>
+ <message_argument value="org.eclipse.jgit.lib.FileTreeEntry"/>
+ <message_argument value="org.eclipse.jgit_4.2.0"/>
+ </message_arguments>
+ </filter>
+ </resource>
+ <resource path="META-INF/MANIFEST.MF" type="org.eclipse.jgit.lib.GitlinkTreeEntry">
+ <filter id="305324134">
+ <message_arguments>
+ <message_argument value="org.eclipse.jgit.lib.GitlinkTreeEntry"/>
+ <message_argument value="org.eclipse.jgit_4.2.0"/>
+ </message_arguments>
+ </filter>
+ </resource>
+ <resource path="META-INF/MANIFEST.MF" type="org.eclipse.jgit.lib.SymlinkTreeEntry">
+ <filter id="305324134">
+ <message_arguments>
+ <message_argument value="org.eclipse.jgit.lib.SymlinkTreeEntry"/>
+ <message_argument value="org.eclipse.jgit_4.2.0"/>
+ </message_arguments>
+ </filter>
+ </resource>
+ <resource path="META-INF/MANIFEST.MF" type="org.eclipse.jgit.lib.Tree">
+ <filter id="305324134">
+ <message_arguments>
+ <message_argument value="org.eclipse.jgit.lib.Tree"/>
+ <message_argument value="org.eclipse.jgit_4.2.0"/>
+ </message_arguments>
+ </filter>
+ </resource>
+ <resource path="META-INF/MANIFEST.MF" type="org.eclipse.jgit.lib.TreeEntry">
+ <filter id="305324134">
+ <message_arguments>
+ <message_argument value="org.eclipse.jgit.lib.TreeEntry"/>
+ <message_argument value="org.eclipse.jgit_4.2.0"/>
+ </message_arguments>
+ </filter>
+ </resource>
<resource path="src/org/eclipse/jgit/attributes/AttributesNode.java" type="org.eclipse.jgit.attributes.AttributesNode">
<filter comment="attributes weren't really usable in earlier versions" id="338792546">
<message_arguments>
diff --git a/org.eclipse.jgit/BUCK b/org.eclipse.jgit/BUCK
new file mode 100644
index 0000000000..73e2080576
--- /dev/null
+++ b/org.eclipse.jgit/BUCK
@@ -0,0 +1,20 @@
+SRCS = glob(['src/**'])
+RESOURCES = glob(['resources/**'])
+
+java_library(
+ name = 'jgit',
+ srcs = SRCS,
+ resources = RESOURCES,
+ deps = [
+ '//lib:javaewah',
+ '//lib:jsch',
+ '//lib:httpcomponents',
+ '//lib:slf4j-api',
+ ],
+ visibility = ['PUBLIC'],
+)
+
+java_sources(
+ name = 'jgit_src',
+ srcs = SRCS + RESOURCES,
+)
diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF
index 2a953b559d..3d3e74f5fd 100644
--- a/org.eclipse.jgit/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit/META-INF/MANIFEST.MF
@@ -59,15 +59,17 @@ Export-Package: org.eclipse.jgit.annotations;version="4.2.0",
org.eclipse.jgit.ignore;version="4.2.0",
org.eclipse.jgit.ignore.internal;version="4.2.0";x-friends:="org.eclipse.jgit.test",
org.eclipse.jgit.internal;version="4.2.0";x-friends:="org.eclipse.jgit.test,org.eclipse.jgit.http.test",
+ org.eclipse.jgit.internal.ketch;version="4.2.0";x-friends:="org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm",
org.eclipse.jgit.internal.storage.dfs;version="4.2.0";x-friends:="org.eclipse.jgit.test,org.eclipse.jgit.http.server",
org.eclipse.jgit.internal.storage.file;version="4.2.0";
x-friends:="org.eclipse.jgit.test,
org.eclipse.jgit.junit,
org.eclipse.jgit.junit.http,
org.eclipse.jgit.http.server,
- org.eclipse.jgit.java7.test,
+ org.eclipse.jgit.pgm.test,
org.eclipse.jgit.pgm",
org.eclipse.jgit.internal.storage.pack;version="4.2.0";x-friends:="org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm",
+ org.eclipse.jgit.internal.storage.reftree;version="4.2.0";x-friends:="org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm",
org.eclipse.jgit.lib;version="4.2.0";
uses:="org.eclipse.jgit.revwalk,
org.eclipse.jgit.treewalk.filter,
diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
index 0e9b0b59e6..992e10bad6 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
@@ -99,6 +99,7 @@ cannotSquashFixupWithoutPreviousCommit=Cannot {0} without previous commit.
cannotStoreObjects=cannot store objects
cannotResolveUniquelyAbbrevObjectId=Could not resolve uniquely the abbreviated object ID
cannotUnloadAModifiedTree=Cannot unload a modified tree.
+cannotUpdateUnbornBranch=Cannot update unborn branch
cannotWorkWithOtherStagesThanZeroRightNow=Cannot work with other stages than zero right now. Won't write corrupt index.
cannotWriteObjectsPath=Cannot write {0}/{1}: {2}
canOnlyCherryPickCommitsWithOneParent=Cannot cherry-pick commit ''{0}'' because it has {1} parents, only commits with exactly one parent are supported.
@@ -125,14 +126,15 @@ connectionFailed=connection failed
connectionTimeOut=Connection time out: {0}
contextMustBeNonNegative=context must be >= 0
corruptionDetectedReReadingAt=Corruption detected re-reading at {0}
+corruptObjectBadDate=bad date
+corruptObjectBadEmail=bad email
corruptObjectBadStream=bad stream
corruptObjectBadStreamCorruptHeader=bad stream, corrupt header
+corruptObjectBadTimezone=bad time zone
corruptObjectDuplicateEntryNames=duplicate entry names
corruptObjectGarbageAfterSize=garbage after size
corruptObjectIncorrectLength=incorrect length
corruptObjectIncorrectSorting=incorrectly sorted
-corruptObjectInvalidAuthor=invalid author
-corruptObjectInvalidCommitter=invalid committer
corruptObjectInvalidEntryMode=invalid entry mode
corruptObjectInvalidMode=invalid mode
corruptObjectInvalidModeChar=invalid mode character
@@ -151,11 +153,11 @@ corruptObjectInvalidNameNul=invalid name 'NUL'
corruptObjectInvalidNamePrn=invalid name 'PRN'
corruptObjectInvalidObject=invalid object
corruptObjectInvalidParent=invalid parent
-corruptObjectInvalidTagger=invalid tagger
corruptObjectInvalidTree=invalid tree
corruptObjectInvalidType=invalid type
corruptObjectInvalidType2=invalid type {0}
corruptObjectMalformedHeader=malformed header: {0}
+corruptObjectMissingEmail=missing email
corruptObjectNameContainsByte=name contains byte 0x%x
corruptObjectNameContainsChar=name contains '%c'
corruptObjectNameContainsNullByte=name contains byte 0x00
@@ -181,6 +183,7 @@ corruptObjectPackfileChecksumIncorrect=Packfile checksum incorrect.
corruptObjectTruncatedInMode=truncated in mode
corruptObjectTruncatedInName=truncated in name
corruptObjectTruncatedInObjectId=truncated in object id
+corruptObjectZeroId=entry points to null SHA-1
couldNotCheckOutBecauseOfConflicts=Could not check out because of conflicts
couldNotDeleteLockFileShouldNotHappen=Could not delete lock file. Should not happen
couldNotDeleteTemporaryIndexFileShouldNotHappen=Could not delete temporary index file. Should not happen
@@ -432,6 +435,7 @@ noXMLParserAvailable=No XML parser available.
objectAtHasBadZlibStream=Object at {0} in {1} has bad zlib stream
objectAtPathDoesNotHaveId=Object at path "{0}" does not have an id assigned. All object ids must be assigned prior to writing a tree.
objectIsCorrupt=Object {0} is corrupt: {1}
+objectIsCorrupt3={0}: object {1}: {2}
objectIsNotA=Object {0} is not a {1}.
objectNotFound=Object {0} not found.
objectNotFoundIn=Object {0} not found in {1}.
@@ -595,6 +599,7 @@ transportExceptionInvalid=Invalid {0} {1}:{2}
transportExceptionMissingAssumed=Missing assumed {0}
transportExceptionReadRef=read {0}
transportNeedsRepository=Transport needs repository
+transportProvidedRefWithNoObjectId=Transport provided ref {0} with no object id
transportProtoAmazonS3=Amazon S3
transportProtoBundleFile=Git Bundle File
transportProtoFTP=FTP
diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/ketch/KetchText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/ketch/KetchText.properties
new file mode 100644
index 0000000000..1fbb7cb3b5
--- /dev/null
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/ketch/KetchText.properties
@@ -0,0 +1,13 @@
+accepted=accepted.
+cannotFetchFromLocalReplica=cannot fetch from LocalReplica
+failed=failed!
+invalidFollowerUri=invalid follower URI
+leaderFailedToStore=leader failed to store
+localReplicaRequired=LocalReplica instance is required
+mismatchedTxnNamespace=mismatched txnNamespace; expected {0} found {1}
+outsideTxnNamespace=ref {0} is outside of txnNamespace {1}
+proposingUpdates=Proposing updates
+queuedProposalFailedToApply=queued proposal failed to apply
+starting=starting!
+unsupportedVoterCount=unsupported voter count {0}, expected one of {1}
+waitingForQueue=Waiting for queue
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java
index 67fb342fe2..3b94f16f1a 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java
@@ -43,6 +43,10 @@
*/
package org.eclipse.jgit.api;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+import static org.eclipse.jgit.lib.FileMode.GITLINK;
+import static org.eclipse.jgit.lib.FileMode.TYPE_TREE;
+
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
@@ -58,12 +62,12 @@ import org.eclipse.jgit.dircache.DirCacheBuilder;
import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.dircache.DirCacheIterator;
import org.eclipse.jgit.internal.JGitText;
-import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.treewalk.FileTreeIterator;
-import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.NameConflictTreeWalk;
import org.eclipse.jgit.treewalk.TreeWalk.OperationType;
import org.eclipse.jgit.treewalk.WorkingTreeIterator;
import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
@@ -135,15 +139,12 @@ public class AddCommand extends GitCommand<DirCache> {
throw new NoFilepatternException(JGitText.get().atLeastOnePatternIsRequired);
checkCallable();
DirCache dc = null;
- boolean addAll = false;
- if (filepatterns.contains(".")) //$NON-NLS-1$
- addAll = true;
+ boolean addAll = filepatterns.contains("."); //$NON-NLS-1$
try (ObjectInserter inserter = repo.newObjectInserter();
- final TreeWalk tw = new TreeWalk(repo)) {
+ NameConflictTreeWalk tw = new NameConflictTreeWalk(repo)) {
tw.setOperationType(OperationType.CHECKIN_OP);
dc = repo.lockDirCache();
- DirCacheIterator c;
DirCacheBuilder builder = dc.builder();
tw.addTree(new DirCacheBuildIterator(builder));
@@ -151,62 +152,85 @@ public class AddCommand extends GitCommand<DirCache> {
workingTreeIterator = new FileTreeIterator(repo);
workingTreeIterator.setDirCacheIterator(tw, 0);
tw.addTree(workingTreeIterator);
- tw.setRecursive(true);
if (!addAll)
tw.setFilter(PathFilterGroup.createFromStrings(filepatterns));
- String lastAddedFile = null;
+ byte[] lastAdded = null;
while (tw.next()) {
- String path = tw.getPathString();
-
+ DirCacheIterator c = tw.getTree(0, DirCacheIterator.class);
WorkingTreeIterator f = tw.getTree(1, WorkingTreeIterator.class);
- if (tw.getTree(0, DirCacheIterator.class) == null &&
- f != null && f.isEntryIgnored()) {
+ if (c == null && f != null && f.isEntryIgnored()) {
// file is not in index but is ignored, do nothing
+ continue;
+ } else if (c == null && update) {
+ // Only update of existing entries was requested.
+ continue;
+ }
+
+ DirCacheEntry entry = c != null ? c.getDirCacheEntry() : null;
+ if (entry != null && entry.getStage() > 0
+ && lastAdded != null
+ && lastAdded.length == tw.getPathLength()
+ && tw.isPathPrefix(lastAdded, lastAdded.length) == 0) {
+ // In case of an existing merge conflict the
+ // DirCacheBuildIterator iterates over all stages of
+ // this path, we however want to add only one
+ // new DirCacheEntry per path.
+ continue;
+ }
+
+ if (tw.isSubtree() && !tw.isDirectoryFileConflict()) {
+ tw.enterSubtree();
+ continue;
+ }
+
+ if (f == null) { // working tree file does not exist
+ if (entry != null
+ && (!update || GITLINK == entry.getFileMode())) {
+ builder.add(entry);
+ }
+ continue;
+ }
+
+ if (entry != null && entry.isAssumeValid()) {
+ // Index entry is marked assume valid. Even though
+ // the user specified the file to be added JGit does
+ // not consider the file for addition.
+ builder.add(entry);
+ continue;
+ }
+
+ if (f.getEntryRawMode() == TYPE_TREE) {
+ // Index entry exists and is symlink, gitlink or file,
+ // otherwise the tree would have been entered above.
+ // Replace the index entry by diving into tree of files.
+ tw.enterSubtree();
+ continue;
+ }
+
+ byte[] path = tw.getRawPath();
+ if (entry == null || entry.getStage() > 0) {
+ entry = new DirCacheEntry(path);
}
- // In case of an existing merge conflict the
- // DirCacheBuildIterator iterates over all stages of
- // this path, we however want to add only one
- // new DirCacheEntry per path.
- else if (!(path.equals(lastAddedFile))) {
- if (!(update && tw.getTree(0, DirCacheIterator.class) == null)) {
- c = tw.getTree(0, DirCacheIterator.class);
- if (f != null) { // the file exists
- long sz = f.getEntryLength();
- DirCacheEntry entry = new DirCacheEntry(path);
- if (c == null || c.getDirCacheEntry() == null
- || !c.getDirCacheEntry().isAssumeValid()) {
- FileMode mode = f.getIndexFileMode(c);
- entry.setFileMode(mode);
-
- if (FileMode.GITLINK != mode) {
- entry.setLength(sz);
- entry.setLastModified(f
- .getEntryLastModified());
- long contentSize = f
- .getEntryContentLength();
- InputStream in = f.openEntryStream();
- try {
- entry.setObjectId(inserter.insert(
- Constants.OBJ_BLOB, contentSize, in));
- } finally {
- in.close();
- }
- } else
- entry.setObjectId(f.getEntryObjectId());
- builder.add(entry);
- lastAddedFile = path;
- } else {
- builder.add(c.getDirCacheEntry());
- }
-
- } else if (c != null
- && (!update || FileMode.GITLINK == c
- .getEntryFileMode()))
- builder.add(c.getDirCacheEntry());
+ FileMode mode = f.getIndexFileMode(c);
+ entry.setFileMode(mode);
+
+ if (GITLINK != mode) {
+ entry.setLength(f.getEntryLength());
+ entry.setLastModified(f.getEntryLastModified());
+ long len = f.getEntryContentLength();
+ try (InputStream in = f.openEntryStream()) {
+ ObjectId id = inserter.insert(OBJ_BLOB, len, in);
+ entry.setObjectId(id);
}
+ } else {
+ entry.setLength(0);
+ entry.setLastModified(0);
+ entry.setObjectId(f.getEntryObjectId());
}
+ builder.add(entry);
+ lastAdded = path;
}
inserter.flush();
builder.commit();
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java
index 6a945e4d39..676ae03009 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java
@@ -47,6 +47,7 @@ import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
+import java.nio.file.StandardCopyOption;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
@@ -141,9 +142,13 @@ public class ApplyCommand extends GitCommand<ApplyResult> {
case RENAME:
f = getFile(fh.getOldPath(), false);
File dest = getFile(fh.getNewPath(), false);
- if (!f.renameTo(dest))
+ try {
+ FileUtils.rename(f, dest,
+ StandardCopyOption.ATOMIC_MOVE);
+ } catch (IOException e) {
throw new PatchApplyException(MessageFormat.format(
- JGitText.get().renameFileFailed, f, dest));
+ JGitText.get().renameFileFailed, f, dest), e);
+ }
break;
case COPY:
f = getFile(fh.getOldPath(), false);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java
index 8743ea9ac7..4f918fa357 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java
@@ -331,9 +331,16 @@ public class CheckoutCommand extends GitCommand<Ref> {
}
private String getShortBranchName(Ref headRef) {
- if (headRef.getTarget().getName().equals(headRef.getName()))
- return headRef.getTarget().getObjectId().getName();
- return Repository.shortenRefName(headRef.getTarget().getName());
+ if (headRef.isSymbolic()) {
+ return Repository.shortenRefName(headRef.getTarget().getName());
+ }
+ // Detached HEAD. Every non-symbolic ref in the ref database has an
+ // object id, so this cannot be null.
+ ObjectId id = headRef.getObjectId();
+ if (id == null) {
+ throw new NullPointerException();
+ }
+ return id.getName();
}
/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java
index b3bc319aef..2ac8729507 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java
@@ -61,6 +61,7 @@ import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ProgressMonitor;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
@@ -235,7 +236,7 @@ public class CloneCommand extends TransportCommand<CloneCommand, Git> {
}
if (head == null || head.getObjectId() == null)
- return; // throw exception?
+ return; // TODO throw exception?
if (head.getName().startsWith(Constants.R_HEADS)) {
final RefUpdate newHead = clonedRepo.updateRef(Constants.HEAD);
@@ -287,20 +288,24 @@ public class CloneCommand extends TransportCommand<CloneCommand, Git> {
private Ref findBranchToCheckout(FetchResult result) {
final Ref idHEAD = result.getAdvertisedRef(Constants.HEAD);
- if (idHEAD == null)
+ ObjectId headId = idHEAD != null ? idHEAD.getObjectId() : null;
+ if (headId == null) {
return null;
+ }
Ref master = result.getAdvertisedRef(Constants.R_HEADS
+ Constants.MASTER);
- if (master != null && master.getObjectId().equals(idHEAD.getObjectId()))
+ ObjectId objectId = master != null ? master.getObjectId() : null;
+ if (headId.equals(objectId)) {
return master;
+ }
Ref foundBranch = null;
for (final Ref r : result.getAdvertisedRefs()) {
final String n = r.getName();
if (!n.startsWith(Constants.R_HEADS))
continue;
- if (r.getObjectId().equals(idHEAD.getObjectId())) {
+ if (headId.equals(r.getObjectId())) {
foundBranch = r;
break;
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
index 6828ed338f..b5057ad282 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
@@ -53,6 +53,7 @@ import java.util.List;
import org.eclipse.jgit.api.errors.AbortedByHookException;
import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
+import org.eclipse.jgit.api.errors.EmtpyCommitException;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.JGitInternalException;
import org.eclipse.jgit.api.errors.NoFilepatternException;
@@ -130,6 +131,8 @@ public class CommitCommand extends GitCommand<RevCommit> {
private PrintStream hookOutRedirect;
+ private Boolean allowEmpty;
+
/**
* @param repo
*/
@@ -231,6 +234,16 @@ public class CommitCommand extends GitCommand<RevCommit> {
if (insertChangeId)
insertChangeId(indexTreeId);
+ // Check for empty commits
+ if (headId != null && !allowEmpty.booleanValue()) {
+ RevCommit headCommit = rw.parseCommit(headId);
+ headCommit.getTree();
+ if (indexTreeId.equals(headCommit.getTree())) {
+ throw new EmtpyCommitException(
+ JGitText.get().emptyCommit);
+ }
+ }
+
// Create a Commit object, populate it and write it
CommitBuilder commit = new CommitBuilder();
commit.setCommitter(committer);
@@ -457,6 +470,8 @@ public class CommitCommand extends GitCommand<RevCommit> {
// there must be at least one change
if (emptyCommit)
+ // Would like to throw a EmptyCommitException. But this would break the API
+ // TODO(ch): Change this in the next release
throw new JGitInternalException(JGitText.get().emptyCommit);
// update index
@@ -510,6 +525,12 @@ public class CommitCommand extends GitCommand<RevCommit> {
committer = new PersonIdent(repo);
if (author == null && !amend)
author = committer;
+ if (allowEmpty == null)
+ // JGit allows empty commits by default. Only when pathes are
+ // specified the commit should not be empty. This behaviour differs
+ // from native git but can only be adapted in the next release.
+ // TODO(ch) align the defaults with native git
+ allowEmpty = (only.isEmpty()) ? Boolean.TRUE : Boolean.FALSE;
// when doing a merge commit parse MERGE_HEAD and MERGE_MSG files
if (state == RepositoryState.MERGING_RESOLVED
@@ -579,6 +600,27 @@ public class CommitCommand extends GitCommand<RevCommit> {
}
/**
+ * @param allowEmpty
+ * whether it should be allowed to create a commit which has the
+ * same tree as it's sole predecessor (a commit which doesn't
+ * change anything). By default when creating standard commits
+ * (without specifying paths) JGit allows to create such commits.
+ * When this flag is set to false an attempt to create an "empty"
+ * standard commit will lead to an EmptyCommitException.
+ * <p>
+ * By default when creating a commit containing only specified
+ * paths an attempt to create an empty commit leads to a
+ * {@link JGitInternalException}. By setting this flag to
+ * <code>true</code> this exception will not be thrown.
+ * @return {@code this}
+ * @since 4.2
+ */
+ public CommitCommand setAllowEmpty(boolean allowEmpty) {
+ this.allowEmpty = Boolean.valueOf(allowEmpty);
+ return this;
+ }
+
+ /**
* @return the commit message used for the <code>commit</code>
*/
public String getMessage() {
@@ -681,7 +723,7 @@ public class CommitCommand extends GitCommand<RevCommit> {
*/
public CommitCommand setAll(boolean all) {
checkCallable();
- if (!only.isEmpty())
+ if (all && !only.isEmpty())
throw new JGitInternalException(MessageFormat.format(
JGitText.get().illegalCombinationOfArguments, "--all", //$NON-NLS-1$
"--only")); //$NON-NLS-1$
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java
index 9620089b08..de512761a4 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java
@@ -116,22 +116,17 @@ public class FetchCommand extends TransportCommand<FetchCommand, FetchResult> {
org.eclipse.jgit.api.errors.TransportException {
checkCallable();
- try {
- Transport transport = Transport.open(repo, remote);
- try {
- transport.setCheckFetchedObjects(checkFetchedObjects);
- transport.setRemoveDeletedRefs(isRemoveDeletedRefs());
- transport.setDryRun(dryRun);
- if (tagOption != null)
- transport.setTagOpt(tagOption);
- transport.setFetchThin(thin);
- configure(transport);
-
- FetchResult result = transport.fetch(monitor, refSpecs);
- return result;
- } finally {
- transport.close();
- }
+ try (Transport transport = Transport.open(repo, remote)) {
+ transport.setCheckFetchedObjects(checkFetchedObjects);
+ transport.setRemoveDeletedRefs(isRemoveDeletedRefs());
+ transport.setDryRun(dryRun);
+ if (tagOption != null)
+ transport.setTagOpt(tagOption);
+ transport.setFetchThin(thin);
+ configure(transport);
+
+ FetchResult result = transport.fetch(monitor, refSpecs);
+ return result;
} catch (NoRemoteRepositoryException e) {
throw new InvalidRemoteException(MessageFormat.format(
JGitText.get().invalidRemote, remote), e);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/LsRemoteCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/LsRemoteCommand.java
index 3363a0fc8f..f3527fd805 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/LsRemoteCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/LsRemoteCommand.java
@@ -182,13 +182,9 @@ public class LsRemoteCommand extends
org.eclipse.jgit.api.errors.TransportException {
checkCallable();
- Transport transport = null;
- FetchConnection fc = null;
- try {
- if (repo != null)
- transport = Transport.open(repo, remote);
- else
- transport = Transport.open(new URIish(remote));
+ try (Transport transport = repo != null
+ ? Transport.open(repo, remote)
+ : Transport.open(new URIish(remote))) {
transport.setOptionUploadPack(uploadPack);
configure(transport);
Collection<RefSpec> refSpecs = new ArrayList<RefSpec>(1);
@@ -199,19 +195,20 @@ public class LsRemoteCommand extends
refSpecs.add(new RefSpec("refs/heads/*:refs/remotes/origin/*")); //$NON-NLS-1$
Collection<Ref> refs;
Map<String, Ref> refmap = new HashMap<String, Ref>();
- fc = transport.openFetch();
- refs = fc.getRefs();
- if (refSpecs.isEmpty())
- for (Ref r : refs)
- refmap.put(r.getName(), r);
- else
- for (Ref r : refs)
- for (RefSpec rs : refSpecs)
- if (rs.matchSource(r)) {
- refmap.put(r.getName(), r);
- break;
- }
- return refmap;
+ try (FetchConnection fc = transport.openFetch()) {
+ refs = fc.getRefs();
+ if (refSpecs.isEmpty())
+ for (Ref r : refs)
+ refmap.put(r.getName(), r);
+ else
+ for (Ref r : refs)
+ for (RefSpec rs : refSpecs)
+ if (rs.matchSource(r)) {
+ refmap.put(r.getName(), r);
+ break;
+ }
+ return refmap;
+ }
} catch (URISyntaxException e) {
throw new InvalidRemoteException(MessageFormat.format(
JGitText.get().invalidRemote, remote));
@@ -223,11 +220,6 @@ public class LsRemoteCommand extends
throw new org.eclipse.jgit.api.errors.TransportException(
e.getMessage(),
e);
- } finally {
- if (fc != null)
- fc.close();
- if (transport != null)
- transport.close();
}
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java
index d2075a70f2..bfe90a3a4f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java
@@ -1,6 +1,7 @@
/*
* Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com>
* Copyright (C) 2010-2014, Stefan Lay <stefan.lay@sap.com>
+ * Copyright (C) 2016, Laurent Delaigue <laurent.delaigue@obeo.fr>
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
@@ -65,8 +66,10 @@ import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.Config.ConfigEnum;
import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.ProgressMonitor;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Ref.Storage;
import org.eclipse.jgit.lib.RefUpdate;
@@ -106,6 +109,8 @@ public class MergeCommand extends GitCommand<MergeResult> {
private String message;
+ private ProgressMonitor monitor = NullProgressMonitor.INSTANCE;
+
/**
* The modes available for fast forward merges corresponding to the
* <code>--ff</code>, <code>--no-ff</code> and <code>--ff-only</code>
@@ -330,6 +335,7 @@ public class MergeCommand extends GitCommand<MergeResult> {
repo.writeSquashCommitMsg(squashMessage);
}
Merger merger = mergeStrategy.newMerger(repo);
+ merger.setProgressMonitor(monitor);
boolean noProblems;
Map<String, org.eclipse.jgit.merge.MergeResult<?>> lowLevelResults = null;
Map<String, MergeFailureReason> failingPaths = null;
@@ -586,4 +592,23 @@ public class MergeCommand extends GitCommand<MergeResult> {
this.message = message;
return this;
}
+
+ /**
+ * The progress monitor associated with the diff operation. By default, this
+ * is set to <code>NullProgressMonitor</code>
+ *
+ * @see NullProgressMonitor
+ *
+ * @param monitor
+ * A progress monitor
+ * @return this instance
+ * @since 4.2
+ */
+ public MergeCommand setProgressMonitor(ProgressMonitor monitor) {
+ if (monitor == null) {
+ monitor = NullProgressMonitor.INSTANCE;
+ }
+ this.monitor = monitor;
+ return this;
+ }
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java
index 2783edd5d8..549ef6cf13 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java
@@ -1,6 +1,7 @@
/*
* Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com>
* Copyright (C) 2010, Mathias Kinzler <mathias.kinzler@sap.com>
+ * Copyright (C) 2016, Laurent Delaigue <laurent.delaigue@obeo.fr>
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
@@ -326,6 +327,7 @@ public class PullCommand extends TransportCommand<PullCommand, PullResult> {
MergeCommand merge = new MergeCommand(repo);
merge.include(upstreamName, commitToMerge);
merge.setStrategy(strategy);
+ merge.setProgressMonitor(monitor);
MergeResult mergeRes = merge.call();
monitor.update(1);
result = new PullResult(fetchRes, remote, mergeRes);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
index 8582bbb0dc..643ec7a51f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
@@ -1,5 +1,6 @@
/*
* Copyright (C) 2010, 2013 Mathias Kinzler <mathias.kinzler@sap.com>
+ * Copyright (C) 2016, Laurent Delaigue <laurent.delaigue@obeo.fr>
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
@@ -560,6 +561,8 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
lastStepWasForward = newHead != null;
if (!lastStepWasForward) {
ObjectId headId = getHead().getObjectId();
+ // getHead() checks for null
+ assert headId != null;
if (!AnyObjectId.equals(headId, newParents.get(0)))
checkoutCommit(headId.getName(), newParents.get(0));
@@ -609,6 +612,7 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
// their non-first parents rewritten
MergeCommand merge = git.merge()
.setFastForward(MergeCommand.FastForwardMode.NO_FF)
+ .setProgressMonitor(monitor)
.setCommit(false);
for (int i = 1; i < commitToPick.getParentCount(); i++)
merge.include(newParents.get(i));
@@ -674,6 +678,8 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
return;
ObjectId headId = getHead().getObjectId();
+ // getHead() checks for null
+ assert headId != null;
String head = headId.getName();
String currentCommits = rebaseState.readFile(CURRENT_COMMIT);
for (String current : currentCommits.split("\n")) //$NON-NLS-1$
@@ -1073,11 +1079,12 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
Ref head = getHead();
- String headName = getHeadName(head);
ObjectId headId = head.getObjectId();
- if (headId == null)
+ if (headId == null) {
throw new RefNotFoundException(MessageFormat.format(
JGitText.get().refNotResolved, Constants.HEAD));
+ }
+ String headName = getHeadName(head);
RevCommit headCommit = walk.lookupCommit(headId);
RevCommit upstream = walk.lookupCommit(upstreamCommit.getId());
@@ -1188,10 +1195,14 @@ public class RebaseCommand extends GitCommand<RebaseResult> {
private static String getHeadName(Ref head) {
String headName;
- if (head.isSymbolic())
+ if (head.isSymbolic()) {
headName = head.getTarget().getName();
- else
- headName = head.getObjectId().getName();
+ } else {
+ ObjectId headId = head.getObjectId();
+ // the callers are checking this already
+ assert headId != null;
+ headName = headId.getName();
+ }
return headName;
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java
index 8f4bc4f26c..4c91e6c17f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java
@@ -190,10 +190,8 @@ public class ResetCommand extends GitCommand<Ref> {
ObjectId origHead = ru.getOldObjectId();
if (origHead != null)
repo.writeOrigHead(origHead);
- result = ru.getRef();
- } else {
- result = repo.getRef(Constants.HEAD);
}
+ result = repo.exactRef(Constants.HEAD);
if (mode == null)
mode = ResetType.MIXED;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashDropCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashDropCommand.java
index 7923fd49be..f6903be05c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashDropCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashDropCommand.java
@@ -46,6 +46,7 @@ import static org.eclipse.jgit.lib.Constants.R_STASH;
import java.io.File;
import java.io.IOException;
+import java.nio.file.StandardCopyOption;
import java.text.MessageFormat;
import java.util.List;
@@ -220,12 +221,14 @@ public class StashDropCommand extends GitCommand<ObjectId> {
entry.getWho(), entry.getComment());
entryId = entry.getNewId();
}
- if (!stashLockFile.renameTo(stashFile)) {
- FileUtils.delete(stashFile);
- if (!stashLockFile.renameTo(stashFile))
+ try {
+ FileUtils.rename(stashLockFile, stashFile,
+ StandardCopyOption.ATOMIC_MOVE);
+ } catch (IOException e) {
throw new JGitInternalException(MessageFormat.format(
JGitText.get().renameFileFailed,
- stashLockFile.getPath(), stashFile.getPath()));
+ stashLockFile.getPath(), stashFile.getPath()),
+ e);
}
} catch (IOException e) {
throw new JGitInternalException(JGitText.get().stashDropFailed, e);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleUpdateCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleUpdateCommand.java
index e288d7755b..342d7f42f3 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleUpdateCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleUpdateCommand.java
@@ -1,5 +1,6 @@
/*
* Copyright (C) 2011, GitHub Inc.
+ * Copyright (C) 2016, Laurent Delaigue <laurent.delaigue@obeo.fr>
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
@@ -178,11 +179,13 @@ public class SubmoduleUpdateCommand extends
if (ConfigConstants.CONFIG_KEY_MERGE.equals(update)) {
MergeCommand merge = new MergeCommand(submoduleRepo);
merge.include(commit);
+ merge.setProgressMonitor(monitor);
merge.setStrategy(strategy);
merge.call();
} else if (ConfigConstants.CONFIG_KEY_REBASE.equals(update)) {
RebaseCommand rebase = new RebaseCommand(submoduleRepo);
rebase.setUpstream(commit);
+ rebase.setProgressMonitor(monitor);
rebase.setStrategy(strategy);
rebase.call();
} else {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/TransportCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/TransportCommand.java
index 1aeb6109ec..3d2e46b26e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/TransportCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/TransportCommand.java
@@ -79,6 +79,7 @@ public abstract class TransportCommand<C extends GitCommand, T> extends
*/
protected TransportCommand(final Repository repo) {
super(repo);
+ setCredentialsProvider(CredentialsProvider.getDefault());
}
/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/EmtpyCommitException.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/EmtpyCommitException.java
new file mode 100644
index 0000000000..b3cc1bfcf2
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/EmtpyCommitException.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2015, Christian Halstrick <christian.halstrick@sap.com>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v1.0 which accompanies this
+ * distribution, is reproduced below, and is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from this
+ * software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.api.errors;
+
+/**
+ * Exception thrown when a newly created commit does not contain any changes
+ *
+ * @since 4.2
+ */
+public class EmtpyCommitException extends GitAPIException {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * @param message
+ * @param cause
+ */
+ public EmtpyCommitException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * @param message
+ */
+ public EmtpyCommitException(String message) {
+ super(message);
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/BaseDirCacheEditor.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/BaseDirCacheEditor.java
index 70f80aeb7a..0fbc1f8acf 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/BaseDirCacheEditor.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/BaseDirCacheEditor.java
@@ -44,8 +44,13 @@
package org.eclipse.jgit.dircache;
+import static org.eclipse.jgit.lib.FileMode.TYPE_TREE;
+import static org.eclipse.jgit.util.Paths.compareSameName;
+
import java.io.IOException;
+import org.eclipse.jgit.errors.DirCacheNameConflictException;
+
/**
* Generic update/editing support for {@link DirCache}.
* <p>
@@ -168,6 +173,7 @@ abstract class BaseDirCacheEditor {
* {@link #finish()}, and only after {@link #entries} is sorted.
*/
protected void replace() {
+ checkNameConflicts();
if (entryCnt < entries.length / 2) {
final DirCacheEntry[] n = new DirCacheEntry[entryCnt];
System.arraycopy(entries, 0, n, 0, entryCnt);
@@ -176,6 +182,76 @@ abstract class BaseDirCacheEditor {
cache.replace(entries, entryCnt);
}
+ private void checkNameConflicts() {
+ int end = entryCnt - 1;
+ for (int eIdx = 0; eIdx < end; eIdx++) {
+ DirCacheEntry e = entries[eIdx];
+ if (e.getStage() != 0) {
+ continue;
+ }
+
+ byte[] ePath = e.path;
+ int prefixLen = lastSlash(ePath) + 1;
+
+ for (int nIdx = eIdx + 1; nIdx < entryCnt; nIdx++) {
+ DirCacheEntry n = entries[nIdx];
+ if (n.getStage() != 0) {
+ continue;
+ }
+
+ byte[] nPath = n.path;
+ if (!startsWith(ePath, nPath, prefixLen)) {
+ // Different prefix; this entry is in another directory.
+ break;
+ }
+
+ int s = nextSlash(nPath, prefixLen);
+ int m = s < nPath.length ? TYPE_TREE : n.getRawMode();
+ int cmp = compareSameName(
+ ePath, prefixLen, ePath.length,
+ nPath, prefixLen, s, m);
+ if (cmp < 0) {
+ break;
+ } else if (cmp == 0) {
+ throw new DirCacheNameConflictException(
+ e.getPathString(),
+ n.getPathString());
+ }
+ }
+ }
+ }
+
+ private static int lastSlash(byte[] path) {
+ for (int i = path.length - 1; i >= 0; i--) {
+ if (path[i] == '/') {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private static int nextSlash(byte[] b, int p) {
+ final int n = b.length;
+ for (; p < n; p++) {
+ if (b[p] == '/') {
+ return p;
+ }
+ }
+ return n;
+ }
+
+ private static boolean startsWith(byte[] a, byte[] b, int n) {
+ if (b.length < n) {
+ return false;
+ }
+ for (n--; n >= 0; n--) {
+ if (a[n] != b[n]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
/**
* Finish, write, commit this change, and release the index lock.
* <p>
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java
index fa0339544f..ecdfe823a8 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java
@@ -800,8 +800,11 @@ public class DirCache {
* information. If &lt; 0 the entry does not exist in the index.
* @since 3.4
*/
- public int findEntry(final byte[] p, final int pLen) {
- int low = 0;
+ public int findEntry(byte[] p, int pLen) {
+ return findEntry(0, p, pLen);
+ }
+
+ int findEntry(int low, byte[] p, int pLen) {
int high = entryCnt;
while (low < high) {
int mid = (low + high) >>> 1;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheBuildIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheBuildIterator.java
index da55306665..c10e416082 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheBuildIterator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheBuildIterator.java
@@ -130,4 +130,9 @@ public class DirCacheBuildIterator extends DirCacheIterator {
if (cur < cnt)
builder.keep(cur, cnt - cur);
}
+
+ @Override
+ protected boolean needsStopWalk() {
+ return ptr < cache.getEntryCount();
+ }
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
index 4eb688170c..a1e1d15ac6 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
@@ -46,6 +46,7 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
+import java.nio.file.StandardCopyOption;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
@@ -1319,11 +1320,12 @@ public class DirCacheCheckout {
if (deleteRecursive && f.isDirectory()) {
FileUtils.delete(f, FileUtils.RECURSIVE);
}
- FileUtils.rename(tmpFile, f);
+ FileUtils.rename(tmpFile, f, StandardCopyOption.ATOMIC_MOVE);
} catch (IOException e) {
- throw new IOException(MessageFormat.format(
- JGitText.get().renameFileFailed, tmpFile.getPath(),
- f.getPath()));
+ throw new IOException(
+ MessageFormat.format(JGitText.get().renameFileFailed,
+ tmpFile.getPath(), f.getPath()),
+ e);
} finally {
if (tmpFile.exists()) {
FileUtils.delete(tmpFile);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEditor.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEditor.java
index 13885d370c..c987c964c4 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEditor.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEditor.java
@@ -44,6 +44,10 @@
package org.eclipse.jgit.dircache;
+import static org.eclipse.jgit.dircache.DirCache.cmp;
+import static org.eclipse.jgit.dircache.DirCacheTree.peq;
+import static org.eclipse.jgit.lib.FileMode.TYPE_TREE;
+
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
@@ -53,6 +57,7 @@ import java.util.List;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.util.Paths;
/**
* Updates a {@link DirCache} by supplying discrete edit commands.
@@ -72,11 +77,12 @@ public class DirCacheEditor extends BaseDirCacheEditor {
public int compare(final PathEdit o1, final PathEdit o2) {
final byte[] a = o1.path;
final byte[] b = o2.path;
- return DirCache.cmp(a, a.length, b, b.length);
+ return cmp(a, a.length, b, b.length);
}
};
private final List<PathEdit> edits;
+ private int editIdx;
/**
* Construct a new editor.
@@ -126,35 +132,44 @@ public class DirCacheEditor extends BaseDirCacheEditor {
private void applyEdits() {
Collections.sort(edits, EDIT_CMP);
+ editIdx = 0;
final int maxIdx = cache.getEntryCount();
int lastIdx = 0;
- for (final PathEdit e : edits) {
- int eIdx = cache.findEntry(e.path, e.path.length);
+ while (editIdx < edits.size()) {
+ PathEdit e = edits.get(editIdx++);
+ int eIdx = cache.findEntry(lastIdx, e.path, e.path.length);
final boolean missing = eIdx < 0;
if (eIdx < 0)
eIdx = -(eIdx + 1);
final int cnt = Math.min(eIdx, maxIdx) - lastIdx;
if (cnt > 0)
fastKeep(lastIdx, cnt);
- lastIdx = missing ? eIdx : cache.nextEntry(eIdx);
- if (e instanceof DeletePath)
+ if (e instanceof DeletePath) {
+ lastIdx = missing ? eIdx : cache.nextEntry(eIdx);
continue;
+ }
if (e instanceof DeleteTree) {
lastIdx = cache.nextEntry(e.path, e.path.length, eIdx);
continue;
}
if (missing) {
- final DirCacheEntry ent = new DirCacheEntry(e.path);
+ DirCacheEntry ent = new DirCacheEntry(e.path);
e.apply(ent);
- if (ent.getRawMode() == 0)
- throw new IllegalArgumentException(MessageFormat.format(JGitText.get().fileModeNotSetForPath
- , ent.getPathString()));
+ if (ent.getRawMode() == 0) {
+ throw new IllegalArgumentException(MessageFormat.format(
+ JGitText.get().fileModeNotSetForPath,
+ ent.getPathString()));
+ }
+ lastIdx = e.replace
+ ? deleteOverlappingSubtree(ent, eIdx)
+ : eIdx;
fastAdd(ent);
} else {
// Apply to all entries of the current path (different stages)
+ lastIdx = cache.nextEntry(eIdx);
for (int i = eIdx; i < lastIdx; i++) {
final DirCacheEntry ent = cache.getEntry(i);
e.apply(ent);
@@ -168,6 +183,102 @@ public class DirCacheEditor extends BaseDirCacheEditor {
fastKeep(lastIdx, cnt);
}
+ private int deleteOverlappingSubtree(DirCacheEntry ent, int eIdx) {
+ byte[] entPath = ent.path;
+ int entLen = entPath.length;
+
+ // Delete any file that was previously processed and overlaps
+ // the parent directory for the new entry. Since the editor
+ // always processes entries in path order, binary search back
+ // for the overlap for each parent directory.
+ for (int p = pdir(entPath, entLen); p > 0; p = pdir(entPath, p)) {
+ int i = findEntry(entPath, p);
+ if (i >= 0) {
+ // A file does overlap, delete the file from the array.
+ // No other parents can have overlaps as the file should
+ // have taken care of that itself.
+ int n = --entryCnt - i;
+ System.arraycopy(entries, i + 1, entries, i, n);
+ break;
+ }
+
+ // If at least one other entry already exists in this parent
+ // directory there is no need to continue searching up the tree.
+ i = -(i + 1);
+ if (i < entryCnt && inDir(entries[i], entPath, p)) {
+ break;
+ }
+ }
+
+ int maxEnt = cache.getEntryCount();
+ if (eIdx >= maxEnt) {
+ return maxEnt;
+ }
+
+ DirCacheEntry next = cache.getEntry(eIdx);
+ if (Paths.compare(next.path, 0, next.path.length, 0,
+ entPath, 0, entLen, TYPE_TREE) < 0) {
+ // Next DirCacheEntry sorts before new entry as tree. Defer a
+ // DeleteTree command to delete any entries if they exist. This
+ // case only happens for A, A.c, A/c type of conflicts (rare).
+ insertEdit(new DeleteTree(entPath));
+ return eIdx;
+ }
+
+ // Next entry may be contained by the entry-as-tree, skip if so.
+ while (eIdx < maxEnt && inDir(cache.getEntry(eIdx), entPath, entLen)) {
+ eIdx++;
+ }
+ return eIdx;
+ }
+
+ private int findEntry(byte[] p, int pLen) {
+ int low = 0;
+ int high = entryCnt;
+ while (low < high) {
+ int mid = (low + high) >>> 1;
+ int cmp = cmp(p, pLen, entries[mid]);
+ if (cmp < 0) {
+ high = mid;
+ } else if (cmp == 0) {
+ while (mid > 0 && cmp(p, pLen, entries[mid - 1]) == 0) {
+ mid--;
+ }
+ return mid;
+ } else {
+ low = mid + 1;
+ }
+ }
+ return -(low + 1);
+ }
+
+ private void insertEdit(DeleteTree d) {
+ for (int i = editIdx; i < edits.size(); i++) {
+ int cmp = EDIT_CMP.compare(d, edits.get(i));
+ if (cmp < 0) {
+ edits.add(i, d);
+ return;
+ } else if (cmp == 0) {
+ return;
+ }
+ }
+ edits.add(d);
+ }
+
+ private static boolean inDir(DirCacheEntry e, byte[] path, int pLen) {
+ return e.path.length > pLen && e.path[pLen] == '/'
+ && peq(path, e.path, pLen);
+ }
+
+ private static int pdir(byte[] path, int e) {
+ for (e--; e > 0; e--) {
+ if (path[e] == '/') {
+ return e;
+ }
+ }
+ return 0;
+ }
+
/**
* Any index record update.
* <p>
@@ -179,6 +290,7 @@ public class DirCacheEditor extends BaseDirCacheEditor {
*/
public abstract static class PathEdit {
final byte[] path;
+ boolean replace = true;
/**
* Create a new update command by path name.
@@ -190,6 +302,10 @@ public class DirCacheEditor extends BaseDirCacheEditor {
path = Constants.encode(entryPath);
}
+ PathEdit(byte[] path) {
+ this.path = path;
+ }
+
/**
* Create a new update command for an existing entry instance.
*
@@ -202,6 +318,22 @@ public class DirCacheEditor extends BaseDirCacheEditor {
}
/**
+ * Configure if a file can replace a directory (or vice versa).
+ * <p>
+ * Default is {@code true} as this is usually the desired behavior.
+ *
+ * @param ok
+ * if true a file can replace a directory, or a directory can
+ * replace a file.
+ * @return {@code this}
+ * @since 4.2
+ */
+ public PathEdit setReplace(boolean ok) {
+ replace = ok;
+ return this;
+ }
+
+ /**
* Apply the update to a single cache entry matching the path.
* <p>
* After apply is invoked the entry is added to the output table, and
@@ -212,6 +344,12 @@ public class DirCacheEditor extends BaseDirCacheEditor {
* the path is a new path in the index.
*/
public abstract void apply(DirCacheEntry ent);
+
+ @Override
+ public String toString() {
+ String p = DirCacheEntry.toString(path);
+ return getClass().getSimpleName() + '[' + p + ']';
+ }
}
/**
@@ -272,10 +410,26 @@ public class DirCacheEditor extends BaseDirCacheEditor {
* only the subtree's contents are matched by the command.
* The special case "" (not "/"!) deletes all entries.
*/
- public DeleteTree(final String entryPath) {
- super(
- (entryPath.endsWith("/") || entryPath.length() == 0) ? entryPath //$NON-NLS-1$
- : entryPath + "/"); //$NON-NLS-1$
+ public DeleteTree(String entryPath) {
+ super(entryPath.isEmpty()
+ || entryPath.charAt(entryPath.length() - 1) == '/'
+ ? entryPath
+ : entryPath + '/');
+ }
+
+ DeleteTree(byte[] path) {
+ super(appendSlash(path));
+ }
+
+ private static byte[] appendSlash(byte[] path) {
+ int n = path.length;
+ if (n > 0 && path[n - 1] != '/') {
+ byte[] r = new byte[n + 1];
+ System.arraycopy(path, 0, r, 0, n);
+ r[n] = '/';
+ return r;
+ }
+ return path;
}
public void apply(final DirCacheEntry ent) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java
index c8bc0960f4..4ebf2e0d71 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java
@@ -294,6 +294,23 @@ public class DirCacheEntry {
NB.encodeInt16(info, infoOffset + P_FLAGS, flags);
}
+ /**
+ * Duplicate DirCacheEntry with same path and copied info.
+ * <p>
+ * The same path buffer is reused (avoiding copying), however a new info
+ * buffer is created and its contents are copied.
+ *
+ * @param src
+ * entry to clone.
+ * @since 4.2
+ */
+ public DirCacheEntry(DirCacheEntry src) {
+ path = src.path;
+ info = new byte[INFO_LEN];
+ infoOffset = 0;
+ System.arraycopy(src.info, src.infoOffset, info, 0, INFO_LEN);
+ }
+
void write(final OutputStream os) throws IOException {
final int len = isExtended() ? INFO_LEN_EXTENDED : INFO_LEN;
final int pathLen = path.length;
@@ -745,7 +762,7 @@ public class DirCacheEntry {
}
}
- private static String toString(final byte[] path) {
+ static String toString(final byte[] path) {
return Constants.CHARSET.decode(ByteBuffer.wrap(path)).toString();
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/CorruptObjectException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/CorruptObjectException.java
index c6ea093750..e4db40b889 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/errors/CorruptObjectException.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/CorruptObjectException.java
@@ -49,8 +49,10 @@ package org.eclipse.jgit.errors;
import java.io.IOException;
import java.text.MessageFormat;
+import org.eclipse.jgit.annotations.Nullable;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.ObjectChecker;
import org.eclipse.jgit.lib.ObjectId;
/**
@@ -59,6 +61,26 @@ import org.eclipse.jgit.lib.ObjectId;
public class CorruptObjectException extends IOException {
private static final long serialVersionUID = 1L;
+ private ObjectChecker.ErrorType errorType;
+
+ /**
+ * Report a specific error condition discovered in an object.
+ *
+ * @param type
+ * type of error
+ * @param id
+ * identity of the bad object
+ * @param why
+ * description of the error.
+ * @since 4.2
+ */
+ public CorruptObjectException(ObjectChecker.ErrorType type, AnyObjectId id,
+ String why) {
+ super(MessageFormat.format(JGitText.get().objectIsCorrupt3,
+ type.getMessageId(), id.name(), why));
+ this.errorType = type;
+ }
+
/**
* Construct a CorruptObjectException for reporting a problem specified
* object id
@@ -66,8 +88,8 @@ public class CorruptObjectException extends IOException {
* @param id
* @param why
*/
- public CorruptObjectException(final AnyObjectId id, final String why) {
- this(id.toObjectId(), why);
+ public CorruptObjectException(AnyObjectId id, String why) {
+ super(MessageFormat.format(JGitText.get().objectIsCorrupt, id.name(), why));
}
/**
@@ -77,7 +99,7 @@ public class CorruptObjectException extends IOException {
* @param id
* @param why
*/
- public CorruptObjectException(final ObjectId id, final String why) {
+ public CorruptObjectException(ObjectId id, String why) {
super(MessageFormat.format(JGitText.get().objectIsCorrupt, id.name(), why));
}
@@ -87,7 +109,7 @@ public class CorruptObjectException extends IOException {
*
* @param why
*/
- public CorruptObjectException(final String why) {
+ public CorruptObjectException(String why) {
super(why);
}
@@ -105,4 +127,15 @@ public class CorruptObjectException extends IOException {
super(why);
initCause(cause);
}
+
+ /**
+ * Specific error condition identified by {@link ObjectChecker}.
+ *
+ * @return error condition or null.
+ * @since 4.2
+ */
+ @Nullable
+ public ObjectChecker.ErrorType getErrorType() {
+ return errorType;
+ }
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GitlinkTreeEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/DirCacheNameConflictException.java
index 936fd82bfd..5f67e3439b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GitlinkTreeEntry.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/DirCacheNameConflictException.java
@@ -1,7 +1,5 @@
/*
- * Copyright (C) 2009, Jonas Fonseca <fonseca@diku.dk>
- * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2007, Shawn O. Pearce <spearce@spearce.org>
+ * Copyright (C) 2015, Google Inc.
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
@@ -43,45 +41,40 @@
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package org.eclipse.jgit.lib;
+package org.eclipse.jgit.errors;
/**
- * A tree entry representing a gitlink entry used for submodules.
+ * Thrown by DirCache code when entries overlap in impossible way.
*
- * Note. Java cannot really handle these as file system objects.
- *
- * @deprecated To look up information about a single path, use
- * {@link org.eclipse.jgit.treewalk.TreeWalk#forPath(Repository, String, org.eclipse.jgit.revwalk.RevTree)}.
- * To lookup information about multiple paths at once, use a
- * {@link org.eclipse.jgit.treewalk.TreeWalk} and obtain the current entry's
- * information from its getter methods.
+ * @since 4.2
*/
-@Deprecated
-public class GitlinkTreeEntry extends TreeEntry {
+public class DirCacheNameConflictException extends IllegalStateException {
+ private static final long serialVersionUID = 1L;
+
+ private final String path1;
+ private final String path2;
/**
- * Construct a {@link GitlinkTreeEntry} with the specified name and SHA-1 in
- * the specified parent
+ * Construct an exception for a specific path.
*
- * @param parent
- * @param id
- * @param nameUTF8
+ * @param path1
+ * one path that conflicts.
+ * @param path2
+ * another path that conflicts.
*/
- public GitlinkTreeEntry(final Tree parent, final ObjectId id,
- final byte[] nameUTF8) {
- super(parent, id, nameUTF8);
+ public DirCacheNameConflictException(String path1, String path2) {
+ super(path1 + ' ' + path2);
+ this.path1 = path1;
+ this.path2 = path2;
}
- public FileMode getMode() {
- return FileMode.GITLINK;
+ /** @return one of the paths that has a conflict. */
+ public String getPath1() {
+ return path1;
}
- @Override
- public String toString() {
- final StringBuilder r = new StringBuilder();
- r.append(ObjectId.toString(getId()));
- r.append(" G "); //$NON-NLS-1$
- r.append(getFullName());
- return r.toString();
+ /** @return another path that has a conflict. */
+ public String getPath2() {
+ return path2;
}
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java
index 891479d1f4..7eb955006e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java
@@ -338,6 +338,20 @@ public class ManifestParser extends DefaultHandler {
else
last = p;
}
+ removeNestedCopyfiles();
+ }
+
+ /** Remove copyfiles that sit in a subdirectory of any other project. */
+ void removeNestedCopyfiles() {
+ for (RepoProject proj : filteredProjects) {
+ List<CopyFile> copyfiles = new ArrayList<>(proj.getCopyFiles());
+ proj.clearCopyFiles();
+ for (CopyFile copyfile : copyfiles) {
+ if (!isNestedCopyfile(copyfile)) {
+ proj.addCopyFile(copyfile);
+ }
+ }
+ }
}
boolean inGroups(RepoProject proj) {
@@ -357,4 +371,22 @@ public class ManifestParser extends DefaultHandler {
}
return false;
}
+
+ private boolean isNestedCopyfile(CopyFile copyfile) {
+ if (copyfile.dest.indexOf('/') == -1) {
+ // If the copyfile is at root level then it won't be nested.
+ return false;
+ }
+ for (RepoProject proj : filteredProjects) {
+ if (proj.getPath().compareTo(copyfile.dest) > 0) {
+ // Early return as remaining projects can't be ancestor of this
+ // copyfile config (filteredProjects is sorted).
+ return false;
+ }
+ if (proj.isAncestorOf(copyfile.dest)) {
+ return true;
+ }
+ }
+ return false;
+ }
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java
index 9a072114a7..915066d58f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java
@@ -258,6 +258,15 @@ public class RepoProject implements Comparable<RepoProject> {
this.copyfiles.addAll(copyfiles);
}
+ /**
+ * Clear all the copyfiles.
+ *
+ * @since 4.2
+ */
+ public void clearCopyFiles() {
+ this.copyfiles.clear();
+ }
+
private String getPathWithSlash() {
if (path.endsWith("/")) //$NON-NLS-1$
return path;
@@ -273,7 +282,19 @@ public class RepoProject implements Comparable<RepoProject> {
* @return true if this sub repo is the ancestor of given sub repo.
*/
public boolean isAncestorOf(RepoProject that) {
- return that.getPathWithSlash().startsWith(this.getPathWithSlash());
+ return isAncestorOf(that.getPathWithSlash());
+ }
+
+ /**
+ * Check if this sub repo is an ancestor of the given path.
+ *
+ * @param path
+ * path to be checked to see if it is within this repository
+ * @return true if this sub repo is an ancestor of the given path.
+ * @since 4.2
+ */
+ public boolean isAncestorOf(String path) {
+ return path.startsWith(getPathWithSlash());
}
@Override
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
index 796eaaebf5..7740a2bb80 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
@@ -158,6 +158,7 @@ public class JGitText extends TranslationBundle {
/***/ public String cannotStoreObjects;
/***/ public String cannotResolveUniquelyAbbrevObjectId;
/***/ public String cannotUnloadAModifiedTree;
+ /***/ public String cannotUpdateUnbornBranch;
/***/ public String cannotWorkWithOtherStagesThanZeroRightNow;
/***/ public String cannotWriteObjectsPath;
/***/ public String canOnlyCherryPickCommitsWithOneParent;
@@ -184,14 +185,15 @@ public class JGitText extends TranslationBundle {
/***/ public String connectionTimeOut;
/***/ public String contextMustBeNonNegative;
/***/ public String corruptionDetectedReReadingAt;
+ /***/ public String corruptObjectBadDate;
+ /***/ public String corruptObjectBadEmail;
/***/ public String corruptObjectBadStream;
/***/ public String corruptObjectBadStreamCorruptHeader;
+ /***/ public String corruptObjectBadTimezone;
/***/ public String corruptObjectDuplicateEntryNames;
/***/ public String corruptObjectGarbageAfterSize;
/***/ public String corruptObjectIncorrectLength;
/***/ public String corruptObjectIncorrectSorting;
- /***/ public String corruptObjectInvalidAuthor;
- /***/ public String corruptObjectInvalidCommitter;
/***/ public String corruptObjectInvalidEntryMode;
/***/ public String corruptObjectInvalidMode;
/***/ public String corruptObjectInvalidModeChar;
@@ -210,11 +212,11 @@ public class JGitText extends TranslationBundle {
/***/ public String corruptObjectInvalidNamePrn;
/***/ public String corruptObjectInvalidObject;
/***/ public String corruptObjectInvalidParent;
- /***/ public String corruptObjectInvalidTagger;
/***/ public String corruptObjectInvalidTree;
/***/ public String corruptObjectInvalidType;
/***/ public String corruptObjectInvalidType2;
/***/ public String corruptObjectMalformedHeader;
+ /***/ public String corruptObjectMissingEmail;
/***/ public String corruptObjectNameContainsByte;
/***/ public String corruptObjectNameContainsChar;
/***/ public String corruptObjectNameContainsNullByte;
@@ -240,6 +242,7 @@ public class JGitText extends TranslationBundle {
/***/ public String corruptObjectTruncatedInMode;
/***/ public String corruptObjectTruncatedInName;
/***/ public String corruptObjectTruncatedInObjectId;
+ /***/ public String corruptObjectZeroId;
/***/ public String corruptPack;
/***/ public String couldNotCheckOutBecauseOfConflicts;
/***/ public String couldNotDeleteLockFileShouldNotHappen;
@@ -491,6 +494,7 @@ public class JGitText extends TranslationBundle {
/***/ public String objectAtHasBadZlibStream;
/***/ public String objectAtPathDoesNotHaveId;
/***/ public String objectIsCorrupt;
+ /***/ public String objectIsCorrupt3;
/***/ public String objectIsNotA;
/***/ public String objectNotFound;
/***/ public String objectNotFoundIn;
@@ -663,6 +667,7 @@ public class JGitText extends TranslationBundle {
/***/ public String transportProtoSFTP;
/***/ public String transportProtoSSH;
/***/ public String transportProtoTest;
+ /***/ public String transportProvidedRefWithNoObjectId;
/***/ public String transportSSHRetryInterrupt;
/***/ public String treeEntryAlreadyExists;
/***/ public String treeFilterMarkerTooManyFilters;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ElectionRound.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ElectionRound.java
new file mode 100644
index 0000000000..014eab2b45
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ElectionRound.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import static org.eclipse.jgit.internal.ketch.KetchConstants.TERM;
+
+import java.io.IOException;
+import java.util.List;
+
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.TreeFormatter;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The initial {@link Round} for a leaderless repository, used to establish a
+ * leader.
+ */
+class ElectionRound extends Round {
+ private static final Logger log = LoggerFactory.getLogger(ElectionRound.class);
+
+ private long term;
+
+ ElectionRound(KetchLeader leader, LogIndex head) {
+ super(leader, head);
+ }
+
+ @Override
+ void start() throws IOException {
+ ObjectId id;
+ try (Repository git = leader.openRepository();
+ ObjectInserter inserter = git.newObjectInserter()) {
+ id = bumpTerm(git, inserter);
+ inserter.flush();
+ }
+ runAsync(id);
+ }
+
+ @Override
+ void success() {
+ // Do nothing upon election, KetchLeader will copy the term.
+ }
+
+ long getTerm() {
+ return term;
+ }
+
+ private ObjectId bumpTerm(Repository git, ObjectInserter inserter)
+ throws IOException {
+ CommitBuilder b = new CommitBuilder();
+ if (!ObjectId.zeroId().equals(acceptedOldIndex)) {
+ try (RevWalk rw = new RevWalk(git)) {
+ RevCommit c = rw.parseCommit(acceptedOldIndex);
+ b.setTreeId(c.getTree());
+ b.setParentId(acceptedOldIndex);
+ term = parseTerm(c.getFooterLines(TERM)) + 1;
+ }
+ } else {
+ term = 1;
+ b.setTreeId(inserter.insert(new TreeFormatter()));
+ }
+
+ StringBuilder msg = new StringBuilder();
+ msg.append(KetchConstants.TERM.getName())
+ .append(": ") //$NON-NLS-1$
+ .append(term);
+
+ String tag = leader.getSystem().newLeaderTag();
+ if (tag != null && !tag.isEmpty()) {
+ msg.append(' ').append(tag);
+ }
+
+ b.setAuthor(leader.getSystem().newCommitter());
+ b.setCommitter(b.getAuthor());
+ b.setMessage(msg.toString());
+
+ if (log.isDebugEnabled()) {
+ log.debug("Trying to elect myself " + b.getMessage()); //$NON-NLS-1$
+ }
+ return inserter.insert(b);
+ }
+
+ private static long parseTerm(List<String> footer) {
+ if (footer.isEmpty()) {
+ return 0;
+ }
+
+ String s = footer.get(0);
+ int p = s.indexOf(' ');
+ if (p > 0) {
+ s = s.substring(0, p);
+ }
+ return Long.parseLong(s, 10);
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchConstants.java
new file mode 100644
index 0000000000..171c059db1
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchConstants.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import org.eclipse.jgit.revwalk.FooterKey;
+
+/** Frequently used constants in a Ketch system. */
+public class KetchConstants {
+ /**
+ * Default reference namespace holding {@link #ACCEPTED} and
+ * {@link #COMMITTED} references and the {@link #STAGE} sub-namespace.
+ */
+ public static final String DEFAULT_TXN_NAMESPACE = "refs/txn/"; //$NON-NLS-1$
+
+ /** Reference name holding the RefTree accepted by a follower. */
+ public static final String ACCEPTED = "accepted"; //$NON-NLS-1$
+
+ /** Reference name holding the RefTree known to be committed. */
+ public static final String COMMITTED = "committed"; //$NON-NLS-1$
+
+ /** Reference subdirectory holding proposed heads. */
+ public static final String STAGE = "stage/"; //$NON-NLS-1$
+
+ /** Footer containing the current term. */
+ public static final FooterKey TERM = new FooterKey("Term"); //$NON-NLS-1$
+
+ /** Section for Ketch configuration ({@code ketch}). */
+ public static final String CONFIG_SECTION_KETCH = "ketch"; //$NON-NLS-1$
+
+ /** Behavior for a replica ({@code remote.$name.ketch-type}) */
+ public static final String CONFIG_KEY_TYPE = "ketch-type"; //$NON-NLS-1$
+
+ /** Behavior for a replica ({@code remote.$name.ketch-commit}) */
+ public static final String CONFIG_KEY_COMMIT = "ketch-commit"; //$NON-NLS-1$
+
+ /** Behavior for a replica ({@code remote.$name.ketch-speed}) */
+ public static final String CONFIG_KEY_SPEED = "ketch-speed"; //$NON-NLS-1$
+
+ private KetchConstants() {
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchLeader.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchLeader.java
new file mode 100644
index 0000000000..3bcd6bcb24
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchLeader.java
@@ -0,0 +1,624 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import static org.eclipse.jgit.internal.ketch.KetchLeader.State.CANDIDATE;
+import static org.eclipse.jgit.internal.ketch.KetchLeader.State.LEADER;
+import static org.eclipse.jgit.internal.ketch.KetchLeader.State.SHUTDOWN;
+import static org.eclipse.jgit.internal.ketch.KetchReplica.Participation.FOLLOWER_ONLY;
+import static org.eclipse.jgit.internal.ketch.Proposal.State.QUEUED;
+
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+import org.eclipse.jgit.internal.storage.reftree.RefTree;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A leader managing consensus across remote followers.
+ * <p>
+ * A leader instance starts up in {@link State#CANDIDATE} and tries to begin a
+ * new term by sending an {@link ElectionRound} to all replicas. Its term starts
+ * if a majority of replicas have accepted this leader instance for the term.
+ * <p>
+ * Once elected by a majority the instance enters {@link State#LEADER} and runs
+ * proposals offered to {@link #queueProposal(Proposal)}. This continues until
+ * the leader is timed out for inactivity, or is deposed by a competing leader
+ * gaining its own majority.
+ * <p>
+ * Once timed out or deposed this {@code KetchLeader} instance should be
+ * discarded, and a new instance takes over.
+ * <p>
+ * Each leader instance coordinates a group of {@link KetchReplica}s. Replica
+ * instances are owned by the leader instance and must be discarded when the
+ * leader is discarded.
+ * <p>
+ * In Ketch all push requests are issued through the leader. The steps are as
+ * follows (see {@link KetchPreReceive} for an example):
+ * <ul>
+ * <li>Create a {@link Proposal} with the
+ * {@link org.eclipse.jgit.transport.ReceiveCommand}s that represent the push.
+ * <li>Invoke {@link #queueProposal(Proposal)} on the leader instance.
+ * <li>Wait for consensus with {@link Proposal#await()}.
+ * <li>To examine the status of the push, check {@link Proposal#getCommands()},
+ * looking at
+ * {@link org.eclipse.jgit.internal.storage.reftree.Command#getResult()}.
+ * </ul>
+ * <p>
+ * The leader gains consensus by first pushing the needed objects and a
+ * {@link RefTree} representing the desired target repository state to the
+ * {@code refs/txn/accepted} branch on each of the replicas. Once a majority has
+ * succeeded, the leader commits the state by either pushing the
+ * {@code refs/txn/accepted} value to {@code refs/txn/committed} (for
+ * Ketch-aware replicas) or by pushing updates to {@code refs/heads/master},
+ * etc. for stock Git replicas.
+ * <p>
+ * Internally, the actual transport to replicas is performed on background
+ * threads via the {@link KetchSystem}'s executor service. For performance, the
+ * {@link KetchLeader}, {@link KetchReplica} and {@link Proposal} objects share
+ * some state, and may invoke each other's methods on different threads. This
+ * access is protected by the leader's {@link #lock} object. Care must be taken
+ * to prevent concurrent access by correctly obtaining the leader's lock.
+ */
+public abstract class KetchLeader {
+ private static final Logger log = LoggerFactory.getLogger(KetchLeader.class);
+
+ /** Current state of the leader instance. */
+ public static enum State {
+ /** Newly created instance trying to elect itself leader. */
+ CANDIDATE,
+
+ /** Leader instance elected by a majority. */
+ LEADER,
+
+ /** Instance has been deposed by another with a more recent term. */
+ DEPOSED,
+
+ /** Leader has been gracefully shutdown, e.g. due to inactivity. */
+ SHUTDOWN;
+ }
+
+ private final KetchSystem system;
+
+ /** Leader's knowledge of replicas for this repository. */
+ private KetchReplica[] voters;
+ private KetchReplica[] followers;
+ private LocalReplica self;
+
+ /**
+ * Lock protecting all data within this leader instance.
+ * <p>
+ * This lock extends into the {@link KetchReplica} instances used by the
+ * leader. They share the same lock instance to simplify concurrency.
+ */
+ final Lock lock;
+
+ private State state = CANDIDATE;
+
+ /** Term of this leader, once elected. */
+ private long term;
+
+ /**
+ * Pending proposals accepted into the queue in FIFO order.
+ * <p>
+ * These proposals were preflighted and do not contain any conflicts with
+ * each other and their expectations matched the leader's local view of the
+ * agreed upon {@code refs/txn/accepted} tree.
+ */
+ private final List<Proposal> queued;
+
+ /**
+ * State of the repository's RefTree after applying all entries in
+ * {@link #queued}. New proposals must be consistent with this tree to be
+ * appended to the end of {@link #queued}.
+ * <p>
+ * Must be deep-copied with {@link RefTree#copy()} if
+ * {@link #roundHoldsReferenceToRefTree} is {@code true}.
+ */
+ private RefTree refTree;
+
+ /**
+ * If {@code true} {@link #refTree} must be duplicated before queuing the
+ * next proposal. The {@link #refTree} was passed into the constructor of a
+ * {@link ProposalRound}, and that external reference to the {@link RefTree}
+ * object is held by the proposal until it materializes the tree object in
+ * the object store. This field is set {@code true} when the proposal begins
+ * execution and set {@code false} once tree objects are persisted in the
+ * local repository's object store or {@link #refTree} is replaced with a
+ * copy to isolate it from any running rounds.
+ * <p>
+ * If proposals arrive less frequently than the {@code RefTree} is written
+ * out to the repository the {@link #roundHoldsReferenceToRefTree} behavior
+ * avoids duplicating {@link #refTree}, reducing both time and memory used.
+ * However if proposals arrive more frequently {@link #refTree} must be
+ * duplicated to prevent newly queued proposals from corrupting the
+ * {@link #runningRound}.
+ */
+ volatile boolean roundHoldsReferenceToRefTree;
+
+ /** End of the leader's log. */
+ private LogIndex headIndex;
+
+ /** Leader knows this (and all prior) states are committed. */
+ private LogIndex committedIndex;
+
+ /**
+ * Is the leader idle with no work pending? If {@code true} there is no work
+ * for the leader (normal state). This field is {@code false} when the
+ * leader thread is scheduled for execution, or while {@link #runningRound}
+ * defines a round in progress.
+ */
+ private boolean idle;
+
+ /** Current round the leader is preparing and waiting for a vote on. */
+ private Round runningRound;
+
+ /**
+ * Construct a leader for a Ketch instance.
+ *
+ * @param system
+ * Ketch system configuration the leader must adhere to.
+ */
+ protected KetchLeader(KetchSystem system) {
+ this.system = system;
+ this.lock = new ReentrantLock(true /* fair */);
+ this.queued = new ArrayList<>(4);
+ this.idle = true;
+ }
+
+ /** @return system configuration. */
+ KetchSystem getSystem() {
+ return system;
+ }
+
+ /**
+ * Configure the replicas used by this Ketch instance.
+ * <p>
+ * Replicas should be configured once at creation before any proposals are
+ * executed. Once elections happen, <b>reconfiguration is a complicated
+ * concept that is not currently supported</b>.
+ *
+ * @param replicas
+ * members participating with the same repository.
+ */
+ public void setReplicas(Collection<KetchReplica> replicas) {
+ List<KetchReplica> v = new ArrayList<>(5);
+ List<KetchReplica> f = new ArrayList<>(5);
+ for (KetchReplica r : replicas) {
+ switch (r.getParticipation()) {
+ case FULL:
+ v.add(r);
+ break;
+
+ case FOLLOWER_ONLY:
+ f.add(r);
+ break;
+ }
+ }
+
+ Collection<Integer> validVoters = validVoterCounts();
+ if (!validVoters.contains(Integer.valueOf(v.size()))) {
+ throw new IllegalArgumentException(MessageFormat.format(
+ KetchText.get().unsupportedVoterCount,
+ Integer.valueOf(v.size()),
+ validVoters));
+ }
+
+ LocalReplica me = findLocal(v);
+ if (me == null) {
+ throw new IllegalArgumentException(
+ KetchText.get().localReplicaRequired);
+ }
+
+ lock.lock();
+ try {
+ voters = v.toArray(new KetchReplica[v.size()]);
+ followers = f.toArray(new KetchReplica[f.size()]);
+ self = me;
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ private static Collection<Integer> validVoterCounts() {
+ @SuppressWarnings("boxing")
+ Integer[] valid = {
+ // An odd number of voting replicas is required.
+ 1, 3, 5, 7, 9 };
+ return Arrays.asList(valid);
+ }
+
+ private static LocalReplica findLocal(Collection<KetchReplica> voters) {
+ for (KetchReplica r : voters) {
+ if (r instanceof LocalReplica) {
+ return (LocalReplica) r;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get an instance of the repository for use by a leader thread.
+ * <p>
+ * The caller will close the repository.
+ *
+ * @return opened repository for use by the leader thread.
+ * @throws IOException
+ * cannot reopen the repository for the leader.
+ */
+ protected abstract Repository openRepository() throws IOException;
+
+ /**
+ * Queue a reference update proposal for consensus.
+ * <p>
+ * This method does not wait for consensus to be reached. The proposal is
+ * checked to look for risks of conflicts, and then submitted into the queue
+ * for distribution as soon as possible.
+ * <p>
+ * Callers must use {@link Proposal#await()} to see if the proposal is done.
+ *
+ * @param proposal
+ * the proposed reference updates to queue for consideration.
+ * Once execution is complete the individual reference result
+ * fields will be populated with the outcome.
+ * @throws InterruptedException
+ * current thread was interrupted. The proposal may have been
+ * aborted if it was not yet queued for execution.
+ * @throws IOException
+ * unrecoverable error preventing proposals from being attempted
+ * by this leader.
+ */
+ public void queueProposal(Proposal proposal)
+ throws InterruptedException, IOException {
+ try {
+ lock.lockInterruptibly();
+ } catch (InterruptedException e) {
+ proposal.abort();
+ throw e;
+ }
+ try {
+ if (refTree == null) {
+ initialize();
+ for (Proposal p : queued) {
+ refTree.apply(p.getCommands());
+ }
+ } else if (roundHoldsReferenceToRefTree) {
+ refTree = refTree.copy();
+ roundHoldsReferenceToRefTree = false;
+ }
+
+ if (!refTree.apply(proposal.getCommands())) {
+ // A conflict exists so abort the proposal.
+ proposal.abort();
+ return;
+ }
+
+ queued.add(proposal);
+ proposal.notifyState(QUEUED);
+
+ if (idle) {
+ scheduleLeader();
+ }
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ private void initialize() throws IOException {
+ try (Repository git = openRepository(); RevWalk rw = new RevWalk(git)) {
+ self.initialize(git);
+
+ ObjectId accepted = self.getTxnAccepted();
+ if (!ObjectId.zeroId().equals(accepted)) {
+ RevCommit c = rw.parseCommit(accepted);
+ headIndex = LogIndex.unknown(accepted);
+ refTree = RefTree.read(rw.getObjectReader(), c.getTree());
+ } else {
+ headIndex = LogIndex.unknown(ObjectId.zeroId());
+ refTree = RefTree.newEmptyTree();
+ }
+ }
+ }
+
+ private void scheduleLeader() {
+ idle = false;
+ system.getExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ runLeader();
+ }
+ });
+ }
+
+ private void runLeader() {
+ Round round;
+ lock.lock();
+ try {
+ switch (state) {
+ case CANDIDATE:
+ round = new ElectionRound(this, headIndex);
+ break;
+
+ case LEADER:
+ round = newProposalRound();
+ break;
+
+ case DEPOSED:
+ case SHUTDOWN:
+ default:
+ log.warn("Leader cannot run {}", state); //$NON-NLS-1$
+ // TODO(sop): Redirect proposals.
+ return;
+ }
+ } finally {
+ lock.unlock();
+ }
+
+ try {
+ round.start();
+ } catch (IOException e) {
+ // TODO(sop) Depose leader if it cannot use its repository.
+ log.error(KetchText.get().leaderFailedToStore, e);
+ lock.lock();
+ try {
+ nextRound();
+ } finally {
+ lock.unlock();
+ }
+ }
+ }
+
+ private ProposalRound newProposalRound() {
+ List<Proposal> todo = new ArrayList<>(queued);
+ queued.clear();
+ roundHoldsReferenceToRefTree = true;
+ return new ProposalRound(this, headIndex, todo, refTree);
+ }
+
+ /** @return term of this leader's reign. */
+ long getTerm() {
+ return term;
+ }
+
+ /** @return end of the leader's log. */
+ LogIndex getHead() {
+ return headIndex;
+ }
+
+ /**
+ * @return state leader knows it has committed across a quorum of replicas.
+ */
+ LogIndex getCommitted() {
+ return committedIndex;
+ }
+
+ boolean isIdle() {
+ return idle;
+ }
+
+ void runAsync(Round round) {
+ lock.lock();
+ try {
+ // End of the log is this round. Once transport begins it is
+ // reasonable to assume at least one replica will eventually get
+ // this, and there is reasonable probability it commits.
+ headIndex = round.acceptedNewIndex;
+ runningRound = round;
+
+ for (KetchReplica replica : voters) {
+ replica.pushTxnAcceptedAsync(round);
+ }
+ for (KetchReplica replica : followers) {
+ replica.pushTxnAcceptedAsync(round);
+ }
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Asynchronous signal from a replica after completion.
+ * <p>
+ * Must be called while {@link #lock} is held by the replica.
+ *
+ * @param replica
+ * replica posting a completion event.
+ */
+ void onReplicaUpdate(KetchReplica replica) {
+ if (log.isDebugEnabled()) {
+ log.debug("Replica {} finished:\n{}", //$NON-NLS-1$
+ replica.describeForLog(), snapshot());
+ }
+
+ if (replica.getParticipation() == FOLLOWER_ONLY) {
+ // Followers cannot vote, so votes haven't changed.
+ return;
+ } else if (runningRound == null) {
+ // No round running, no need to tally votes.
+ return;
+ }
+
+ assert headIndex.equals(runningRound.acceptedNewIndex);
+ int matching = 0;
+ for (KetchReplica r : voters) {
+ if (r.hasAccepted(headIndex)) {
+ matching++;
+ }
+ }
+
+ int quorum = voters.length / 2 + 1;
+ boolean success = matching >= quorum;
+ if (!success) {
+ return;
+ }
+
+ switch (state) {
+ case CANDIDATE:
+ term = ((ElectionRound) runningRound).getTerm();
+ state = LEADER;
+ if (log.isDebugEnabled()) {
+ log.debug("Won election, running term " + term); //$NON-NLS-1$
+ }
+
+ //$FALL-THROUGH$
+ case LEADER:
+ committedIndex = headIndex;
+ if (log.isDebugEnabled()) {
+ log.debug("Committed {} in term {}", //$NON-NLS-1$
+ committedIndex.describeForLog(),
+ Long.valueOf(term));
+ }
+ nextRound();
+ commitAsync(replica);
+ notifySuccess(runningRound);
+ if (log.isDebugEnabled()) {
+ log.debug("Leader state:\n{}", snapshot()); //$NON-NLS-1$
+ }
+ break;
+
+ default:
+ log.debug("Leader ignoring replica while in {}", state); //$NON-NLS-1$
+ break;
+ }
+ }
+
+ private void notifySuccess(Round round) {
+ // Drop the leader lock while notifying Proposal listeners.
+ lock.unlock();
+ try {
+ round.success();
+ } finally {
+ lock.lock();
+ }
+ }
+
+ private void commitAsync(KetchReplica caller) {
+ for (KetchReplica r : voters) {
+ if (r == caller) {
+ continue;
+ }
+ if (r.shouldPushUnbatchedCommit(committedIndex, isIdle())) {
+ r.pushCommitAsync(committedIndex);
+ }
+ }
+ for (KetchReplica r : followers) {
+ if (r == caller) {
+ continue;
+ }
+ if (r.shouldPushUnbatchedCommit(committedIndex, isIdle())) {
+ r.pushCommitAsync(committedIndex);
+ }
+ }
+ }
+
+ /** Schedule the next round; invoked while {@link #lock} is held. */
+ void nextRound() {
+ runningRound = null;
+
+ if (queued.isEmpty()) {
+ idle = true;
+ } else {
+ // Caller holds lock. Reschedule leader on a new thread so
+ // the call stack can unwind and lock is not held unexpectedly
+ // during prepare for the next round.
+ scheduleLeader();
+ }
+ }
+
+ /** @return snapshot this leader. */
+ public LeaderSnapshot snapshot() {
+ lock.lock();
+ try {
+ LeaderSnapshot s = new LeaderSnapshot();
+ s.state = state;
+ s.term = term;
+ s.headIndex = headIndex;
+ s.committedIndex = committedIndex;
+ s.idle = isIdle();
+ for (KetchReplica r : voters) {
+ s.replicas.add(r.snapshot());
+ }
+ for (KetchReplica r : followers) {
+ s.replicas.add(r.snapshot());
+ }
+ return s;
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /** Gracefully shutdown this leader and cancel outstanding operations. */
+ public void shutdown() {
+ lock.lock();
+ try {
+ if (state != SHUTDOWN) {
+ state = SHUTDOWN;
+ for (KetchReplica r : voters) {
+ r.shutdown();
+ }
+ for (KetchReplica r : followers) {
+ r.shutdown();
+ }
+ }
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return snapshot().toString();
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchLeaderCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchLeaderCache.java
new file mode 100644
index 0000000000..ba033c1a42
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchLeaderCache.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import java.net.URISyntaxException;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+import org.eclipse.jgit.internal.storage.dfs.DfsRepository;
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * A cache of live leader instances, keyed by repository.
+ * <p>
+ * Ketch only assigns a leader to a repository when needed. If
+ * {@link #get(Repository)} is called for a repository that does not have a
+ * leader, the leader is created and added to the cache.
+ */
+public class KetchLeaderCache {
+ private final KetchSystem system;
+ private final ConcurrentMap<String, KetchLeader> leaders;
+ private final Lock startLock;
+
+ /**
+ * Initialize a new leader cache.
+ *
+ * @param system
+ * system configuration for the leaders
+ */
+ public KetchLeaderCache(KetchSystem system) {
+ this.system = system;
+ leaders = new ConcurrentHashMap<>();
+ startLock = new ReentrantLock(true /* fair */);
+ }
+
+ /**
+ * Lookup the leader instance for a given repository.
+ *
+ * @param repo
+ * repository to get the leader for.
+ * @return the leader instance for the repository.
+ * @throws URISyntaxException
+ * remote configuration contains an invalid URL.
+ */
+ public KetchLeader get(Repository repo)
+ throws URISyntaxException {
+ String key = computeKey(repo);
+ KetchLeader leader = leaders.get(key);
+ if (leader != null) {
+ return leader;
+ }
+ return startLeader(key, repo);
+ }
+
+ private KetchLeader startLeader(String key, Repository repo)
+ throws URISyntaxException {
+ startLock.lock();
+ try {
+ KetchLeader leader = leaders.get(key);
+ if (leader != null) {
+ return leader;
+ }
+ leader = system.createLeader(repo);
+ leaders.put(key, leader);
+ return leader;
+ } finally {
+ startLock.unlock();
+ }
+ }
+
+ private static String computeKey(Repository repo) {
+ if (repo instanceof DfsRepository) {
+ DfsRepository dfs = (DfsRepository) repo;
+ return dfs.getDescription().getRepositoryName();
+ }
+
+ if (repo.getDirectory() != null) {
+ return repo.getDirectory().toURI().toString();
+ }
+
+ throw new IllegalArgumentException();
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchPreReceive.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchPreReceive.java
new file mode 100644
index 0000000000..1b4307f3fb
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchPreReceive.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.eclipse.jgit.internal.ketch.Proposal.State.EXECUTED;
+import static org.eclipse.jgit.internal.ketch.Proposal.State.QUEUED;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
+
+import java.io.IOException;
+import java.util.Collection;
+
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.transport.PreReceiveHook;
+import org.eclipse.jgit.transport.ProgressSpinner;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.transport.ReceivePack;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * PreReceiveHook for handling push traffic in a Ketch system.
+ * <p>
+ * Install an instance on {@link ReceivePack} to capture the commands and other
+ * connection state and relay them through the {@link KetchLeader}, allowing the
+ * leader to gain consensus about the new reference state.
+ */
+public class KetchPreReceive implements PreReceiveHook {
+ private static final Logger log = LoggerFactory.getLogger(KetchPreReceive.class);
+
+ private final KetchLeader leader;
+
+ /**
+ * Construct a hook executing updates through a {@link KetchLeader}.
+ *
+ * @param leader
+ * leader for this repository.
+ */
+ public KetchPreReceive(KetchLeader leader) {
+ this.leader = leader;
+ }
+
+ @Override
+ public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> cmds) {
+ cmds = ReceiveCommand.filter(cmds, NOT_ATTEMPTED);
+ if (cmds.isEmpty()) {
+ return;
+ }
+
+ try {
+ Proposal proposal = new Proposal(rp.getRevWalk(), cmds)
+ .setPushCertificate(rp.getPushCertificate())
+ .setAuthor(rp.getRefLogIdent())
+ .setMessage("push"); //$NON-NLS-1$
+ leader.queueProposal(proposal);
+ if (proposal.isDone()) {
+ // This failed fast, e.g. conflict or bad precondition.
+ return;
+ }
+
+ ProgressSpinner spinner = new ProgressSpinner(
+ rp.getMessageOutputStream());
+ if (proposal.getState() == QUEUED) {
+ waitForQueue(proposal, spinner);
+ }
+ if (!proposal.isDone()) {
+ waitForPropose(proposal, spinner);
+ }
+ } catch (IOException | InterruptedException e) {
+ String msg = JGitText.get().transactionAborted;
+ for (ReceiveCommand cmd : cmds) {
+ if (cmd.getResult() == NOT_ATTEMPTED) {
+ cmd.setResult(REJECTED_OTHER_REASON, msg);
+ }
+ }
+ log.error(msg, e);
+ }
+ }
+
+ private void waitForQueue(Proposal proposal, ProgressSpinner spinner)
+ throws InterruptedException {
+ spinner.beginTask(KetchText.get().waitingForQueue, 1, SECONDS);
+ while (!proposal.awaitStateChange(QUEUED, 250, MILLISECONDS)) {
+ spinner.update();
+ }
+ switch (proposal.getState()) {
+ case RUNNING:
+ default:
+ spinner.endTask(KetchText.get().starting);
+ break;
+
+ case EXECUTED:
+ spinner.endTask(KetchText.get().accepted);
+ break;
+
+ case ABORTED:
+ spinner.endTask(KetchText.get().failed);
+ break;
+ }
+ }
+
+ private void waitForPropose(Proposal proposal, ProgressSpinner spinner)
+ throws InterruptedException {
+ spinner.beginTask(KetchText.get().proposingUpdates, 2, SECONDS);
+ while (!proposal.await(250, MILLISECONDS)) {
+ spinner.update();
+ }
+ spinner.endTask(proposal.getState() == EXECUTED
+ ? KetchText.get().accepted
+ : KetchText.get().failed);
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchReplica.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchReplica.java
new file mode 100644
index 0000000000..a30bbb260a
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchReplica.java
@@ -0,0 +1,755 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static org.eclipse.jgit.internal.ketch.KetchReplica.CommitSpeed.BATCHED;
+import static org.eclipse.jgit.internal.ketch.KetchReplica.CommitSpeed.FAST;
+import static org.eclipse.jgit.internal.ketch.KetchReplica.State.CURRENT;
+import static org.eclipse.jgit.internal.ketch.KetchReplica.State.LAGGING;
+import static org.eclipse.jgit.internal.ketch.KetchReplica.State.OFFLINE;
+import static org.eclipse.jgit.internal.ketch.KetchReplica.State.UNKNOWN;
+import static org.eclipse.jgit.lib.Constants.HEAD;
+import static org.eclipse.jgit.lib.FileMode.TYPE_GITLINK;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
+import static org.eclipse.jgit.transport.ReceiveCommand.Type.CREATE;
+
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Future;
+
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.internal.storage.reftree.RefTree;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.util.SystemReader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A Ketch replica, either {@link LocalReplica} or {@link RemoteGitReplica}.
+ * <p>
+ * Replicas can be either a stock Git replica, or a Ketch-aware replica.
+ * <p>
+ * A stock Git replica has no special knowledge of Ketch and simply stores
+ * objects and references. Ketch communicates with the stock Git replica using
+ * the Git push wire protocol. The {@link KetchLeader} commits an agreed upon
+ * state by pushing all references to the Git replica, for example
+ * {@code "refs/heads/master"} is pushed during commit. Stock Git replicas use
+ * {@link CommitMethod#ALL_REFS} to record the final state.
+ * <p>
+ * Ketch-aware replicas understand the {@code RefTree} sent during the proposal
+ * and during commit are able to update their own reference space to match the
+ * state represented by the {@code RefTree}. Ketch-aware replicas typically use
+ * a {@link org.eclipse.jgit.internal.storage.reftree.RefTreeDatabase} and
+ * {@link CommitMethod#TXN_COMMITTED} to record the final state.
+ * <p>
+ * KetchReplica instances are tightly coupled with a single {@link KetchLeader}.
+ * Some state may be accessed by the leader thread and uses the leader's own
+ * {@link KetchLeader#lock} to protect shared data.
+ */
+public abstract class KetchReplica {
+ static final Logger log = LoggerFactory.getLogger(KetchReplica.class);
+ private static final byte[] PEEL = { ' ', '^' };
+
+ /** Participation of a replica in establishing consensus. */
+ public enum Participation {
+ /** Replica can vote. */
+ FULL,
+
+ /** Replica does not vote, but tracks leader. */
+ FOLLOWER_ONLY;
+ }
+
+ /** How this replica wants to receive Ketch commit operations. */
+ public enum CommitMethod {
+ /** All references are pushed to the peer as standard Git. */
+ ALL_REFS,
+
+ /** Only {@code refs/txn/committed} is written/updated. */
+ TXN_COMMITTED;
+ }
+
+ /** Delay before committing to a replica. */
+ public enum CommitSpeed {
+ /**
+ * Send the commit immediately, even if it could be batched with the
+ * next proposal.
+ */
+ FAST,
+
+ /**
+ * If the next proposal is available, batch the commit with it,
+ * otherwise just send the commit. This generates less network use, but
+ * may provide slower consistency on the replica.
+ */
+ BATCHED;
+ }
+
+ /** Current state of a replica. */
+ public enum State {
+ /** Leader has not yet contacted the replica. */
+ UNKNOWN,
+
+ /** Replica is behind the consensus. */
+ LAGGING,
+
+ /** Replica matches the consensus. */
+ CURRENT,
+
+ /** Replica has a different (or unknown) history. */
+ DIVERGENT,
+
+ /** Replica's history contains the leader's history. */
+ AHEAD,
+
+ /** Replica can not be contacted. */
+ OFFLINE;
+ }
+
+ private final KetchLeader leader;
+ private final String replicaName;
+ private final Participation participation;
+ private final CommitMethod commitMethod;
+ private final CommitSpeed commitSpeed;
+ private final long minRetryMillis;
+ private final long maxRetryMillis;
+ private final Map<ObjectId, List<ReceiveCommand>> staged;
+ private final Map<String, ReceiveCommand> running;
+ private final Map<String, ReceiveCommand> waiting;
+ private final List<ReplicaPushRequest> queued;
+
+ /**
+ * Value known for {@code "refs/txn/accepted"}.
+ * <p>
+ * Raft literature refers to this as {@code matchIndex}.
+ */
+ private ObjectId txnAccepted;
+
+ /**
+ * Value known for {@code "refs/txn/committed"}.
+ * <p>
+ * Raft literature refers to this as {@code commitIndex}. In traditional
+ * Raft this is a state variable inside the follower implementation, but
+ * Ketch keeps it in the leader.
+ */
+ private ObjectId txnCommitted;
+
+ /** What is happening with this replica. */
+ private State state = UNKNOWN;
+ private String error;
+
+ /** Scheduled retry due to communication failure. */
+ private Future<?> retryFuture;
+ private long lastRetryMillis;
+ private long retryAtMillis;
+
+ /**
+ * Configure a replica representation.
+ *
+ * @param leader
+ * instance this replica follows.
+ * @param name
+ * unique-ish name identifying this replica for debugging.
+ * @param cfg
+ * how Ketch should treat the replica.
+ */
+ protected KetchReplica(KetchLeader leader, String name, ReplicaConfig cfg) {
+ this.leader = leader;
+ this.replicaName = name;
+ this.participation = cfg.getParticipation();
+ this.commitMethod = cfg.getCommitMethod();
+ this.commitSpeed = cfg.getCommitSpeed();
+ this.minRetryMillis = cfg.getMinRetry(MILLISECONDS);
+ this.maxRetryMillis = cfg.getMaxRetry(MILLISECONDS);
+ this.staged = new HashMap<>();
+ this.running = new HashMap<>();
+ this.waiting = new HashMap<>();
+ this.queued = new ArrayList<>(4);
+ }
+
+ /** @return system configuration. */
+ public KetchSystem getSystem() {
+ return getLeader().getSystem();
+ }
+
+ /** @return leader instance this replica follows. */
+ public KetchLeader getLeader() {
+ return leader;
+ }
+
+ /** @return unique-ish name for debugging. */
+ public String getName() {
+ return replicaName;
+ }
+
+ /** @return description of this replica for error/debug logging purposes. */
+ protected String describeForLog() {
+ return getName();
+ }
+
+ /** @return how the replica participates in this Ketch system. */
+ public Participation getParticipation() {
+ return participation;
+ }
+
+ /** @return how Ketch will commit to the repository. */
+ public CommitMethod getCommitMethod() {
+ return commitMethod;
+ }
+
+ /** @return when Ketch will commit to the repository. */
+ public CommitSpeed getCommitSpeed() {
+ return commitSpeed;
+ }
+
+ /**
+ * Called by leader to perform graceful shutdown.
+ * <p>
+ * Default implementation cancels any scheduled retry. Subclasses may add
+ * additional logic before or after calling {@code super.shutdown()}.
+ * <p>
+ * Called with {@link KetchLeader#lock} held by caller.
+ */
+ protected void shutdown() {
+ Future<?> f = retryFuture;
+ if (f != null) {
+ retryFuture = null;
+ f.cancel(true);
+ }
+ }
+
+ ReplicaSnapshot snapshot() {
+ ReplicaSnapshot s = new ReplicaSnapshot(this);
+ s.accepted = txnAccepted;
+ s.committed = txnCommitted;
+ s.state = state;
+ s.error = error;
+ s.retryAtMillis = waitingForRetry() ? retryAtMillis : 0;
+ return s;
+ }
+
+ /**
+ * Update the leader's view of the replica after a poll.
+ * <p>
+ * Called with {@link KetchLeader#lock} held by caller.
+ *
+ * @param refs
+ * map of refs from the replica.
+ */
+ void initialize(Map<String, Ref> refs) {
+ if (txnAccepted == null) {
+ txnAccepted = getId(refs.get(getSystem().getTxnAccepted()));
+ }
+ if (txnCommitted == null) {
+ txnCommitted = getId(refs.get(getSystem().getTxnCommitted()));
+ }
+ }
+
+ ObjectId getTxnAccepted() {
+ return txnAccepted;
+ }
+
+ boolean hasAccepted(LogIndex id) {
+ return equals(txnAccepted, id);
+ }
+
+ private static boolean equals(@Nullable ObjectId a, LogIndex b) {
+ return a != null && b != null && AnyObjectId.equals(a, b);
+ }
+
+ /**
+ * Schedule a proposal round with the replica.
+ * <p>
+ * Called with {@link KetchLeader#lock} held by caller.
+ *
+ * @param round
+ * current round being run by the leader.
+ */
+ void pushTxnAcceptedAsync(Round round) {
+ List<ReceiveCommand> cmds = new ArrayList<>();
+ if (commitSpeed == BATCHED) {
+ LogIndex committedIndex = leader.getCommitted();
+ if (equals(txnAccepted, committedIndex)
+ && !equals(txnCommitted, committedIndex)) {
+ prepareTxnCommitted(cmds, committedIndex);
+ }
+ }
+
+ // TODO(sop) Lagging replicas should build accept on the fly.
+ if (round.stageCommands != null) {
+ for (ReceiveCommand cmd : round.stageCommands) {
+ // TODO(sop): Do not send certain object graphs to replica.
+ cmds.add(copy(cmd));
+ }
+ }
+ cmds.add(new ReceiveCommand(
+ round.acceptedOldIndex, round.acceptedNewIndex,
+ getSystem().getTxnAccepted()));
+ pushAsync(new ReplicaPushRequest(this, cmds));
+ }
+
+ private static ReceiveCommand copy(ReceiveCommand c) {
+ return new ReceiveCommand(c.getOldId(), c.getNewId(), c.getRefName());
+ }
+
+ boolean shouldPushUnbatchedCommit(LogIndex committed, boolean leaderIdle) {
+ return (leaderIdle || commitSpeed == FAST) && hasAccepted(committed);
+ }
+
+ void pushCommitAsync(LogIndex committed) {
+ List<ReceiveCommand> cmds = new ArrayList<>();
+ prepareTxnCommitted(cmds, committed);
+ pushAsync(new ReplicaPushRequest(this, cmds));
+ }
+
+ private void prepareTxnCommitted(List<ReceiveCommand> cmds,
+ ObjectId committed) {
+ removeStaged(cmds, committed);
+ cmds.add(new ReceiveCommand(
+ txnCommitted, committed,
+ getSystem().getTxnCommitted()));
+ }
+
+ private void removeStaged(List<ReceiveCommand> cmds, ObjectId committed) {
+ List<ReceiveCommand> a = staged.remove(committed);
+ if (a != null) {
+ delete(cmds, a);
+ }
+ if (staged.isEmpty() || !(committed instanceof LogIndex)) {
+ return;
+ }
+
+ LogIndex committedIndex = (LogIndex) committed;
+ Iterator<Map.Entry<ObjectId, List<ReceiveCommand>>> itr = staged
+ .entrySet().iterator();
+ while (itr.hasNext()) {
+ Map.Entry<ObjectId, List<ReceiveCommand>> e = itr.next();
+ if (e.getKey() instanceof LogIndex) {
+ LogIndex stagedIndex = (LogIndex) e.getKey();
+ if (stagedIndex.isBefore(committedIndex)) {
+ delete(cmds, e.getValue());
+ itr.remove();
+ }
+ }
+ }
+ }
+
+ private static void delete(List<ReceiveCommand> cmds,
+ List<ReceiveCommand> createCmds) {
+ for (ReceiveCommand cmd : createCmds) {
+ ObjectId id = cmd.getNewId();
+ String name = cmd.getRefName();
+ cmds.add(new ReceiveCommand(id, ObjectId.zeroId(), name));
+ }
+ }
+
+ /**
+ * Determine the next push for this replica (if any) and start it.
+ * <p>
+ * If the replica has successfully accepted the committed state of the
+ * leader, this method will push all references to the replica using the
+ * configured {@link CommitMethod}.
+ * <p>
+ * If the replica is {@link State#LAGGING} this method will begin catch up
+ * by sending a more recent {@code refs/txn/accepted}.
+ * <p>
+ * Must be invoked with {@link KetchLeader#lock} held by caller.
+ */
+ private void runNextPushRequest() {
+ LogIndex committed = leader.getCommitted();
+ if (!equals(txnCommitted, committed)
+ && shouldPushUnbatchedCommit(committed, leader.isIdle())) {
+ pushCommitAsync(committed);
+ }
+
+ if (queued.isEmpty() || !running.isEmpty() || waitingForRetry()) {
+ return;
+ }
+
+ // Collapse all queued requests into a single request.
+ Map<String, ReceiveCommand> cmdMap = new HashMap<>();
+ for (ReplicaPushRequest req : queued) {
+ for (ReceiveCommand cmd : req.getCommands()) {
+ String name = cmd.getRefName();
+ ReceiveCommand old = cmdMap.remove(name);
+ if (old != null) {
+ cmd = new ReceiveCommand(
+ old.getOldId(), cmd.getNewId(),
+ name);
+ }
+ cmdMap.put(name, cmd);
+ }
+ }
+ queued.clear();
+ waiting.clear();
+
+ List<ReceiveCommand> next = new ArrayList<>(cmdMap.values());
+ for (ReceiveCommand cmd : next) {
+ running.put(cmd.getRefName(), cmd);
+ }
+ startPush(new ReplicaPushRequest(this, next));
+ }
+
+ private void pushAsync(ReplicaPushRequest req) {
+ if (defer(req)) {
+ // TODO(sop) Collapse during long retry outage.
+ for (ReceiveCommand cmd : req.getCommands()) {
+ waiting.put(cmd.getRefName(), cmd);
+ }
+ queued.add(req);
+ } else {
+ for (ReceiveCommand cmd : req.getCommands()) {
+ running.put(cmd.getRefName(), cmd);
+ }
+ startPush(req);
+ }
+ }
+
+ private boolean defer(ReplicaPushRequest req) {
+ if (waitingForRetry()) {
+ // Prior communication failure; everything is deferred.
+ return true;
+ }
+
+ for (ReceiveCommand nextCmd : req.getCommands()) {
+ ReceiveCommand priorCmd = waiting.get(nextCmd.getRefName());
+ if (priorCmd == null) {
+ priorCmd = running.get(nextCmd.getRefName());
+ }
+ if (priorCmd != null) {
+ // Another request pending on same ref; that must go first.
+ // Verify priorCmd.newId == nextCmd.oldId?
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean waitingForRetry() {
+ Future<?> f = retryFuture;
+ return f != null && !f.isDone();
+ }
+
+ private void retryLater(ReplicaPushRequest req) {
+ Collection<ReceiveCommand> cmds = req.getCommands();
+ for (ReceiveCommand cmd : cmds) {
+ cmd.setResult(NOT_ATTEMPTED, null);
+ if (!waiting.containsKey(cmd.getRefName())) {
+ waiting.put(cmd.getRefName(), cmd);
+ }
+ }
+ queued.add(0, new ReplicaPushRequest(this, cmds));
+
+ if (!waitingForRetry()) {
+ long delay = KetchSystem.delay(
+ lastRetryMillis,
+ minRetryMillis, maxRetryMillis);
+ if (log.isDebugEnabled()) {
+ log.debug("Retrying {} after {} ms", //$NON-NLS-1$
+ describeForLog(), Long.valueOf(delay));
+ }
+ lastRetryMillis = delay;
+ retryAtMillis = SystemReader.getInstance().getCurrentTime() + delay;
+ retryFuture = getSystem().getExecutor()
+ .schedule(new WeakRetryPush(this), delay, MILLISECONDS);
+ }
+ }
+
+ /** Weakly holds a retrying replica, allowing it to garbage collect. */
+ static class WeakRetryPush extends WeakReference<KetchReplica>
+ implements Callable<Void> {
+ WeakRetryPush(KetchReplica r) {
+ super(r);
+ }
+
+ @Override
+ public Void call() throws Exception {
+ KetchReplica r = get();
+ if (r != null) {
+ r.doRetryPush();
+ }
+ return null;
+ }
+ }
+
+ private void doRetryPush() {
+ leader.lock.lock();
+ try {
+ retryFuture = null;
+ runNextPushRequest();
+ } finally {
+ leader.lock.unlock();
+ }
+ }
+
+ /**
+ * Begin executing a single push.
+ * <p>
+ * This method must move processing onto another thread.
+ * Called with {@link KetchLeader#lock} held by caller.
+ *
+ * @param req
+ * the request to send to the replica.
+ */
+ protected abstract void startPush(ReplicaPushRequest req);
+
+ /**
+ * Callback from {@link ReplicaPushRequest} upon success or failure.
+ * <p>
+ * Acquires the {@link KetchLeader#lock} and updates the leader's internal
+ * knowledge about this replica to reflect what has been learned during a
+ * push to the replica. In some cases of divergence this method may take
+ * some time to determine how the replica has diverged; to reduce contention
+ * this is evaluated before acquiring the leader lock.
+ *
+ * @param repo
+ * local repository instance used by the push thread.
+ * @param req
+ * push request just attempted.
+ */
+ void afterPush(@Nullable Repository repo, ReplicaPushRequest req) {
+ ReceiveCommand acceptCmd = null;
+ ReceiveCommand commitCmd = null;
+ List<ReceiveCommand> stages = null;
+
+ for (ReceiveCommand cmd : req.getCommands()) {
+ String name = cmd.getRefName();
+ if (name.equals(getSystem().getTxnAccepted())) {
+ acceptCmd = cmd;
+ } else if (name.equals(getSystem().getTxnCommitted())) {
+ commitCmd = cmd;
+ } else if (cmd.getResult() == OK && cmd.getType() == CREATE
+ && name.startsWith(getSystem().getTxnStage())) {
+ if (stages == null) {
+ stages = new ArrayList<>();
+ }
+ stages.add(cmd);
+ }
+ }
+
+ State newState = null;
+ ObjectId acceptId = readId(req, acceptCmd);
+ if (repo != null && acceptCmd != null && acceptCmd.getResult() != OK
+ && req.getException() == null) {
+ try (LagCheck lag = new LagCheck(this, repo)) {
+ newState = lag.check(acceptId, acceptCmd);
+ acceptId = lag.getRemoteId();
+ }
+ }
+
+ leader.lock.lock();
+ try {
+ for (ReceiveCommand cmd : req.getCommands()) {
+ running.remove(cmd.getRefName());
+ }
+
+ Throwable err = req.getException();
+ if (err != null) {
+ state = OFFLINE;
+ error = err.toString();
+ retryLater(req);
+ leader.onReplicaUpdate(this);
+ return;
+ }
+
+ lastRetryMillis = 0;
+ error = null;
+ updateView(req, acceptId, commitCmd);
+
+ if (acceptCmd != null && acceptCmd.getResult() == OK) {
+ state = hasAccepted(leader.getHead()) ? CURRENT : LAGGING;
+ if (stages != null) {
+ staged.put(acceptCmd.getNewId(), stages);
+ }
+ } else if (newState != null) {
+ state = newState;
+ }
+
+ leader.onReplicaUpdate(this);
+ runNextPushRequest();
+ } finally {
+ leader.lock.unlock();
+ }
+ }
+
+ private void updateView(ReplicaPushRequest req, @Nullable ObjectId acceptId,
+ ReceiveCommand commitCmd) {
+ if (acceptId != null) {
+ txnAccepted = acceptId;
+ }
+
+ ObjectId committed = readId(req, commitCmd);
+ if (committed != null) {
+ txnCommitted = committed;
+ } else if (acceptId != null && txnCommitted == null) {
+ // Initialize during first conversation.
+ Map<String, Ref> adv = req.getRefs();
+ if (adv != null) {
+ Ref refs = adv.get(getSystem().getTxnCommitted());
+ txnCommitted = getId(refs);
+ }
+ }
+ }
+
+ @Nullable
+ private static ObjectId readId(ReplicaPushRequest req,
+ @Nullable ReceiveCommand cmd) {
+ if (cmd == null) {
+ // Ref was not in the command list, do not trust advertisement.
+ return null;
+
+ } else if (cmd.getResult() == OK) {
+ // Currently at newId.
+ return cmd.getNewId();
+ }
+
+ Map<String, Ref> refs = req.getRefs();
+ return refs != null ? getId(refs.get(cmd.getRefName())) : null;
+ }
+
+ /**
+ * Fetch objects from the remote using the calling thread.
+ * <p>
+ * Called without {@link KetchLeader#lock}.
+ *
+ * @param repo
+ * local repository to fetch objects into.
+ * @param req
+ * the request to fetch from a replica.
+ * @throws IOException
+ * communication with the replica was not possible.
+ */
+ protected abstract void blockingFetch(Repository repo,
+ ReplicaFetchRequest req) throws IOException;
+
+ /**
+ * Build a list of commands to commit {@link CommitMethod#ALL_REFS}.
+ *
+ * @param git
+ * local leader repository to read committed state from.
+ * @param current
+ * all known references in the replica's repository. Typically
+ * this comes from a push advertisement.
+ * @param committed
+ * state being pushed to {@code refs/txn/committed}.
+ * @return commands to update during commit.
+ * @throws IOException
+ * cannot read the committed state.
+ */
+ protected Collection<ReceiveCommand> prepareCommit(Repository git,
+ Map<String, Ref> current, ObjectId committed) throws IOException {
+ List<ReceiveCommand> delta = new ArrayList<>();
+ Map<String, Ref> remote = new HashMap<>(current);
+ try (RevWalk rw = new RevWalk(git);
+ TreeWalk tw = new TreeWalk(rw.getObjectReader())) {
+ tw.setRecursive(true);
+ tw.addTree(rw.parseCommit(committed).getTree());
+ while (tw.next()) {
+ if (tw.getRawMode(0) != TYPE_GITLINK
+ || tw.isPathSuffix(PEEL, 2)) {
+ // Symbolic references cannot be pushed.
+ // Caching peeled values is handled remotely.
+ continue;
+ }
+
+ // TODO(sop) Do not send certain ref names to replica.
+ String name = RefTree.refName(tw.getPathString());
+ Ref oldRef = remote.remove(name);
+ ObjectId oldId = getId(oldRef);
+ ObjectId newId = tw.getObjectId(0);
+ if (!AnyObjectId.equals(oldId, newId)) {
+ delta.add(new ReceiveCommand(oldId, newId, name));
+ }
+ }
+ }
+
+ // Delete any extra references not in the committed state.
+ for (Ref ref : remote.values()) {
+ if (canDelete(ref)) {
+ delta.add(new ReceiveCommand(
+ ref.getObjectId(), ObjectId.zeroId(),
+ ref.getName()));
+ }
+ }
+ return delta;
+ }
+
+ boolean canDelete(Ref ref) {
+ String name = ref.getName();
+ if (HEAD.equals(name)) {
+ return false;
+ }
+ if (name.startsWith(getSystem().getTxnNamespace())) {
+ return false;
+ }
+ // TODO(sop) Do not delete precious names from replica.
+ return true;
+ }
+
+ @NonNull
+ static ObjectId getId(@Nullable Ref ref) {
+ if (ref != null) {
+ ObjectId id = ref.getObjectId();
+ if (id != null) {
+ return id;
+ }
+ }
+ return ObjectId.zeroId();
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchSystem.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchSystem.java
new file mode 100644
index 0000000000..71e872e3fa
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchSystem.java
@@ -0,0 +1,301 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import static org.eclipse.jgit.internal.ketch.KetchConstants.ACCEPTED;
+import static org.eclipse.jgit.internal.ketch.KetchConstants.COMMITTED;
+import static org.eclipse.jgit.internal.ketch.KetchConstants.CONFIG_KEY_TYPE;
+import static org.eclipse.jgit.internal.ketch.KetchConstants.CONFIG_SECTION_KETCH;
+import static org.eclipse.jgit.internal.ketch.KetchConstants.DEFAULT_TXN_NAMESPACE;
+import static org.eclipse.jgit.internal.ketch.KetchConstants.STAGE;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_NAME;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_REMOTE;
+
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.RemoteConfig;
+import org.eclipse.jgit.transport.URIish;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Ketch system-wide configuration.
+ * <p>
+ * This class provides useful defaults for testing and small proof of concepts.
+ * Full scale installations are expected to subclass and override methods to
+ * provide consistent configuration across all managed repositories.
+ * <p>
+ * Servers should configure their own {@link ScheduledExecutorService}.
+ */
+public class KetchSystem {
+ private static final Random RNG = new Random();
+
+ /** @return default executor, one thread per available processor. */
+ public static ScheduledExecutorService defaultExecutor() {
+ return DefaultExecutorHolder.I;
+ }
+
+ private final ScheduledExecutorService executor;
+ private final String txnNamespace;
+ private final String txnAccepted;
+ private final String txnCommitted;
+ private final String txnStage;
+
+ /** Create a default system with a thread pool of 1 thread per CPU. */
+ public KetchSystem() {
+ this(defaultExecutor(), DEFAULT_TXN_NAMESPACE);
+ }
+
+ /**
+ * Create a Ketch system with the provided executor service.
+ *
+ * @param executor
+ * thread pool to run background operations.
+ * @param txnNamespace
+ * reference namespace for the RefTree graph and associated
+ * transaction state. Must begin with {@code "refs/"} and end
+ * with {@code '/'}, for example {@code "refs/txn/"}.
+ */
+ public KetchSystem(ScheduledExecutorService executor, String txnNamespace) {
+ this.executor = executor;
+ this.txnNamespace = txnNamespace;
+ this.txnAccepted = txnNamespace + ACCEPTED;
+ this.txnCommitted = txnNamespace + COMMITTED;
+ this.txnStage = txnNamespace + STAGE;
+ }
+
+ /** @return executor to perform background operations. */
+ public ScheduledExecutorService getExecutor() {
+ return executor;
+ }
+
+ /**
+ * Get the namespace used for the RefTree graph and transaction management.
+ *
+ * @return reference namespace such as {@code "refs/txn/"}.
+ */
+ public String getTxnNamespace() {
+ return txnNamespace;
+ }
+
+ /** @return name of the accepted RefTree graph. */
+ public String getTxnAccepted() {
+ return txnAccepted;
+ }
+
+ /** @return name of the committed RefTree graph. */
+ public String getTxnCommitted() {
+ return txnCommitted;
+ }
+
+ /** @return prefix for staged objects, e.g. {@code "refs/txn/stage/"}. */
+ public String getTxnStage() {
+ return txnStage;
+ }
+
+ /** @return identity line for the committer header of a RefTreeGraph. */
+ public PersonIdent newCommitter() {
+ String name = "ketch"; //$NON-NLS-1$
+ String email = "ketch@system"; //$NON-NLS-1$
+ return new PersonIdent(name, email);
+ }
+
+ /**
+ * Construct a random tag to identify a candidate during leader election.
+ * <p>
+ * Multiple processes trying to elect themselves leaders at exactly the same
+ * time (rounded to seconds) using the same {@link #newCommitter()} identity
+ * strings, for the same term, may generate the same ObjectId for the
+ * election commit and falsely assume they have both won.
+ * <p>
+ * Candidates add this tag to their election ballot commit to disambiguate
+ * the election. The tag only needs to be unique for a given triplet of
+ * {@link #newCommitter()}, system time (rounded to seconds), and term. If
+ * every replica in the system uses a unique {@code newCommitter} (such as
+ * including the host name after the {@code "@"} in the email address) the
+ * tag could be the empty string.
+ * <p>
+ * The default implementation generates a few bytes of random data.
+ *
+ * @return unique tag; null or empty string if {@code newCommitter()} is
+ * sufficiently unique to identify the leader.
+ */
+ @Nullable
+ public String newLeaderTag() {
+ int n = RNG.nextInt(1 << (6 * 4));
+ return String.format("%06x", Integer.valueOf(n)); //$NON-NLS-1$
+ }
+
+ /**
+ * Construct the KetchLeader instance of a repository.
+ *
+ * @param repo
+ * local repository stored by the leader.
+ * @return leader instance.
+ * @throws URISyntaxException
+ * a follower configuration contains an unsupported URI.
+ */
+ public KetchLeader createLeader(final Repository repo)
+ throws URISyntaxException {
+ KetchLeader leader = new KetchLeader(this) {
+ @Override
+ protected Repository openRepository() {
+ repo.incrementOpen();
+ return repo;
+ }
+ };
+ leader.setReplicas(createReplicas(leader, repo));
+ return leader;
+ }
+
+ /**
+ * Get the collection of replicas for a repository.
+ * <p>
+ * The collection of replicas must include the local repository.
+ *
+ * @param leader
+ * the leader driving these replicas.
+ * @param repo
+ * repository to get the replicas of.
+ * @return collection of replicas for the specified repository.
+ * @throws URISyntaxException
+ * a configured URI is invalid.
+ */
+ protected List<KetchReplica> createReplicas(KetchLeader leader,
+ Repository repo) throws URISyntaxException {
+ List<KetchReplica> replicas = new ArrayList<>();
+ Config cfg = repo.getConfig();
+ String localName = getLocalName(cfg);
+ for (String name : cfg.getSubsections(CONFIG_KEY_REMOTE)) {
+ if (!hasParticipation(cfg, name)) {
+ continue;
+ }
+
+ ReplicaConfig kc = ReplicaConfig.newFromConfig(cfg, name);
+ if (name.equals(localName)) {
+ replicas.add(new LocalReplica(leader, name, kc));
+ continue;
+ }
+
+ RemoteConfig rc = new RemoteConfig(cfg, name);
+ List<URIish> uris = rc.getPushURIs();
+ if (uris.isEmpty()) {
+ uris = rc.getURIs();
+ }
+ for (URIish uri : uris) {
+ String n = uris.size() == 1 ? name : uri.getHost();
+ replicas.add(new RemoteGitReplica(leader, n, uri, kc, rc));
+ }
+ }
+ return replicas;
+ }
+
+ private static boolean hasParticipation(Config cfg, String name) {
+ return cfg.getString(CONFIG_KEY_REMOTE, name, CONFIG_KEY_TYPE) != null;
+ }
+
+ private static String getLocalName(Config cfg) {
+ return cfg.getString(CONFIG_SECTION_KETCH, null, CONFIG_KEY_NAME);
+ }
+
+ static class DefaultExecutorHolder {
+ private static final Logger log = LoggerFactory.getLogger(KetchSystem.class);
+ static final ScheduledExecutorService I = create();
+
+ private static ScheduledExecutorService create() {
+ int cores = Runtime.getRuntime().availableProcessors();
+ int threads = Math.max(5, cores);
+ log.info("Using {} threads", Integer.valueOf(threads)); //$NON-NLS-1$
+ return Executors.newScheduledThreadPool(
+ threads,
+ new ThreadFactory() {
+ private final AtomicInteger threadCnt = new AtomicInteger();
+
+ @Override
+ public Thread newThread(Runnable r) {
+ int id = threadCnt.incrementAndGet();
+ Thread thr = new Thread(r);
+ thr.setName("KetchExecutor-" + id); //$NON-NLS-1$
+ return thr;
+ }
+ });
+ }
+
+ private DefaultExecutorHolder() {
+ }
+ }
+
+ /**
+ * Compute a delay in a {@code min..max} interval with random jitter.
+ *
+ * @param last
+ * amount of delay waited before the last attempt. This is used
+ * to seed the next delay interval. Should be 0 if there was no
+ * prior delay.
+ * @param min
+ * shortest amount of allowable delay between attempts.
+ * @param max
+ * longest amount of allowable delay between attempts.
+ * @return new amount of delay to wait before the next attempt.
+ */
+ static long delay(long last, long min, long max) {
+ long r = Math.max(0, last * 3 - min);
+ if (r > 0) {
+ int c = (int) Math.min(r + 1, Integer.MAX_VALUE);
+ r = RNG.nextInt(c);
+ }
+ return Math.max(Math.min(min + r, max), min);
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchText.java
new file mode 100644
index 0000000000..b6c3bc92c5
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchText.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import org.eclipse.jgit.nls.NLS;
+import org.eclipse.jgit.nls.TranslationBundle;
+
+/** Translation bundle for the Ketch implementation. */
+public class KetchText extends TranslationBundle {
+ /** @return instance of this translation bundle. */
+ public static KetchText get() {
+ return NLS.getBundleFor(KetchText.class);
+ }
+
+ // @formatter:off
+ /***/ public String accepted;
+ /***/ public String cannotFetchFromLocalReplica;
+ /***/ public String failed;
+ /***/ public String invalidFollowerUri;
+ /***/ public String leaderFailedToStore;
+ /***/ public String localReplicaRequired;
+ /***/ public String mismatchedTxnNamespace;
+ /***/ public String outsideTxnNamespace;
+ /***/ public String proposingUpdates;
+ /***/ public String queuedProposalFailedToApply;
+ /***/ public String starting;
+ /***/ public String unsupportedVoterCount;
+ /***/ public String waitingForQueue;
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LagCheck.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LagCheck.java
new file mode 100644
index 0000000000..35327ea0b3
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LagCheck.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import static org.eclipse.jgit.internal.ketch.KetchReplica.State.AHEAD;
+import static org.eclipse.jgit.internal.ketch.KetchReplica.State.DIVERGENT;
+import static org.eclipse.jgit.internal.ketch.KetchReplica.State.LAGGING;
+import static org.eclipse.jgit.internal.ketch.KetchReplica.State.UNKNOWN;
+import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Map;
+
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+/**
+ * A helper to check if a {@link KetchReplica} is ahead or behind the leader.
+ */
+class LagCheck implements AutoCloseable {
+ private final KetchReplica replica;
+ private final Repository repo;
+ private RevWalk rw;
+ private ObjectId remoteId;
+
+ LagCheck(KetchReplica replica, Repository repo) {
+ this.replica = replica;
+ this.repo = repo;
+ initRevWalk();
+ }
+
+ private void initRevWalk() {
+ if (rw != null) {
+ rw.close();
+ }
+
+ rw = new RevWalk(repo);
+ rw.setRetainBody(false);
+ }
+
+ public void close() {
+ if (rw != null) {
+ rw.close();
+ rw = null;
+ }
+ }
+
+ ObjectId getRemoteId() {
+ return remoteId;
+ }
+
+ KetchReplica.State check(ObjectId acceptId, ReceiveCommand acceptCmd) {
+ remoteId = acceptId;
+ if (remoteId == null) {
+ // Nothing advertised by the replica, value is unknown.
+ return UNKNOWN;
+ }
+
+ if (AnyObjectId.equals(remoteId, ObjectId.zeroId())) {
+ // Replica does not have the txnAccepted reference.
+ return LAGGING;
+ }
+
+ try {
+ RevCommit remote;
+ try {
+ remote = parseRemoteCommit(acceptCmd.getRefName());
+ } catch (RefGoneException gone) {
+ // Replica does not have the txnAccepted reference.
+ return LAGGING;
+ } catch (MissingObjectException notFound) {
+ // Local repository does not know this commit so it cannot
+ // be including the replica's log.
+ return DIVERGENT;
+ }
+
+ RevCommit head = rw.parseCommit(acceptCmd.getNewId());
+ if (rw.isMergedInto(remote, head)) {
+ return LAGGING;
+ }
+
+ // TODO(sop) Check term to see if my leader was deposed.
+ if (rw.isMergedInto(head, remote)) {
+ return AHEAD;
+ } else {
+ return DIVERGENT;
+ }
+ } catch (IOException err) {
+ KetchReplica.log.error(String.format(
+ "Cannot compare %s", //$NON-NLS-1$
+ acceptCmd.getRefName()), err);
+ return UNKNOWN;
+ }
+ }
+
+ private RevCommit parseRemoteCommit(String refName)
+ throws IOException, MissingObjectException, RefGoneException {
+ try {
+ return rw.parseCommit(remoteId);
+ } catch (MissingObjectException notLocal) {
+ // Fall through and try to acquire the object by fetching it.
+ }
+
+ ReplicaFetchRequest fetch = new ReplicaFetchRequest(
+ Collections.singleton(refName),
+ Collections.<ObjectId> emptySet());
+ try {
+ replica.blockingFetch(repo, fetch);
+ } catch (IOException fetchErr) {
+ KetchReplica.log.error(String.format(
+ "Cannot fetch %s (%s) from %s", //$NON-NLS-1$
+ remoteId.abbreviate(8).name(), refName,
+ replica.describeForLog()), fetchErr);
+ throw new MissingObjectException(remoteId, OBJ_COMMIT);
+ }
+
+ Map<String, Ref> adv = fetch.getRefs();
+ if (adv == null) {
+ throw new MissingObjectException(remoteId, OBJ_COMMIT);
+ }
+
+ Ref ref = adv.get(refName);
+ if (ref == null || ref.getObjectId() == null) {
+ throw new RefGoneException();
+ }
+
+ initRevWalk();
+ remoteId = ref.getObjectId();
+ return rw.parseCommit(remoteId);
+ }
+
+ private static class RefGoneException extends Exception {
+ private static final long serialVersionUID = 1L;
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LeaderSnapshot.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LeaderSnapshot.java
new file mode 100644
index 0000000000..28a49df97a
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LeaderSnapshot.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import static org.eclipse.jgit.internal.ketch.KetchReplica.State.OFFLINE;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.lib.ObjectId;
+
+/** A snapshot of a leader and its view of the world. */
+public class LeaderSnapshot {
+ final List<ReplicaSnapshot> replicas = new ArrayList<>();
+ KetchLeader.State state;
+ long term;
+ LogIndex headIndex;
+ LogIndex committedIndex;
+ boolean idle;
+
+ LeaderSnapshot() {
+ }
+
+ /** @return unmodifiable view of configured replicas. */
+ public Collection<ReplicaSnapshot> getReplicas() {
+ return Collections.unmodifiableList(replicas);
+ }
+
+ /** @return current state of the leader. */
+ public KetchLeader.State getState() {
+ return state;
+ }
+
+ /**
+ * @return {@code true} if the leader is not running a round to reach
+ * consensus, and has no rounds queued.
+ */
+ public boolean isIdle() {
+ return idle;
+ }
+
+ /**
+ * @return term of this leader. Valid only if {@link #getState()} is
+ * currently {@link KetchLeader.State#LEADER}.
+ */
+ public long getTerm() {
+ return term;
+ }
+
+ /**
+ * @return end of the leader's log; null if leader hasn't started up enough
+ * to begin its own election.
+ */
+ @Nullable
+ public LogIndex getHead() {
+ return headIndex;
+ }
+
+ /**
+ * @return state the leader knows is committed on a majority of participant
+ * replicas. Null until the leader instance has committed a log
+ * index within its own term.
+ */
+ @Nullable
+ public LogIndex getCommitted() {
+ return committedIndex;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder s = new StringBuilder();
+ s.append(isIdle() ? "IDLE" : "RUNNING"); //$NON-NLS-1$ //$NON-NLS-2$
+ s.append(" state ").append(getState()); //$NON-NLS-1$
+ if (getTerm() > 0) {
+ s.append(" term ").append(getTerm()); //$NON-NLS-1$
+ }
+ s.append('\n');
+ s.append(String.format(
+ "%-10s %12s %12s\n", //$NON-NLS-1$
+ "Replica", "Accepted", "Committed")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+ s.append("------------------------------------\n"); //$NON-NLS-1$
+ debug(s, "(leader)", getHead(), getCommitted()); //$NON-NLS-1$
+ s.append('\n');
+ for (ReplicaSnapshot r : getReplicas()) {
+ debug(s, r);
+ s.append('\n');
+ }
+ s.append('\n');
+ return s.toString();
+ }
+
+ private static void debug(StringBuilder b, ReplicaSnapshot s) {
+ KetchReplica replica = s.getReplica();
+ debug(b, replica.getName(), s.getAccepted(), s.getCommitted());
+ b.append(String.format(" %-8s %s", //$NON-NLS-1$
+ replica.getParticipation(), s.getState()));
+ if (s.getState() == OFFLINE) {
+ String err = s.getErrorMessage();
+ if (err != null) {
+ b.append(" (").append(err).append(')'); //$NON-NLS-1$
+ }
+ }
+ }
+
+ private static void debug(StringBuilder s, String name,
+ ObjectId accepted, ObjectId committed) {
+ s.append(String.format(
+ "%-10s %-12s %-12s", //$NON-NLS-1$
+ name, str(accepted), str(committed)));
+ }
+
+ static String str(ObjectId c) {
+ if (c instanceof LogIndex) {
+ return ((LogIndex) c).describeForLog();
+ } else if (c != null) {
+ return c.abbreviate(8).name();
+ }
+ return "-"; //$NON-NLS-1$
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LocalReplica.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LocalReplica.java
new file mode 100644
index 0000000000..e297bca45e
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LocalReplica.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import static org.eclipse.jgit.internal.ketch.KetchReplica.CommitMethod.ALL_REFS;
+import static org.eclipse.jgit.internal.ketch.KetchReplica.CommitMethod.TXN_COMMITTED;
+import static org.eclipse.jgit.lib.RefDatabase.ALL;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
+
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jgit.internal.storage.reftree.RefTreeDatabase;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+/** Ketch replica running on the same system as the {@link KetchLeader}. */
+public class LocalReplica extends KetchReplica {
+ /**
+ * Configure a local replica.
+ *
+ * @param leader
+ * instance this replica follows.
+ * @param name
+ * unique-ish name identifying this replica for debugging.
+ * @param cfg
+ * how Ketch should treat the local system.
+ */
+ public LocalReplica(KetchLeader leader, String name, ReplicaConfig cfg) {
+ super(leader, name, cfg);
+ }
+
+ @Override
+ protected String describeForLog() {
+ return String.format("%s (leader)", getName()); //$NON-NLS-1$
+ }
+
+ /**
+ * Initializes local replica by reading accepted and committed references.
+ * <p>
+ * Loads accepted and committed references from the reference database of
+ * the local replica and stores their current ObjectIds in memory.
+ *
+ * @param repo
+ * repository to initialize state from.
+ * @throws IOException
+ * cannot read repository state.
+ */
+ void initialize(Repository repo) throws IOException {
+ RefDatabase refdb = repo.getRefDatabase();
+ if (refdb instanceof RefTreeDatabase) {
+ RefTreeDatabase treeDb = (RefTreeDatabase) refdb;
+ String txnNamespace = getSystem().getTxnNamespace();
+ if (!txnNamespace.equals(treeDb.getTxnNamespace())) {
+ throw new IOException(MessageFormat.format(
+ KetchText.get().mismatchedTxnNamespace,
+ txnNamespace, treeDb.getTxnNamespace()));
+ }
+ refdb = treeDb.getBootstrap();
+ }
+ initialize(refdb.exactRef(
+ getSystem().getTxnAccepted(),
+ getSystem().getTxnCommitted()));
+ }
+
+ @Override
+ protected void startPush(final ReplicaPushRequest req) {
+ getSystem().getExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ try (Repository git = getLeader().openRepository()) {
+ try {
+ update(git, req);
+ req.done(git);
+ } catch (Throwable err) {
+ req.setException(git, err);
+ }
+ } catch (IOException err) {
+ req.setException(null, err);
+ }
+ }
+ });
+ }
+
+ @Override
+ protected void blockingFetch(Repository repo, ReplicaFetchRequest req)
+ throws IOException {
+ throw new IOException(KetchText.get().cannotFetchFromLocalReplica);
+ }
+
+ private void update(Repository git, ReplicaPushRequest req)
+ throws IOException {
+ RefDatabase refdb = git.getRefDatabase();
+ CommitMethod method = getCommitMethod();
+
+ // Local replica probably uses RefTreeDatabase, the request should
+ // be only for the txnNamespace, so drop to the bootstrap layer.
+ if (refdb instanceof RefTreeDatabase) {
+ if (!isOnlyTxnNamespace(req.getCommands())) {
+ return;
+ }
+
+ refdb = ((RefTreeDatabase) refdb).getBootstrap();
+ method = TXN_COMMITTED;
+ }
+
+ BatchRefUpdate batch = refdb.newBatchUpdate();
+ batch.setRefLogIdent(getSystem().newCommitter());
+ batch.setRefLogMessage("ketch", false); //$NON-NLS-1$
+ batch.setAllowNonFastForwards(true);
+
+ // RefDirectory updates multiple references sequentially.
+ // Run everything else first, then accepted (if present),
+ // then committed (if present). This ensures an earlier
+ // failure will not update these critical references.
+ ReceiveCommand accepted = null;
+ ReceiveCommand committed = null;
+ for (ReceiveCommand cmd : req.getCommands()) {
+ String name = cmd.getRefName();
+ if (name.equals(getSystem().getTxnAccepted())) {
+ accepted = cmd;
+ } else if (name.equals(getSystem().getTxnCommitted())) {
+ committed = cmd;
+ } else {
+ batch.addCommand(cmd);
+ }
+ }
+ if (committed != null && method == ALL_REFS) {
+ Map<String, Ref> refs = refdb.getRefs(ALL);
+ batch.addCommand(prepareCommit(git, refs, committed.getNewId()));
+ }
+ if (accepted != null) {
+ batch.addCommand(accepted);
+ }
+ if (committed != null) {
+ batch.addCommand(committed);
+ }
+
+ try (RevWalk rw = new RevWalk(git)) {
+ batch.execute(rw, NullProgressMonitor.INSTANCE);
+ }
+
+ // KetchReplica only cares about accepted and committed in
+ // advertisement. If they failed, store the current values
+ // back in the ReplicaPushRequest.
+ List<String> failed = new ArrayList<>(2);
+ checkFailed(failed, accepted);
+ checkFailed(failed, committed);
+ if (!failed.isEmpty()) {
+ String[] arr = failed.toArray(new String[failed.size()]);
+ req.setRefs(refdb.exactRef(arr));
+ }
+ }
+
+ private static void checkFailed(List<String> failed, ReceiveCommand cmd) {
+ if (cmd != null && cmd.getResult() != OK) {
+ failed.add(cmd.getRefName());
+ }
+ }
+
+ private boolean isOnlyTxnNamespace(Collection<ReceiveCommand> cmdList) {
+ // Be paranoid and reject non txnNamespace names, this
+ // is a programming error in Ketch that should not occur.
+
+ String txnNamespace = getSystem().getTxnNamespace();
+ for (ReceiveCommand cmd : cmdList) {
+ if (!cmd.getRefName().startsWith(txnNamespace)) {
+ cmd.setResult(REJECTED_OTHER_REASON,
+ MessageFormat.format(
+ KetchText.get().outsideTxnNamespace,
+ cmd.getRefName(), txnNamespace));
+ ReceiveCommand.abort(cmdList);
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LogIndex.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LogIndex.java
new file mode 100644
index 0000000000..350c8ed62e
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LogIndex.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.ObjectId;
+
+/**
+ * An ObjectId for a commit extended with incrementing log index.
+ * <p>
+ * For any two LogIndex instances, {@code A} is an ancestor of {@code C}
+ * reachable through parent edges in the graph if {@code A.index < C.index}.
+ * LogIndex provides a performance optimization for Ketch, the same information
+ * can be obtained from {@link org.eclipse.jgit.revwalk.RevWalk}.
+ * <p>
+ * Index values are only valid within a single {@link KetchLeader} instance
+ * after it has won an election. By restricting scope to a single leader new
+ * leaders do not need to traverse the entire history to determine the next
+ * {@code index} for new proposals. This differs from Raft, where leader
+ * election uses the log index and the term number to determine which replica
+ * holds a sufficiently up-to-date log. Since Ketch uses Git objects for storage
+ * of its replicated log, it keeps the term number as Raft does but uses
+ * standard Git operations to imply the log index.
+ * <p>
+ * {@link Round#runAsync(AnyObjectId)} bumps the index as each new round is
+ * constructed.
+ */
+public class LogIndex extends ObjectId {
+ static LogIndex unknown(AnyObjectId id) {
+ return new LogIndex(id, 0);
+ }
+
+ private final long index;
+
+ private LogIndex(AnyObjectId id, long index) {
+ super(id);
+ this.index = index;
+ }
+
+ LogIndex nextIndex(AnyObjectId id) {
+ return new LogIndex(id, index + 1);
+ }
+
+ /** @return index provided by the current leader instance. */
+ public long getIndex() {
+ return index;
+ }
+
+ /**
+ * Check if this log position committed before another log position.
+ * <p>
+ * Only valid for log positions in memory for the current leader.
+ *
+ * @param c
+ * other (more recent) log position.
+ * @return true if this log position was before {@code c} or equal to c and
+ * therefore any agreement of {@code c} implies agreement on this
+ * log position.
+ */
+ boolean isBefore(LogIndex c) {
+ return index <= c.index;
+ }
+
+ /**
+ * @return string suitable for debug logging containing the log index and
+ * abbreviated ObjectId.
+ */
+ @SuppressWarnings("boxing")
+ public String describeForLog() {
+ return String.format("%5d/%s", index, abbreviate(6).name()); //$NON-NLS-1$
+ }
+
+ @SuppressWarnings("boxing")
+ @Override
+ public String toString() {
+ return String.format("LogId[%5d/%s]", index, name()); //$NON-NLS-1$
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Proposal.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Proposal.java
new file mode 100644
index 0000000000..0876eb5dbd
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Proposal.java
@@ -0,0 +1,391 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import static org.eclipse.jgit.internal.ketch.Proposal.State.ABORTED;
+import static org.eclipse.jgit.internal.ketch.Proposal.State.EXECUTED;
+import static org.eclipse.jgit.internal.ketch.Proposal.State.NEW;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.internal.storage.reftree.Command;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.PushCertificate;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+/**
+ * A proposal to be applied in a Ketch system.
+ * <p>
+ * Pushing to a Ketch leader results in the leader making a proposal. The
+ * proposal includes the list of reference updates. The leader attempts to send
+ * the proposal to a quorum of replicas by pushing the proposal to a "staging"
+ * area under the {@code refs/txn/stage/} namespace. If the proposal succeeds
+ * then the changes are durable and the leader can commit the proposal.
+ * <p>
+ * Proposals are executed by {@link KetchLeader#queueProposal(Proposal)}, which
+ * runs them asynchronously in the background. Proposals are thread-safe futures
+ * allowing callers to {@link #await()} for results or be notified by callback
+ * using {@link #addListener(Runnable)}.
+ */
+public class Proposal {
+ /** Current state of the proposal. */
+ public enum State {
+ /** Proposal has not yet been given to a {@link KetchLeader}. */
+ NEW(false),
+
+ /**
+ * Proposal was validated and has entered the queue, but a round
+ * containing this proposal has not started yet.
+ */
+ QUEUED(false),
+
+ /** Round containing the proposal has begun and is in progress. */
+ RUNNING(false),
+
+ /**
+ * Proposal was executed through a round. Individual results from
+ * {@link Proposal#getCommands()}, {@link Command#getResult()} explain
+ * the success or failure outcome.
+ */
+ EXECUTED(true),
+
+ /** Proposal was aborted and did not reach consensus. */
+ ABORTED(true);
+
+ private final boolean done;
+
+ private State(boolean done) {
+ this.done = done;
+ }
+
+ /** @return true if this is a terminal state. */
+ public boolean isDone() {
+ return done;
+ }
+ }
+
+ private final List<Command> commands;
+ private PersonIdent author;
+ private String message;
+ private PushCertificate pushCert;
+ private final List<Runnable> listeners = new CopyOnWriteArrayList<>();
+ private final AtomicReference<State> state = new AtomicReference<>(NEW);
+
+ /**
+ * Create a proposal from a list of Ketch commands.
+ *
+ * @param cmds
+ * prepared list of commands.
+ */
+ public Proposal(List<Command> cmds) {
+ commands = Collections.unmodifiableList(new ArrayList<>(cmds));
+ }
+
+ /**
+ * Create a proposal from a collection of received commands.
+ *
+ * @param rw
+ * walker to assist in preparing commands.
+ * @param cmds
+ * list of pending commands.
+ * @throws MissingObjectException
+ * newId of a command is not found locally.
+ * @throws IOException
+ * local objects cannot be accessed.
+ */
+ public Proposal(RevWalk rw, Collection<ReceiveCommand> cmds)
+ throws MissingObjectException, IOException {
+ commands = asCommandList(rw, cmds);
+ }
+
+ private static List<Command> asCommandList(RevWalk rw,
+ Collection<ReceiveCommand> cmds)
+ throws MissingObjectException, IOException {
+ List<Command> commands = new ArrayList<>(cmds.size());
+ for (ReceiveCommand cmd : cmds) {
+ commands.add(new Command(rw, cmd));
+ }
+ return Collections.unmodifiableList(commands);
+ }
+
+ /** @return commands from this proposal. */
+ public Collection<Command> getCommands() {
+ return commands;
+ }
+
+ /** @return optional author of the proposal. */
+ @Nullable
+ public PersonIdent getAuthor() {
+ return author;
+ }
+
+ /**
+ * Set the author for the proposal.
+ *
+ * @param who
+ * optional identity of the author of the proposal.
+ * @return {@code this}
+ */
+ public Proposal setAuthor(@Nullable PersonIdent who) {
+ author = who;
+ return this;
+ }
+
+ /** @return optional message for the commit log of the RefTree. */
+ @Nullable
+ public String getMessage() {
+ return message;
+ }
+
+ /**
+ * Set the message to appear in the commit log of the RefTree.
+ *
+ * @param msg
+ * message text for the commit.
+ * @return {@code this}
+ */
+ public Proposal setMessage(@Nullable String msg) {
+ message = msg != null && !msg.isEmpty() ? msg : null;
+ return this;
+ }
+
+ /** @return optional certificate signing the references. */
+ @Nullable
+ public PushCertificate getPushCertificate() {
+ return pushCert;
+ }
+
+ /**
+ * Set the push certificate signing the references.
+ *
+ * @param cert
+ * certificate, may be null.
+ * @return {@code this}
+ */
+ public Proposal setPushCertificate(@Nullable PushCertificate cert) {
+ pushCert = cert;
+ return this;
+ }
+
+ /**
+ * Add a callback to be invoked when the proposal is done.
+ * <p>
+ * A proposal is done when it has entered either {@link State#EXECUTED} or
+ * {@link State#ABORTED} state. If the proposal is already done
+ * {@code callback.run()} is immediately invoked on the caller's thread.
+ *
+ * @param callback
+ * method to run after the proposal is done. The callback may be
+ * run on a Ketch system thread and should be completed quickly.
+ */
+ public void addListener(Runnable callback) {
+ boolean runNow = false;
+ synchronized (state) {
+ if (state.get().isDone()) {
+ runNow = true;
+ } else {
+ listeners.add(callback);
+ }
+ }
+ if (runNow) {
+ callback.run();
+ }
+ }
+
+ /** Set command result as OK. */
+ void success() {
+ for (Command c : commands) {
+ if (c.getResult() == NOT_ATTEMPTED) {
+ c.setResult(OK);
+ }
+ }
+ notifyState(EXECUTED);
+ }
+
+ /** Mark commands as "transaction aborted". */
+ void abort() {
+ Command.abort(commands, null);
+ notifyState(ABORTED);
+ }
+
+ /** @return read the current state of the proposal. */
+ public State getState() {
+ return state.get();
+ }
+
+ /**
+ * @return {@code true} if the proposal was attempted. A true value does not
+ * mean consensus was reached, only that the proposal was considered
+ * and will not be making any more progress beyond its current
+ * state.
+ */
+ public boolean isDone() {
+ return state.get().isDone();
+ }
+
+ /**
+ * Wait for the proposal to be attempted and {@link #isDone()} to be true.
+ *
+ * @throws InterruptedException
+ * caller was interrupted before proposal executed.
+ */
+ public void await() throws InterruptedException {
+ synchronized (state) {
+ while (!state.get().isDone()) {
+ state.wait();
+ }
+ }
+ }
+
+ /**
+ * Wait for the proposal to be attempted and {@link #isDone()} to be true.
+ *
+ * @param wait
+ * how long to wait.
+ * @param unit
+ * unit describing the wait time.
+ * @return true if the proposal is done; false if the method timed out.
+ * @throws InterruptedException
+ * caller was interrupted before proposal executed.
+ */
+ public boolean await(long wait, TimeUnit unit) throws InterruptedException {
+ synchronized (state) {
+ if (state.get().isDone()) {
+ return true;
+ }
+ state.wait(unit.toMillis(wait));
+ return state.get().isDone();
+ }
+ }
+
+ /**
+ * Wait for the proposal to exit a state.
+ *
+ * @param notIn
+ * state the proposal should not be in to return.
+ * @param wait
+ * how long to wait.
+ * @param unit
+ * unit describing the wait time.
+ * @return true if the proposal exited the state; false on time out.
+ * @throws InterruptedException
+ * caller was interrupted before proposal executed.
+ */
+ public boolean awaitStateChange(State notIn, long wait, TimeUnit unit)
+ throws InterruptedException {
+ synchronized (state) {
+ if (state.get() != notIn) {
+ return true;
+ }
+ state.wait(unit.toMillis(wait));
+ return state.get() != notIn;
+ }
+ }
+
+ void notifyState(State s) {
+ synchronized (state) {
+ state.set(s);
+ state.notifyAll();
+ }
+ if (s.isDone()) {
+ for (Runnable callback : listeners) {
+ callback.run();
+ }
+ listeners.clear();
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder s = new StringBuilder();
+ s.append("Ketch Proposal {\n"); //$NON-NLS-1$
+ s.append(" ").append(state.get()).append('\n'); //$NON-NLS-1$
+ if (author != null) {
+ s.append(" author ").append(author).append('\n'); //$NON-NLS-1$
+ }
+ if (message != null) {
+ s.append(" message ").append(message).append('\n'); //$NON-NLS-1$
+ }
+ for (Command c : commands) {
+ s.append(" "); //$NON-NLS-1$
+ format(s, c.getOldRef(), "CREATE"); //$NON-NLS-1$
+ s.append(' ');
+ format(s, c.getNewRef(), "DELETE"); //$NON-NLS-1$
+ s.append(' ').append(c.getRefName());
+ if (c.getResult() != ReceiveCommand.Result.NOT_ATTEMPTED) {
+ s.append(' ').append(c.getResult()); // $NON-NLS-1$
+ }
+ s.append('\n');
+ }
+ s.append('}');
+ return s.toString();
+ }
+
+ private static void format(StringBuilder s, @Nullable Ref r, String n) {
+ if (r == null) {
+ s.append(n);
+ } else if (r.isSymbolic()) {
+ s.append(r.getTarget().getName());
+ } else {
+ ObjectId id = r.getObjectId();
+ if (id != null) {
+ s.append(id.abbreviate(8).name());
+ }
+ }
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ProposalRound.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ProposalRound.java
new file mode 100644
index 0000000000..d34477ab26
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ProposalRound.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import static org.eclipse.jgit.internal.ketch.Proposal.State.RUNNING;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.internal.storage.reftree.Command;
+import org.eclipse.jgit.internal.storage.reftree.RefTree;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+/** A {@link Round} that aggregates and sends user {@link Proposal}s. */
+class ProposalRound extends Round {
+ private final List<Proposal> todo;
+ private RefTree queuedTree;
+
+ ProposalRound(KetchLeader leader, LogIndex head, List<Proposal> todo,
+ @Nullable RefTree tree) {
+ super(leader, head);
+ this.todo = todo;
+
+ if (tree != null && canCombine(todo)) {
+ this.queuedTree = tree;
+ } else {
+ leader.roundHoldsReferenceToRefTree = false;
+ }
+ }
+
+ private static boolean canCombine(List<Proposal> todo) {
+ Proposal first = todo.get(0);
+ for (int i = 1; i < todo.size(); i++) {
+ if (!canCombine(first, todo.get(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static boolean canCombine(Proposal a, Proposal b) {
+ String aMsg = nullToEmpty(a.getMessage());
+ String bMsg = nullToEmpty(b.getMessage());
+ return aMsg.equals(bMsg) && canCombine(a.getAuthor(), b.getAuthor());
+ }
+
+ private static String nullToEmpty(@Nullable String str) {
+ return str != null ? str : ""; //$NON-NLS-1$
+ }
+
+ private static boolean canCombine(@Nullable PersonIdent a,
+ @Nullable PersonIdent b) {
+ if (a != null && b != null) {
+ // Same name and email address. Combine timestamp as the two
+ // proposals are running concurrently and appear together or
+ // not at all from the point of view of an outside reader.
+ return a.getName().equals(b.getName())
+ && a.getEmailAddress().equals(b.getEmailAddress());
+ }
+
+ // If a and b are null, both will be the system identity.
+ return a == null && b == null;
+ }
+
+ void start() throws IOException {
+ for (Proposal p : todo) {
+ p.notifyState(RUNNING);
+ }
+ try {
+ ObjectId id;
+ try (Repository git = leader.openRepository()) {
+ id = insertProposals(git);
+ }
+ runAsync(id);
+ } catch (NoOp e) {
+ for (Proposal p : todo) {
+ p.success();
+ }
+ leader.lock.lock();
+ try {
+ leader.nextRound();
+ } finally {
+ leader.lock.unlock();
+ }
+ } catch (IOException e) {
+ abort();
+ throw e;
+ }
+ }
+
+ private ObjectId insertProposals(Repository git)
+ throws IOException, NoOp {
+ ObjectId id;
+ try (ObjectInserter inserter = git.newObjectInserter()) {
+ // TODO(sop) Process signed push certificates.
+
+ if (queuedTree != null) {
+ id = insertSingleProposal(git, inserter);
+ } else {
+ id = insertMultiProposal(git, inserter);
+ }
+
+ stageCommands = makeStageList(git, inserter);
+ inserter.flush();
+ }
+ return id;
+ }
+
+ private ObjectId insertSingleProposal(Repository git,
+ ObjectInserter inserter) throws IOException, NoOp {
+ // Fast path: tree is passed in with all proposals applied.
+ ObjectId treeId = queuedTree.writeTree(inserter);
+ queuedTree = null;
+ leader.roundHoldsReferenceToRefTree = false;
+
+ if (!ObjectId.zeroId().equals(acceptedOldIndex)) {
+ try (RevWalk rw = new RevWalk(git)) {
+ RevCommit c = rw.parseCommit(acceptedOldIndex);
+ if (treeId.equals(c.getTree())) {
+ throw new NoOp();
+ }
+ }
+ }
+
+ Proposal p = todo.get(0);
+ CommitBuilder b = new CommitBuilder();
+ b.setTreeId(treeId);
+ if (!ObjectId.zeroId().equals(acceptedOldIndex)) {
+ b.setParentId(acceptedOldIndex);
+ }
+ b.setCommitter(leader.getSystem().newCommitter());
+ b.setAuthor(p.getAuthor() != null ? p.getAuthor() : b.getCommitter());
+ b.setMessage(message(p));
+ return inserter.insert(b);
+ }
+
+ private ObjectId insertMultiProposal(Repository git,
+ ObjectInserter inserter) throws IOException, NoOp {
+ // The tree was not passed in, or there are multiple proposals
+ // each needing their own commit. Reset the tree and replay each
+ // proposal in order as individual commits.
+ ObjectId lastIndex = acceptedOldIndex;
+ ObjectId oldTreeId;
+ RefTree tree;
+ if (ObjectId.zeroId().equals(lastIndex)) {
+ oldTreeId = ObjectId.zeroId();
+ tree = RefTree.newEmptyTree();
+ } else {
+ try (RevWalk rw = new RevWalk(git)) {
+ RevCommit c = rw.parseCommit(lastIndex);
+ oldTreeId = c.getTree();
+ tree = RefTree.read(rw.getObjectReader(), c.getTree());
+ }
+ }
+
+ PersonIdent committer = leader.getSystem().newCommitter();
+ for (Proposal p : todo) {
+ if (!tree.apply(p.getCommands())) {
+ // This should not occur, previously during queuing the
+ // commands were successfully applied to the pending tree.
+ // Abort the entire round.
+ throw new IOException(
+ KetchText.get().queuedProposalFailedToApply);
+ }
+
+ ObjectId treeId = tree.writeTree(inserter);
+ if (treeId.equals(oldTreeId)) {
+ continue;
+ }
+
+ CommitBuilder b = new CommitBuilder();
+ b.setTreeId(treeId);
+ if (!ObjectId.zeroId().equals(lastIndex)) {
+ b.setParentId(lastIndex);
+ }
+ b.setAuthor(p.getAuthor() != null ? p.getAuthor() : committer);
+ b.setCommitter(committer);
+ b.setMessage(message(p));
+ lastIndex = inserter.insert(b);
+ }
+ if (lastIndex.equals(acceptedOldIndex)) {
+ throw new NoOp();
+ }
+ return lastIndex;
+ }
+
+ private String message(Proposal p) {
+ StringBuilder m = new StringBuilder();
+ String msg = p.getMessage();
+ if (msg != null && !msg.isEmpty()) {
+ m.append(msg);
+ while (m.length() < 2 || m.charAt(m.length() - 2) != '\n'
+ || m.charAt(m.length() - 1) != '\n') {
+ m.append('\n');
+ }
+ }
+ m.append(KetchConstants.TERM.getName())
+ .append(": ") //$NON-NLS-1$
+ .append(leader.getTerm());
+ return m.toString();
+ }
+
+ void abort() {
+ for (Proposal p : todo) {
+ p.abort();
+ }
+ }
+
+ void success() {
+ for (Proposal p : todo) {
+ p.success();
+ }
+ }
+
+ private List<ReceiveCommand> makeStageList(Repository git,
+ ObjectInserter inserter) throws IOException {
+ // For each branch, collapse consecutive updates to only most recent,
+ // avoiding sending multiple objects in a rapid fast-forward chain, or
+ // rewritten content.
+ Map<String, ObjectId> byRef = new HashMap<>();
+ for (Proposal p : todo) {
+ for (Command c : p.getCommands()) {
+ Ref n = c.getNewRef();
+ if (n != null && !n.isSymbolic()) {
+ byRef.put(n.getName(), n.getObjectId());
+ }
+ }
+ }
+ if (byRef.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ Set<ObjectId> newObjs = new HashSet<>(byRef.values());
+ StageBuilder b = new StageBuilder(
+ leader.getSystem().getTxnStage(),
+ acceptedNewIndex);
+ return b.makeStageList(newObjs, git, inserter);
+ }
+
+
+ private static class NoOp extends Exception {
+ private static final long serialVersionUID = 1L;
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/RemoteGitReplica.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/RemoteGitReplica.java
new file mode 100644
index 0000000000..6f4a178673
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/RemoteGitReplica.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import static org.eclipse.jgit.internal.ketch.KetchReplica.CommitMethod.ALL_REFS;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.LOCK_FAILURE;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_NODELETE;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_NONFASTFORWARD;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
+import static org.eclipse.jgit.lib.Ref.Storage.NETWORK;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.errors.NotSupportedException;
+import org.eclipse.jgit.errors.TransportException;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.FetchConnection;
+import org.eclipse.jgit.transport.PushConnection;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.transport.RemoteConfig;
+import org.eclipse.jgit.transport.RemoteRefUpdate;
+import org.eclipse.jgit.transport.Transport;
+import org.eclipse.jgit.transport.URIish;
+
+/**
+ * Representation of a Git repository on a remote replica system.
+ * <p>
+ * {@link KetchLeader} will contact the replica using the Git wire protocol.
+ * <p>
+ * The remote replica may be fully Ketch-aware, or a standard Git server.
+ */
+public class RemoteGitReplica extends KetchReplica {
+ private final URIish uri;
+ private final RemoteConfig remoteConfig;
+
+ /**
+ * Configure a new remote.
+ *
+ * @param leader
+ * instance this replica follows.
+ * @param name
+ * unique-ish name identifying this remote for debugging.
+ * @param uri
+ * URI to connect to the follower's repository.
+ * @param cfg
+ * how Ketch should treat the remote system.
+ * @param rc
+ * optional remote configuration describing how to contact the
+ * peer repository.
+ */
+ public RemoteGitReplica(KetchLeader leader, String name, URIish uri,
+ ReplicaConfig cfg, @Nullable RemoteConfig rc) {
+ super(leader, name, cfg);
+ this.uri = uri;
+ this.remoteConfig = rc;
+ }
+
+ /** @return URI to contact the remote peer repository. */
+ public URIish getURI() {
+ return uri;
+ }
+
+ /** @return optional configuration describing how to contact the peer. */
+ @Nullable
+ protected RemoteConfig getRemoteConfig() {
+ return remoteConfig;
+ }
+
+ @Override
+ protected String describeForLog() {
+ return String.format("%s @ %s", getName(), getURI()); //$NON-NLS-1$
+ }
+
+ @Override
+ protected void startPush(final ReplicaPushRequest req) {
+ getSystem().getExecutor().execute(new Runnable() {
+ @Override
+ public void run() {
+ try (Repository git = getLeader().openRepository()) {
+ try {
+ push(git, req);
+ req.done(git);
+ } catch (Throwable err) {
+ req.setException(git, err);
+ }
+ } catch (IOException err) {
+ req.setException(null, err);
+ }
+ }
+ });
+ }
+
+ private void push(Repository repo, ReplicaPushRequest req)
+ throws NotSupportedException, TransportException, IOException {
+ Map<String, Ref> adv;
+ List<RemoteCommand> cmds = asUpdateList(req.getCommands());
+ try (Transport transport = Transport.open(repo, uri)) {
+ RemoteConfig rc = getRemoteConfig();
+ if (rc != null) {
+ transport.applyConfig(rc);
+ }
+ transport.setPushAtomic(true);
+ adv = push(repo, transport, cmds);
+ }
+ for (RemoteCommand c : cmds) {
+ c.copyStatusToResult();
+ }
+ req.setRefs(adv);
+ }
+
+ private Map<String, Ref> push(Repository git, Transport transport,
+ List<RemoteCommand> cmds) throws IOException {
+ Map<String, RemoteRefUpdate> updates = asUpdateMap(cmds);
+ try (PushConnection connection = transport.openPush()) {
+ Map<String, Ref> adv = connection.getRefsMap();
+ RemoteRefUpdate accepted = updates.get(getSystem().getTxnAccepted());
+ if (accepted != null && !isExpectedValue(adv, accepted)) {
+ abort(cmds);
+ return adv;
+ }
+
+ RemoteRefUpdate committed = updates.get(getSystem().getTxnCommitted());
+ if (committed != null && !isExpectedValue(adv, committed)) {
+ abort(cmds);
+ return adv;
+ }
+ if (committed != null && getCommitMethod() == ALL_REFS) {
+ prepareCommit(git, cmds, updates, adv,
+ committed.getNewObjectId());
+ }
+
+ connection.push(NullProgressMonitor.INSTANCE, updates);
+ return adv;
+ }
+ }
+
+ private static boolean isExpectedValue(Map<String, Ref> adv,
+ RemoteRefUpdate u) {
+ Ref r = adv.get(u.getRemoteName());
+ if (!AnyObjectId.equals(getId(r), u.getExpectedOldObjectId())) {
+ ((RemoteCommand) u).cmd.setResult(LOCK_FAILURE);
+ return false;
+ }
+ return true;
+ }
+
+ private void prepareCommit(Repository git, List<RemoteCommand> cmds,
+ Map<String, RemoteRefUpdate> updates, Map<String, Ref> adv,
+ ObjectId committed) throws IOException {
+ for (ReceiveCommand cmd : prepareCommit(git, adv, committed)) {
+ RemoteCommand c = new RemoteCommand(cmd);
+ cmds.add(c);
+ updates.put(c.getRemoteName(), c);
+ }
+ }
+
+ private static List<RemoteCommand> asUpdateList(
+ Collection<ReceiveCommand> cmds) {
+ try {
+ List<RemoteCommand> toPush = new ArrayList<>(cmds.size());
+ for (ReceiveCommand cmd : cmds) {
+ toPush.add(new RemoteCommand(cmd));
+ }
+ return toPush;
+ } catch (IOException e) {
+ // Cannot occur as no IO was required to build the command.
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private static Map<String, RemoteRefUpdate> asUpdateMap(
+ List<RemoteCommand> cmds) {
+ Map<String, RemoteRefUpdate> m = new LinkedHashMap<>();
+ for (RemoteCommand cmd : cmds) {
+ m.put(cmd.getRemoteName(), cmd);
+ }
+ return m;
+ }
+
+ private static void abort(List<RemoteCommand> cmds) {
+ List<ReceiveCommand> tmp = new ArrayList<>(cmds.size());
+ for (RemoteCommand cmd : cmds) {
+ tmp.add(cmd.cmd);
+ }
+ ReceiveCommand.abort(tmp);
+ }
+
+ protected void blockingFetch(Repository repo, ReplicaFetchRequest req)
+ throws NotSupportedException, TransportException {
+ try (Transport transport = Transport.open(repo, uri)) {
+ RemoteConfig rc = getRemoteConfig();
+ if (rc != null) {
+ transport.applyConfig(rc);
+ }
+ fetch(transport, req);
+ }
+ }
+
+ private void fetch(Transport transport, ReplicaFetchRequest req)
+ throws NotSupportedException, TransportException {
+ try (FetchConnection conn = transport.openFetch()) {
+ Map<String, Ref> remoteRefs = conn.getRefsMap();
+ req.setRefs(remoteRefs);
+
+ List<Ref> want = new ArrayList<>();
+ for (String name : req.getWantRefs()) {
+ Ref ref = remoteRefs.get(name);
+ if (ref != null && ref.getObjectId() != null) {
+ want.add(ref);
+ }
+ }
+ for (ObjectId id : req.getWantObjects()) {
+ want.add(new ObjectIdRef.Unpeeled(NETWORK, id.name(), id));
+ }
+
+ conn.fetch(NullProgressMonitor.INSTANCE, want,
+ Collections.<ObjectId> emptySet());
+ }
+ }
+
+ static class RemoteCommand extends RemoteRefUpdate {
+ final ReceiveCommand cmd;
+
+ RemoteCommand(ReceiveCommand cmd) throws IOException {
+ super(null, null,
+ cmd.getNewId(), cmd.getRefName(),
+ true /* force update */,
+ null /* no local tracking ref */,
+ cmd.getOldId());
+ this.cmd = cmd;
+ }
+
+ void copyStatusToResult() {
+ if (cmd.getResult() == NOT_ATTEMPTED) {
+ switch (getStatus()) {
+ case OK:
+ case UP_TO_DATE:
+ case NON_EXISTING:
+ cmd.setResult(OK);
+ break;
+
+ case REJECTED_NODELETE:
+ cmd.setResult(REJECTED_NODELETE);
+ break;
+
+ case REJECTED_NONFASTFORWARD:
+ cmd.setResult(REJECTED_NONFASTFORWARD);
+ break;
+
+ case REJECTED_OTHER_REASON:
+ cmd.setResult(REJECTED_OTHER_REASON, getMessage());
+ break;
+
+ default:
+ cmd.setResult(REJECTED_OTHER_REASON, getStatus().name());
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaConfig.java
new file mode 100644
index 0000000000..e16e63aa7e
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaConfig.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import static java.util.concurrent.TimeUnit.DAYS;
+import static java.util.concurrent.TimeUnit.HOURS;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.eclipse.jgit.internal.ketch.KetchConstants.CONFIG_KEY_COMMIT;
+import static org.eclipse.jgit.internal.ketch.KetchConstants.CONFIG_KEY_SPEED;
+import static org.eclipse.jgit.internal.ketch.KetchConstants.CONFIG_KEY_TYPE;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_REMOTE;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.jgit.internal.ketch.KetchReplica.CommitMethod;
+import org.eclipse.jgit.internal.ketch.KetchReplica.CommitSpeed;
+import org.eclipse.jgit.internal.ketch.KetchReplica.Participation;
+import org.eclipse.jgit.lib.Config;
+
+/** Configures a {@link KetchReplica}. */
+public class ReplicaConfig {
+ /**
+ * Read a configuration from a config block.
+ *
+ * @param cfg
+ * configuration to read.
+ * @param name
+ * of the replica being configured.
+ * @return replica configuration for {@code name}.
+ */
+ public static ReplicaConfig newFromConfig(Config cfg, String name) {
+ return new ReplicaConfig().fromConfig(cfg, name);
+ }
+
+ private Participation participation = Participation.FULL;
+ private CommitMethod commitMethod = CommitMethod.ALL_REFS;
+ private CommitSpeed commitSpeed = CommitSpeed.BATCHED;
+ private long minRetry = SECONDS.toMillis(5);
+ private long maxRetry = MINUTES.toMillis(1);
+
+ /** @return participation of the replica in the system. */
+ public Participation getParticipation() {
+ return participation;
+ }
+
+ /** @return how Ketch should apply committed changes. */
+ public CommitMethod getCommitMethod() {
+ return commitMethod;
+ }
+
+ /** @return how quickly should Ketch commit. */
+ public CommitSpeed getCommitSpeed() {
+ return commitSpeed;
+ }
+
+ /**
+ * Returns the minimum wait delay before retrying a failure.
+ *
+ * @param unit
+ * to get retry delay in.
+ * @return minimum delay before retrying a failure.
+ */
+ public long getMinRetry(TimeUnit unit) {
+ return unit.convert(minRetry, MILLISECONDS);
+ }
+
+ /**
+ * Returns the maximum wait delay before retrying a failure.
+ *
+ * @param unit
+ * to get retry delay in.
+ * @return maximum delay before retrying a failure.
+ */
+ public long getMaxRetry(TimeUnit unit) {
+ return unit.convert(maxRetry, MILLISECONDS);
+ }
+
+ /**
+ * Update the configuration from a config block.
+ *
+ * @param cfg
+ * configuration to read.
+ * @param name
+ * of the replica being configured.
+ * @return {@code this}
+ */
+ public ReplicaConfig fromConfig(Config cfg, String name) {
+ participation = cfg.getEnum(
+ CONFIG_KEY_REMOTE, name, CONFIG_KEY_TYPE,
+ participation);
+ commitMethod = cfg.getEnum(
+ CONFIG_KEY_REMOTE, name, CONFIG_KEY_COMMIT,
+ commitMethod);
+ commitSpeed = cfg.getEnum(
+ CONFIG_KEY_REMOTE, name, CONFIG_KEY_SPEED,
+ commitSpeed);
+ minRetry = getMillis(cfg, name, "ketch-minRetry", minRetry); //$NON-NLS-1$
+ maxRetry = getMillis(cfg, name, "ketch-maxRetry", maxRetry); //$NON-NLS-1$
+ return this;
+ }
+
+ private static long getMillis(Config cfg, String name, String key,
+ long defaultValue) {
+ String valStr = cfg.getString(CONFIG_KEY_REMOTE, name, key);
+ if (valStr == null) {
+ return defaultValue;
+ }
+
+ valStr = valStr.trim();
+ if (valStr.isEmpty()) {
+ return defaultValue;
+ }
+
+ Matcher m = UnitMap.PATTERN.matcher(valStr);
+ if (!m.matches()) {
+ return defaultValue;
+ }
+
+ String digits = m.group(1);
+ String unitName = m.group(2).trim();
+ TimeUnit unit = UnitMap.UNITS.get(unitName);
+ if (unit == null) {
+ return defaultValue;
+ }
+
+ try {
+ if (digits.indexOf('.') == -1) {
+ return unit.toMillis(Long.parseLong(digits));
+ }
+
+ double val = Double.parseDouble(digits);
+ return (long) (val * unit.toMillis(1));
+ } catch (NumberFormatException nfe) {
+ return defaultValue;
+ }
+ }
+
+ static class UnitMap {
+ static final Pattern PATTERN = Pattern
+ .compile("^([1-9][0-9]*(?:\\.[0-9]*)?)\\s*(.*)$"); //$NON-NLS-1$
+
+ static final Map<String, TimeUnit> UNITS;
+
+ static {
+ Map<String, TimeUnit> m = new HashMap<>();
+ TimeUnit u = MILLISECONDS;
+ m.put("", u); //$NON-NLS-1$
+ m.put("ms", u); //$NON-NLS-1$
+ m.put("millis", u); //$NON-NLS-1$
+ m.put("millisecond", u); //$NON-NLS-1$
+ m.put("milliseconds", u); //$NON-NLS-1$
+
+ u = SECONDS;
+ m.put("s", u); //$NON-NLS-1$
+ m.put("sec", u); //$NON-NLS-1$
+ m.put("secs", u); //$NON-NLS-1$
+ m.put("second", u); //$NON-NLS-1$
+ m.put("seconds", u); //$NON-NLS-1$
+
+ u = MINUTES;
+ m.put("m", u); //$NON-NLS-1$
+ m.put("min", u); //$NON-NLS-1$
+ m.put("mins", u); //$NON-NLS-1$
+ m.put("minute", u); //$NON-NLS-1$
+ m.put("minutes", u); //$NON-NLS-1$
+
+ u = HOURS;
+ m.put("h", u); //$NON-NLS-1$
+ m.put("hr", u); //$NON-NLS-1$
+ m.put("hrs", u); //$NON-NLS-1$
+ m.put("hour", u); //$NON-NLS-1$
+ m.put("hours", u); //$NON-NLS-1$
+
+ u = DAYS;
+ m.put("d", u); //$NON-NLS-1$
+ m.put("day", u); //$NON-NLS-1$
+ m.put("days", u); //$NON-NLS-1$
+
+ UNITS = Collections.unmodifiableMap(m);
+ }
+
+ private UnitMap() {
+ }
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaFetchRequest.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaFetchRequest.java
new file mode 100644
index 0000000000..201d9e9743
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaFetchRequest.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+
+/** A fetch request to obtain objects from a replica, and its result. */
+public class ReplicaFetchRequest {
+ private final Set<String> wantRefs;
+ private final Set<ObjectId> wantObjects;
+ private Map<String, Ref> refs;
+
+ /**
+ * Construct a new fetch request for a replica.
+ *
+ * @param wantRefs
+ * named references to be fetched.
+ * @param wantObjects
+ * specific objects to be fetched.
+ */
+ public ReplicaFetchRequest(Set<String> wantRefs,
+ Set<ObjectId> wantObjects) {
+ this.wantRefs = wantRefs;
+ this.wantObjects = wantObjects;
+ }
+
+ /** @return references to be fetched. */
+ public Set<String> getWantRefs() {
+ return wantRefs;
+ }
+
+ /** @return objects to be fetched. */
+ public Set<ObjectId> getWantObjects() {
+ return wantObjects;
+ }
+
+ /** @return remote references, usually from the advertisement. */
+ @Nullable
+ public Map<String, Ref> getRefs() {
+ return refs;
+ }
+
+ /**
+ * @param refs
+ * references observed from the replica.
+ */
+ public void setRefs(Map<String, Ref> refs) {
+ this.refs = refs;
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaPushRequest.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaPushRequest.java
new file mode 100644
index 0000000000..691b1424f4
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaPushRequest.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import java.util.Collection;
+import java.util.Map;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+/**
+ * A push request sending objects to a replica, and its result.
+ * <p>
+ * Implementors of {@link KetchReplica} must populate the command result fields,
+ * {@link #setRefs(Map)}, and call one of
+ * {@link #setException(Repository, Throwable)} or {@link #done(Repository)} to
+ * finish processing.
+ */
+public class ReplicaPushRequest {
+ private final KetchReplica replica;
+ private final Collection<ReceiveCommand> commands;
+ private Map<String, Ref> refs;
+ private Throwable exception;
+ private boolean notified;
+
+ /**
+ * Construct a new push request for a replica.
+ *
+ * @param replica
+ * the replica being pushed to.
+ * @param commands
+ * commands to be executed.
+ */
+ public ReplicaPushRequest(KetchReplica replica,
+ Collection<ReceiveCommand> commands) {
+ this.replica = replica;
+ this.commands = commands;
+ }
+
+ /** @return commands to be executed, and their results. */
+ public Collection<ReceiveCommand> getCommands() {
+ return commands;
+ }
+
+ /** @return remote references, usually from the advertisement. */
+ @Nullable
+ public Map<String, Ref> getRefs() {
+ return refs;
+ }
+
+ /**
+ * @param refs
+ * references observed from the replica.
+ */
+ public void setRefs(Map<String, Ref> refs) {
+ this.refs = refs;
+ }
+
+ /** @return exception thrown, if any. */
+ @Nullable
+ public Throwable getException() {
+ return exception;
+ }
+
+ /**
+ * Mark the request as crashing with a communication error.
+ * <p>
+ * This method may take significant time acquiring the leader lock and
+ * updating the Ketch state machine with the failure.
+ *
+ * @param repo
+ * local repository reference used by the push attempt.
+ * @param err
+ * exception thrown during communication.
+ */
+ public void setException(@Nullable Repository repo, Throwable err) {
+ if (KetchReplica.log.isErrorEnabled()) {
+ KetchReplica.log.error(describe("failed"), err); //$NON-NLS-1$
+ }
+ if (!notified) {
+ notified = true;
+ exception = err;
+ replica.afterPush(repo, this);
+ }
+ }
+
+ /**
+ * Mark the request as completed without exception.
+ * <p>
+ * This method may take significant time acquiring the leader lock and
+ * updating the Ketch state machine with results from this replica.
+ *
+ * @param repo
+ * local repository reference used by the push attempt.
+ */
+ public void done(Repository repo) {
+ if (KetchReplica.log.isDebugEnabled()) {
+ KetchReplica.log.debug(describe("completed")); //$NON-NLS-1$
+ }
+ if (!notified) {
+ notified = true;
+ replica.afterPush(repo, this);
+ }
+ }
+
+ private String describe(String heading) {
+ StringBuilder b = new StringBuilder();
+ b.append("push to "); //$NON-NLS-1$
+ b.append(replica.describeForLog());
+ b.append(' ').append(heading).append(":\n"); //$NON-NLS-1$
+ for (ReceiveCommand cmd : commands) {
+ b.append(String.format(
+ " %-12s %-12s %s %s", //$NON-NLS-1$
+ LeaderSnapshot.str(cmd.getOldId()),
+ LeaderSnapshot.str(cmd.getNewId()),
+ cmd.getRefName(),
+ cmd.getResult()));
+ if (cmd.getMessage() != null) {
+ b.append(' ').append(cmd.getMessage());
+ }
+ b.append('\n');
+ }
+ return b.toString();
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaSnapshot.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaSnapshot.java
new file mode 100644
index 0000000000..8c3de027d2
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaSnapshot.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import java.util.Date;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.lib.ObjectId;
+
+/**
+ * A snapshot of a replica.
+ *
+ * @see LeaderSnapshot
+ */
+public class ReplicaSnapshot {
+ final KetchReplica replica;
+ ObjectId accepted;
+ ObjectId committed;
+ KetchReplica.State state;
+ String error;
+ long retryAtMillis;
+
+ ReplicaSnapshot(KetchReplica replica) {
+ this.replica = replica;
+ }
+
+ /** @return the replica this snapshot describes the state of. */
+ public KetchReplica getReplica() {
+ return replica;
+ }
+
+ /** @return current state of the replica. */
+ public KetchReplica.State getState() {
+ return state;
+ }
+
+ /** @return last known Git commit at {@code refs/txn/accepted}. */
+ @Nullable
+ public ObjectId getAccepted() {
+ return accepted;
+ }
+
+ /** @return last known Git commit at {@code refs/txn/committed}. */
+ @Nullable
+ public ObjectId getCommitted() {
+ return committed;
+ }
+
+ /**
+ * @return if {@link #getState()} == {@link KetchReplica.State#OFFLINE} an
+ * optional human-readable message from the transport system
+ * explaining the failure.
+ */
+ @Nullable
+ public String getErrorMessage() {
+ return error;
+ }
+
+ /**
+ * @return time (usually in the future) when the leader will retry
+ * communication with the offline or lagging replica; null if no
+ * retry is scheduled or necessary.
+ */
+ @Nullable
+ public Date getRetryAt() {
+ return retryAtMillis > 0 ? new Date(retryAtMillis) : null;
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Round.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Round.java
new file mode 100644
index 0000000000..1335b85cca
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Round.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import java.io.IOException;
+import java.util.List;
+
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+/**
+ * One round-trip to all replicas proposing a log entry.
+ * <p>
+ * In Raft a log entry represents a state transition at a specific index in the
+ * replicated log. The leader can only append log entries to the log.
+ * <p>
+ * In Ketch a log entry is recorded under the {@code refs/txn} namespace. This
+ * occurs when:
+ * <ul>
+ * <li>a replica wants to establish itself as a new leader by proposing a new
+ * term (see {@link ElectionRound})
+ * <li>an established leader wants to gain consensus on new {@link Proposal}s
+ * (see {@link ProposalRound})
+ * </ul>
+ */
+abstract class Round {
+ final KetchLeader leader;
+ final LogIndex acceptedOldIndex;
+ LogIndex acceptedNewIndex;
+ List<ReceiveCommand> stageCommands;
+
+ Round(KetchLeader leader, LogIndex head) {
+ this.leader = leader;
+ this.acceptedOldIndex = head;
+ }
+
+ /**
+ * Creates a commit for {@code refs/txn/accepted} and calls
+ * {@link #runAsync(AnyObjectId)} to begin execution of the round across
+ * the system.
+ * <p>
+ * If references are being updated (such as in a {@link ProposalRound}) the
+ * RefTree may be modified.
+ * <p>
+ * Invoked without {@link KetchLeader#lock} to build objects.
+ *
+ * @throws IOException
+ * the round cannot build new objects within the leader's
+ * repository. The leader may be unable to execute.
+ */
+ abstract void start() throws IOException;
+
+ /**
+ * Asynchronously distribute the round's new value for
+ * {@code refs/txn/accepted} to all replicas.
+ * <p>
+ * Invoked by {@link #start()} after new commits have been created for the
+ * log. The method passes {@code newId} to {@link KetchLeader} to be
+ * distributed to all known replicas.
+ *
+ * @param newId
+ * new value for {@code refs/txn/accepted}.
+ */
+ void runAsync(AnyObjectId newId) {
+ acceptedNewIndex = acceptedOldIndex.nextIndex(newId);
+ leader.runAsync(this);
+ }
+
+ /**
+ * Notify the round it was accepted by a majority of the system.
+ * <p>
+ * Invoked by the leader with {@link KetchLeader#lock} held by the caller.
+ */
+ abstract void success();
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/StageBuilder.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/StageBuilder.java
new file mode 100644
index 0000000000..61871a494f
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/StageBuilder.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.ketch;
+
+import static org.eclipse.jgit.lib.FileMode.TYPE_GITLINK;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.treewalk.EmptyTreeIterator;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.filter.TreeFilter;
+
+/** Constructs a set of commands to stage content during a proposal. */
+public class StageBuilder {
+ /**
+ * Acceptable number of references to send in a single stage transaction.
+ * <p>
+ * If the number of unique objects exceeds this amount the builder will
+ * attempt to decrease the reference count by chaining commits..
+ */
+ private static final int SMALL_BATCH_SIZE = 5;
+
+ /**
+ * Acceptable number of commits to chain together using parent pointers.
+ * <p>
+ * When staging many unique commits the {@link StageBuilder} batches
+ * together unrelated commits as parents of a temporary commit. After the
+ * proposal completes the temporary commit is discarded and can be garbage
+ * collected by all replicas.
+ */
+ private static final int TEMP_PARENT_BATCH_SIZE = 128;
+
+ private static final byte[] PEEL = { ' ', '^' };
+
+ private final String txnStage;
+ private final String txnId;
+
+ /**
+ * Construct a stage builder for a transaction.
+ *
+ * @param txnStageNamespace
+ * namespace for transaction references to build
+ * {@code "txnStageNamespace/txnId.n"} style names.
+ * @param txnId
+ * identifier used to name temporary staging refs.
+ */
+ public StageBuilder(String txnStageNamespace, ObjectId txnId) {
+ this.txnStage = txnStageNamespace;
+ this.txnId = txnId.name();
+ }
+
+ /**
+ * Compare two RefTrees and return commands to stage new objects.
+ * <p>
+ * This method ignores the lineage between the two RefTrees and does a
+ * straight diff on the two trees. New objects will be staged. The diff
+ * strategy is useful to catch-up a lagging replica, without sending every
+ * intermediate step. This may mean the replica does not have the same
+ * object set as other replicas if there are rewinds or branch deletes.
+ *
+ * @param git
+ * source repository to read {@code oldTree} and {@code newTree}
+ * from.
+ * @param oldTree
+ * accepted RefTree on the replica ({@code refs/txn/accepted}).
+ * Use {@link ObjectId#zeroId()} if the remote does not have any
+ * ref tree, e.g. a new replica catching up.
+ * @param newTree
+ * RefTree being sent to the replica. The trees will be compared.
+ * @return list of commands to create {@code "refs/txn/stage/..."}
+ * references on replicas anchoring new objects into the repository
+ * while a transaction gains consensus.
+ * @throws IOException
+ * {@code git} cannot be accessed to compare {@code oldTree} and
+ * {@code newTree} to build the object set.
+ */
+ public List<ReceiveCommand> makeStageList(Repository git, ObjectId oldTree,
+ ObjectId newTree) throws IOException {
+ try (RevWalk rw = new RevWalk(git);
+ TreeWalk tw = new TreeWalk(rw.getObjectReader());
+ ObjectInserter ins = git.newObjectInserter()) {
+ if (AnyObjectId.equals(oldTree, ObjectId.zeroId())) {
+ tw.addTree(new EmptyTreeIterator());
+ } else {
+ tw.addTree(rw.parseTree(oldTree));
+ }
+ tw.addTree(rw.parseTree(newTree));
+ tw.setFilter(TreeFilter.ANY_DIFF);
+ tw.setRecursive(true);
+
+ Set<ObjectId> newObjs = new HashSet<>();
+ while (tw.next()) {
+ if (tw.getRawMode(1) == TYPE_GITLINK
+ && !tw.isPathSuffix(PEEL, 2)) {
+ newObjs.add(tw.getObjectId(1));
+ }
+ }
+
+ List<ReceiveCommand> cmds = makeStageList(newObjs, git, ins);
+ ins.flush();
+ return cmds;
+ }
+ }
+
+ /**
+ * Construct a set of commands to stage objects on a replica.
+ *
+ * @param newObjs
+ * objects to send to a replica.
+ * @param git
+ * local repository to read source objects from. Required to
+ * perform minification of {@code newObjs}.
+ * @param inserter
+ * inserter to write temporary commit objects during minification
+ * if many new branches are created by {@code newObjs}.
+ * @return list of commands to create {@code "refs/txn/stage/..."}
+ * references on replicas anchoring {@code newObjs} into the
+ * repository while a transaction gains consensus.
+ * @throws IOException
+ * {@code git} cannot be accessed to perform minification of
+ * {@code newObjs}.
+ */
+ public List<ReceiveCommand> makeStageList(Set<ObjectId> newObjs,
+ @Nullable Repository git, @Nullable ObjectInserter inserter)
+ throws IOException {
+ if (git == null || newObjs.size() <= SMALL_BATCH_SIZE) {
+ // Without a source repository can only construct unique set.
+ List<ReceiveCommand> cmds = new ArrayList<>(newObjs.size());
+ for (ObjectId id : newObjs) {
+ stage(cmds, id);
+ }
+ return cmds;
+ }
+
+ List<ReceiveCommand> cmds = new ArrayList<>();
+ List<RevCommit> commits = new ArrayList<>();
+ reduceObjects(cmds, commits, git, newObjs);
+
+ if (inserter == null || commits.size() <= 1
+ || (cmds.size() + commits.size()) <= SMALL_BATCH_SIZE) {
+ // Without an inserter to aggregate commits, or for a small set of
+ // commits just send one stage ref per commit.
+ for (RevCommit c : commits) {
+ stage(cmds, c.copy());
+ }
+ return cmds;
+ }
+
+ // 'commits' is sorted most recent to least recent commit.
+ // Group batches of commits and build a chain.
+ // TODO(sop) Cluster by restricted graphs to support filtering.
+ ObjectId tip = null;
+ for (int end = commits.size(); end > 0;) {
+ int start = Math.max(0, end - TEMP_PARENT_BATCH_SIZE);
+ List<RevCommit> batch = commits.subList(start, end);
+ List<ObjectId> parents = new ArrayList<>(1 + batch.size());
+ if (tip != null) {
+ parents.add(tip);
+ }
+ parents.addAll(batch);
+
+ CommitBuilder b = new CommitBuilder();
+ b.setTreeId(batch.get(0).getTree());
+ b.setParentIds(parents);
+ b.setAuthor(tmpAuthor(batch));
+ b.setCommitter(b.getAuthor());
+ tip = inserter.insert(b);
+ end = start;
+ }
+ stage(cmds, tip);
+ return cmds;
+ }
+
+ private static PersonIdent tmpAuthor(List<RevCommit> commits) {
+ // Construct a predictable author using most recent commit time.
+ int t = 0;
+ for (int i = 0; i < commits.size();) {
+ t = Math.max(t, commits.get(i).getCommitTime());
+ }
+ String name = "Ketch Stage"; //$NON-NLS-1$
+ String email = "tmp@tmp"; //$NON-NLS-1$
+ return new PersonIdent(name, email, t * 1000L, 0);
+ }
+
+ private void reduceObjects(List<ReceiveCommand> cmds,
+ List<RevCommit> commits, Repository git,
+ Set<ObjectId> newObjs) throws IOException {
+ try (RevWalk rw = new RevWalk(git)) {
+ rw.setRetainBody(false);
+
+ for (ObjectId id : newObjs) {
+ RevObject obj = rw.parseAny(id);
+ if (obj instanceof RevCommit) {
+ rw.markStart((RevCommit) obj);
+ } else {
+ stage(cmds, id);
+ }
+ }
+
+ for (RevCommit c; (c = rw.next()) != null;) {
+ commits.add(c);
+ rw.markUninteresting(c);
+ }
+ }
+ }
+
+ private void stage(List<ReceiveCommand> cmds, ObjectId id) {
+ int estLen = txnStage.length() + txnId.length() + 5;
+ StringBuilder n = new StringBuilder(estLen);
+ n.append(txnStage).append(txnId).append('.');
+ n.append(Integer.toHexString(cmds.size()));
+ cmds.add(new ReceiveCommand(ObjectId.zeroId(), id, n.toString()));
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/package-info.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/package-info.java
new file mode 100644
index 0000000000..dfe03752ca
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Distributed consensus system built on Git.
+ */
+package org.eclipse.jgit.internal.ketch;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollector.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollector.java
index faf27e32bb..33be3b15a8 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollector.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollector.java
@@ -44,18 +44,17 @@
package org.eclipse.jgit.internal.storage.dfs;
import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.GC;
+import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.GC_TXN;
import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.UNREACHABLE_GARBAGE;
import static org.eclipse.jgit.internal.storage.pack.PackExt.BITMAP_INDEX;
import static org.eclipse.jgit.internal.storage.pack.PackExt.INDEX;
import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK;
-import static org.eclipse.jgit.lib.RefDatabase.ALL;
import java.io.IOException;
import java.util.ArrayList;
-import java.util.Collections;
+import java.util.Collection;
import java.util.HashSet;
import java.util.List;
-import java.util.Map;
import java.util.Set;
import org.eclipse.jgit.internal.JGitText;
@@ -63,13 +62,15 @@ import org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource;
import org.eclipse.jgit.internal.storage.file.PackIndex;
import org.eclipse.jgit.internal.storage.pack.PackExt;
import org.eclipse.jgit.internal.storage.pack.PackWriter;
+import org.eclipse.jgit.internal.storage.reftree.RefTreeNames;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectIdOwnerMap;
+import org.eclipse.jgit.lib.ObjectIdSet;
import org.eclipse.jgit.lib.ProgressMonitor;
import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.storage.pack.PackConfig;
import org.eclipse.jgit.storage.pack.PackStatistics;
@@ -78,16 +79,14 @@ import org.eclipse.jgit.util.io.CountingOutputStream;
/** Repack and garbage collect a repository. */
public class DfsGarbageCollector {
private final DfsRepository repo;
-
- private final DfsRefDatabase refdb;
-
+ private final RefDatabase refdb;
private final DfsObjDatabase objdb;
private final List<DfsPackDescription> newPackDesc;
private final List<PackStatistics> newPackStats;
- private final List<PackWriter.ObjectIdSet> newPackObj;
+ private final List<ObjectIdSet> newPackObj;
private DfsReader ctx;
@@ -95,14 +94,11 @@ public class DfsGarbageCollector {
private long coalesceGarbageLimit = 50 << 20;
- private Map<String, Ref> refsBefore;
-
private List<DfsPackFile> packsBefore;
private Set<ObjectId> allHeads;
-
private Set<ObjectId> nonHeads;
-
+ private Set<ObjectId> txnHeads;
private Set<ObjectId> tagTargets;
/**
@@ -117,7 +113,7 @@ public class DfsGarbageCollector {
objdb = repo.getObjectDatabase();
newPackDesc = new ArrayList<DfsPackDescription>(4);
newPackStats = new ArrayList<PackStatistics>(4);
- newPackObj = new ArrayList<PackWriter.ObjectIdSet>(4);
+ newPackObj = new ArrayList<ObjectIdSet>(4);
packConfig = new PackConfig(repo);
packConfig.setIndexVersion(2);
@@ -195,22 +191,25 @@ public class DfsGarbageCollector {
ctx = (DfsReader) objdb.newReader();
try {
- refdb.clearCache();
+ refdb.refresh();
objdb.clearCache();
- refsBefore = refdb.getRefs(ALL);
+ Collection<Ref> refsBefore = getAllRefs();
packsBefore = packsToRebuild();
if (packsBefore.isEmpty())
return true;
allHeads = new HashSet<ObjectId>();
nonHeads = new HashSet<ObjectId>();
+ txnHeads = new HashSet<ObjectId>();
tagTargets = new HashSet<ObjectId>();
- for (Ref ref : refsBefore.values()) {
+ for (Ref ref : refsBefore) {
if (ref.isSymbolic() || ref.getObjectId() == null)
continue;
if (isHead(ref))
allHeads.add(ref.getObjectId());
+ else if (RefTreeNames.isRefTree(refdb, ref.getName()))
+ txnHeads.add(ref.getObjectId());
else
nonHeads.add(ref.getObjectId());
if (ref.getPeeledObjectId() != null)
@@ -222,6 +221,7 @@ public class DfsGarbageCollector {
try {
packHeads(pm);
packRest(pm);
+ packRefTreeGraph(pm);
packGarbage(pm);
objdb.commitPack(newPackDesc, toPrune());
rollback = false;
@@ -235,6 +235,18 @@ public class DfsGarbageCollector {
}
}
+ private Collection<Ref> getAllRefs() throws IOException {
+ Collection<Ref> refs = refdb.getRefs(RefDatabase.ALL).values();
+ List<Ref> addl = refdb.getAdditionalRefs();
+ if (!addl.isEmpty()) {
+ List<Ref> all = new ArrayList<>(refs.size() + addl.size());
+ all.addAll(refs);
+ all.addAll(addl);
+ return all;
+ }
+ return refs;
+ }
+
private List<DfsPackFile> packsToRebuild() throws IOException {
DfsPackFile[] packs = objdb.getPacks();
List<DfsPackFile> out = new ArrayList<DfsPackFile>(packs.length);
@@ -277,18 +289,17 @@ public class DfsGarbageCollector {
try (PackWriter pw = newPackWriter()) {
pw.setTagTargets(tagTargets);
- pw.preparePack(pm, allHeads, Collections.<ObjectId> emptySet());
+ pw.preparePack(pm, allHeads, PackWriter.NONE);
if (0 < pw.getObjectCount())
writePack(GC, pw, pm);
}
}
-
private void packRest(ProgressMonitor pm) throws IOException {
if (nonHeads.isEmpty())
return;
try (PackWriter pw = newPackWriter()) {
- for (PackWriter.ObjectIdSet packedObjs : newPackObj)
+ for (ObjectIdSet packedObjs : newPackObj)
pw.excludeObjects(packedObjs);
pw.preparePack(pm, nonHeads, allHeads);
if (0 < pw.getObjectCount())
@@ -296,6 +307,19 @@ public class DfsGarbageCollector {
}
}
+ private void packRefTreeGraph(ProgressMonitor pm) throws IOException {
+ if (txnHeads.isEmpty())
+ return;
+
+ try (PackWriter pw = newPackWriter()) {
+ for (ObjectIdSet packedObjs : newPackObj)
+ pw.excludeObjects(packedObjs);
+ pw.preparePack(pm, txnHeads, PackWriter.NONE);
+ if (0 < pw.getObjectCount())
+ writePack(GC_TXN, pw, pm);
+ }
+ }
+
private void packGarbage(ProgressMonitor pm) throws IOException {
// TODO(sop) This is ugly. The garbage pack needs to be deleted.
PackConfig cfg = new PackConfig(packConfig);
@@ -328,7 +352,7 @@ public class DfsGarbageCollector {
}
private boolean anyPackHas(AnyObjectId id) {
- for (PackWriter.ObjectIdSet packedObjs : newPackObj)
+ for (ObjectIdSet packedObjs : newPackObj)
if (packedObjs.contains(id))
return true;
return false;
@@ -389,17 +413,10 @@ public class DfsGarbageCollector {
}
}
- final ObjectIdOwnerMap<ObjectIdOwnerMap.Entry> packedObjs = pw
- .getObjectSet();
- newPackObj.add(new PackWriter.ObjectIdSet() {
- public boolean contains(AnyObjectId objectId) {
- return packedObjs.contains(objectId);
- }
- });
-
PackStatistics stats = pw.getStatistics();
pack.setPackStats(stats);
newPackStats.add(stats);
+ newPackObj.add(pw.getObjectSet());
DfsBlockCache.getInstance().getOrCreate(pack, null);
return pack;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjDatabase.java
index 5f491ff2fd..3641560ee9 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjDatabase.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjDatabase.java
@@ -91,6 +91,13 @@ public abstract class DfsObjDatabase extends ObjectDatabase {
GC(1),
/**
+ * RefTreeGraph pack was created by Git garbage collection.
+ *
+ * @see DfsGarbageCollector
+ */
+ GC_TXN(1),
+
+ /**
* The pack was created by compacting multiple packs together.
* <p>
* Packs created by compacting multiple packs together aren't nearly as
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackCompactor.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackCompactor.java
index 7073763a7a..11aef7feaf 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackCompactor.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackCompactor.java
@@ -62,6 +62,7 @@ import org.eclipse.jgit.internal.storage.pack.PackWriter;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdSet;
import org.eclipse.jgit.lib.ProgressMonitor;
import org.eclipse.jgit.revwalk.RevFlag;
import org.eclipse.jgit.revwalk.RevObject;
@@ -91,7 +92,7 @@ public class DfsPackCompactor {
private final List<DfsPackFile> srcPacks;
- private final List<PackWriter.ObjectIdSet> exclude;
+ private final List<ObjectIdSet> exclude;
private final List<DfsPackDescription> newPacks;
@@ -113,7 +114,7 @@ public class DfsPackCompactor {
repo = repository;
autoAddSize = 5 * 1024 * 1024; // 5 MiB
srcPacks = new ArrayList<DfsPackFile>();
- exclude = new ArrayList<PackWriter.ObjectIdSet>(4);
+ exclude = new ArrayList<ObjectIdSet>(4);
newPacks = new ArrayList<DfsPackDescription>(1);
newStats = new ArrayList<PackStatistics>(1);
}
@@ -164,7 +165,7 @@ public class DfsPackCompactor {
* objects to not include.
* @return {@code this}.
*/
- public DfsPackCompactor exclude(PackWriter.ObjectIdSet set) {
+ public DfsPackCompactor exclude(ObjectIdSet set) {
exclude.add(set);
return this;
}
@@ -183,11 +184,7 @@ public class DfsPackCompactor {
try (DfsReader ctx = (DfsReader) repo.newObjectReader()) {
idx = pack.getPackIndex(ctx);
}
- return exclude(new PackWriter.ObjectIdSet() {
- public boolean contains(AnyObjectId id) {
- return idx.hasObject(id);
- }
- });
+ return exclude(idx);
}
/**
@@ -343,7 +340,7 @@ public class DfsPackCompactor {
RevObject obj = rw.lookupOrNull(id);
if (obj != null && (obj.has(added) || obj.has(isBase)))
continue;
- for (PackWriter.ObjectIdSet e : exclude)
+ for (ObjectIdSet e : exclude)
if (e.contains(id))
continue SCAN;
want.add(new ObjectIdWithOffset(id, ent.getOffset()));
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRefDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRefDatabase.java
index a1035a1284..e5469f6b83 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRefDatabase.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRefDatabase.java
@@ -262,6 +262,11 @@ public abstract class DfsRefDatabase extends RefDatabase {
}
@Override
+ public void refresh() {
+ clearCache();
+ }
+
+ @Override
public void close() {
clearCache();
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRepository.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRepository.java
index 0d5fd0f859..ef8845084b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRepository.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRepository.java
@@ -79,9 +79,6 @@ public abstract class DfsRepository extends Repository {
@Override
public abstract DfsObjDatabase getObjectDatabase();
- @Override
- public abstract DfsRefDatabase getRefDatabase();
-
/** @return a description of this repository. */
public DfsRepositoryDescription getDescription() {
return description;
@@ -95,7 +92,10 @@ public abstract class DfsRepository extends Repository {
* the repository cannot be checked.
*/
public boolean exists() throws IOException {
- return getRefDatabase().exists();
+ if (getRefDatabase() instanceof DfsRefDatabase) {
+ return ((DfsRefDatabase) getRefDatabase()).exists();
+ }
+ return true;
}
@Override
@@ -117,7 +117,7 @@ public abstract class DfsRepository extends Repository {
@Override
public void scanForRepoChanges() throws IOException {
- getRefDatabase().clearCache();
+ getRefDatabase().refresh();
getObjectDatabase().clearCache();
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/InMemoryRepository.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/InMemoryRepository.java
index 1c664b4097..5e246b47b4 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/InMemoryRepository.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/InMemoryRepository.java
@@ -16,7 +16,6 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
-import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.internal.storage.pack.PackExt;
import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.ObjectId;
@@ -24,6 +23,7 @@ import org.eclipse.jgit.lib.ObjectIdRef;
import org.eclipse.jgit.lib.ProgressMonitor;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Ref.Storage;
+import org.eclipse.jgit.lib.RefDatabase;
import org.eclipse.jgit.lib.SymbolicRef;
import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.revwalk.RevTag;
@@ -54,7 +54,7 @@ public class InMemoryRepository extends DfsRepository {
static final AtomicInteger packId = new AtomicInteger();
private final DfsObjDatabase objdb;
- private final DfsRefDatabase refdb;
+ private final RefDatabase refdb;
private boolean performsAtomicTransactions = true;
/**
@@ -80,7 +80,7 @@ public class InMemoryRepository extends DfsRepository {
}
@Override
- public DfsRefDatabase getRefDatabase() {
+ public RefDatabase getRefDatabase() {
return refdb;
}
@@ -310,6 +310,11 @@ public class InMemoryRepository extends DfsRepository {
Map<ObjectId, ObjectId> peeled = new HashMap<>();
try (RevWalk rw = new RevWalk(getRepository())) {
for (ReceiveCommand c : cmds) {
+ if (c.getResult() != ReceiveCommand.Result.NOT_ATTEMPTED) {
+ ReceiveCommand.abort(cmds);
+ return;
+ }
+
if (!ObjectId.zeroId().equals(c.getNewId())) {
try {
RevObject o = rw.parseAny(c.getNewId());
@@ -318,7 +323,7 @@ public class InMemoryRepository extends DfsRepository {
}
} catch (IOException e) {
c.setResult(ReceiveCommand.Result.REJECTED_MISSING_OBJECT);
- reject(cmds);
+ ReceiveCommand.abort(cmds);
return;
}
}
@@ -331,14 +336,17 @@ public class InMemoryRepository extends DfsRepository {
if (r == null) {
if (c.getType() != ReceiveCommand.Type.CREATE) {
c.setResult(ReceiveCommand.Result.LOCK_FAILURE);
- reject(cmds);
+ ReceiveCommand.abort(cmds);
+ return;
+ }
+ } else {
+ ObjectId objectId = r.getObjectId();
+ if (r.isSymbolic() || objectId == null
+ || !objectId.equals(c.getOldId())) {
+ c.setResult(ReceiveCommand.Result.LOCK_FAILURE);
+ ReceiveCommand.abort(cmds);
return;
}
- } else if (r.isSymbolic() || r.getObjectId() == null
- || !r.getObjectId().equals(c.getOldId())) {
- c.setResult(ReceiveCommand.Result.LOCK_FAILURE);
- reject(cmds);
- return;
}
}
@@ -365,15 +373,6 @@ public class InMemoryRepository extends DfsRepository {
clearCache();
}
- private void reject(List<ReceiveCommand> cmds) {
- for (ReceiveCommand c : cmds) {
- if (c.getResult() == ReceiveCommand.Result.NOT_ATTEMPTED) {
- c.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON,
- JGitText.get().transactionAborted);
- }
- }
- }
-
@Override
protected boolean compareAndPut(Ref oldRef, Ref newRef)
throws IOException {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java
index 490cbcaa81..62d2d6969f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java
@@ -63,6 +63,7 @@ import org.eclipse.jgit.events.ConfigChangedEvent;
import org.eclipse.jgit.events.ConfigChangedListener;
import org.eclipse.jgit.events.IndexChangedEvent;
import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.storage.reftree.RefTreeDatabase;
import org.eclipse.jgit.internal.storage.file.ObjectDirectory.AlternateHandle;
import org.eclipse.jgit.internal.storage.file.ObjectDirectory.AlternateRepository;
import org.eclipse.jgit.lib.BaseRepositoryBuilder;
@@ -201,7 +202,22 @@ public class FileRepository extends Repository {
}
});
- refs = new RefDirectory(this);
+ final long repositoryFormatVersion = getConfig().getLong(
+ ConfigConstants.CONFIG_CORE_SECTION, null,
+ ConfigConstants.CONFIG_KEY_REPO_FORMAT_VERSION, 0);
+
+ String reftype = repoConfig.getString(
+ "extensions", null, "refsStorage"); //$NON-NLS-1$ //$NON-NLS-2$
+ if (repositoryFormatVersion >= 1 && reftype != null) {
+ if (StringUtils.equalsIgnoreCase(reftype, "reftree")) { //$NON-NLS-1$
+ refs = new RefTreeDatabase(this, new RefDirectory(this));
+ } else {
+ throw new IOException(JGitText.get().unknownRepositoryFormat);
+ }
+ } else {
+ refs = new RefDirectory(this);
+ }
+
objectDatabase = new ObjectDirectory(repoConfig, //
options.getObjectDirectory(), //
options.getAlternateObjectDirectories(), //
@@ -209,10 +225,7 @@ public class FileRepository extends Repository {
new File(getDirectory(), Constants.SHALLOW));
if (objectDatabase.exists()) {
- final long repositoryFormatVersion = getConfig().getLong(
- ConfigConstants.CONFIG_CORE_SECTION, null,
- ConfigConstants.CONFIG_KEY_REPO_FORMAT_VERSION, 0);
- if (repositoryFormatVersion > 0)
+ if (repositoryFormatVersion > 1)
throw new IOException(MessageFormat.format(
JGitText.get().unknownRepositoryFormat2,
Long.valueOf(repositoryFormatVersion)));
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java
index 4c40538b6a..49f9335aed 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java
@@ -45,7 +45,6 @@ package org.eclipse.jgit.internal.storage.file;
import static org.eclipse.jgit.internal.storage.pack.PackExt.BITMAP_INDEX;
import static org.eclipse.jgit.internal.storage.pack.PackExt.INDEX;
-import static org.eclipse.jgit.lib.RefDatabase.ALL;
import java.io.File;
import java.io.FileOutputStream;
@@ -53,6 +52,7 @@ import java.io.IOException;
import java.io.OutputStream;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
+import java.nio.file.StandardCopyOption;
import java.text.MessageFormat;
import java.text.ParseException;
import java.util.ArrayList;
@@ -62,14 +62,14 @@ import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
-import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
-import java.util.Map.Entry;
+import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
+import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.dircache.DirCacheIterator;
import org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -78,13 +78,13 @@ import org.eclipse.jgit.errors.NoWorkTreeException;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.internal.storage.pack.PackExt;
import org.eclipse.jgit.internal.storage.pack.PackWriter;
-import org.eclipse.jgit.internal.storage.pack.PackWriter.ObjectIdSet;
-import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.internal.storage.reftree.RefTreeNames;
import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdSet;
import org.eclipse.jgit.lib.ProgressMonitor;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Ref.Storage;
@@ -127,7 +127,7 @@ public class GC {
* difference between the current refs and the refs which existed during
* last {@link #repack()}.
*/
- private Map<String, Ref> lastPackedRefs;
+ private Collection<Ref> lastPackedRefs;
/**
* Holds the starting time of the last repack() execution. This is needed in
@@ -361,17 +361,20 @@ public class GC {
// during last repack(). Only those refs will survive which have been
// added or modified since the last repack. Only these can save existing
// loose refs from being pruned.
- Map<String, Ref> newRefs;
+ Collection<Ref> newRefs;
if (lastPackedRefs == null || lastPackedRefs.isEmpty())
newRefs = getAllRefs();
else {
- newRefs = new HashMap<String, Ref>();
- for (Iterator<Map.Entry<String, Ref>> i = getAllRefs().entrySet()
- .iterator(); i.hasNext();) {
- Entry<String, Ref> newEntry = i.next();
- Ref old = lastPackedRefs.get(newEntry.getKey());
- if (!equals(newEntry.getValue(), old))
- newRefs.put(newEntry.getKey(), newEntry.getValue());
+ Map<String, Ref> last = new HashMap<>();
+ for (Ref r : lastPackedRefs) {
+ last.put(r.getName(), r);
+ }
+ newRefs = new ArrayList<>();
+ for (Ref r : getAllRefs()) {
+ Ref old = last.get(r.getName());
+ if (!equals(r, old)) {
+ newRefs.add(r);
+ }
}
}
@@ -383,10 +386,10 @@ public class GC {
// leave this method.
ObjectWalk w = new ObjectWalk(repo);
try {
- for (Ref cr : newRefs.values())
+ for (Ref cr : newRefs)
w.markStart(w.parseAny(cr.getObjectId()));
if (lastPackedRefs != null)
- for (Ref lpr : lastPackedRefs.values())
+ for (Ref lpr : lastPackedRefs)
w.markUninteresting(w.parseAny(lpr.getObjectId()));
removeReferenced(deletionCandidates, w);
} finally {
@@ -404,11 +407,11 @@ public class GC {
// additional reflog entries not handled during last repack()
ObjectWalk w = new ObjectWalk(repo);
try {
- for (Ref ar : getAllRefs().values())
+ for (Ref ar : getAllRefs())
for (ObjectId id : listRefLogObjects(ar, lastRepackTime))
w.markStart(w.parseAny(id));
if (lastPackedRefs != null)
- for (Ref lpr : lastPackedRefs.values())
+ for (Ref lpr : lastPackedRefs)
w.markUninteresting(w.parseAny(lpr.getObjectId()));
removeReferenced(deletionCandidates, w);
} finally {
@@ -483,9 +486,10 @@ public class GC {
return false;
return r1.getTarget().getName().equals(r2.getTarget().getName());
} else {
- if (r2.isSymbolic())
+ if (r2.isSymbolic()) {
return false;
- return r1.getObjectId().equals(r2.getObjectId());
+ }
+ return Objects.equals(r1.getObjectId(), r2.getObjectId());
}
}
@@ -528,19 +532,23 @@ public class GC {
Collection<PackFile> toBeDeleted = repo.getObjectDatabase().getPacks();
long time = System.currentTimeMillis();
- Map<String, Ref> refsBefore = getAllRefs();
+ Collection<Ref> refsBefore = getAllRefs();
Set<ObjectId> allHeads = new HashSet<ObjectId>();
Set<ObjectId> nonHeads = new HashSet<ObjectId>();
+ Set<ObjectId> txnHeads = new HashSet<ObjectId>();
Set<ObjectId> tagTargets = new HashSet<ObjectId>();
Set<ObjectId> indexObjects = listNonHEADIndexObjects();
+ RefDatabase refdb = repo.getRefDatabase();
- for (Ref ref : refsBefore.values()) {
+ for (Ref ref : refsBefore) {
nonHeads.addAll(listRefLogObjects(ref, 0));
if (ref.isSymbolic() || ref.getObjectId() == null)
continue;
if (ref.getName().startsWith(Constants.R_HEADS))
allHeads.add(ref.getObjectId());
+ else if (RefTreeNames.isRefTree(refdb, ref.getName()))
+ txnHeads.add(ref.getObjectId());
else
nonHeads.add(ref.getObjectId());
if (ref.getPeeledObjectId() != null)
@@ -550,7 +558,7 @@ public class GC {
List<ObjectIdSet> excluded = new LinkedList<ObjectIdSet>();
for (final PackFile f : repo.getObjectDatabase().getPacks())
if (f.shouldBeKept())
- excluded.add(objectIdSet(f.getIndex()));
+ excluded.add(f.getIndex());
tagTargets.addAll(allHeads);
nonHeads.addAll(indexObjects);
@@ -562,7 +570,7 @@ public class GC {
tagTargets, excluded);
if (heads != null) {
ret.add(heads);
- excluded.add(0, objectIdSet(heads.getIndex()));
+ excluded.add(0, heads.getIndex());
}
}
if (!nonHeads.isEmpty()) {
@@ -570,6 +578,11 @@ public class GC {
if (rest != null)
ret.add(rest);
}
+ if (!txnHeads.isEmpty()) {
+ PackFile txn = writePack(txnHeads, PackWriter.NONE, null, excluded);
+ if (txn != null)
+ ret.add(txn);
+ }
try {
deleteOldPacks(toBeDeleted, ret);
} catch (ParseException e) {
@@ -616,17 +629,23 @@ public class GC {
}
/**
- * Returns a map of all refs and additional refs (e.g. FETCH_HEAD,
+ * Returns a collection of all refs and additional refs (e.g. FETCH_HEAD,
* MERGE_HEAD, ...)
*
- * @return a map where names of refs point to ref objects
+ * @return a collection of refs pointing to live objects.
* @throws IOException
*/
- private Map<String, Ref> getAllRefs() throws IOException {
- Map<String, Ref> ret = repo.getRefDatabase().getRefs(ALL);
- for (Ref ref : repo.getRefDatabase().getAdditionalRefs())
- ret.put(ref.getName(), ref);
- return ret;
+ private Collection<Ref> getAllRefs() throws IOException {
+ RefDatabase refdb = repo.getRefDatabase();
+ Collection<Ref> refs = refdb.getRefs(RefDatabase.ALL).values();
+ List<Ref> addl = refdb.getAdditionalRefs();
+ if (!addl.isEmpty()) {
+ List<Ref> all = new ArrayList<>(refs.size() + addl.size());
+ all.addAll(refs);
+ all.addAll(addl);
+ return all;
+ }
+ return refs;
}
/**
@@ -681,8 +700,8 @@ public class GC {
}
}
- private PackFile writePack(Set<? extends ObjectId> want,
- Set<? extends ObjectId> have, Set<ObjectId> tagTargets,
+ private PackFile writePack(@NonNull Set<? extends ObjectId> want,
+ @NonNull Set<? extends ObjectId> have, Set<ObjectId> tagTargets,
List<ObjectIdSet> excludeObjects) throws IOException {
File tmpPack = null;
Map<PackExt, File> tmpExts = new TreeMap<PackExt, File>(
@@ -788,39 +807,33 @@ public class GC {
break;
}
tmpPack.setReadOnly();
- boolean delete = true;
- try {
- FileUtils.rename(tmpPack, realPack);
- delete = false;
- for (Map.Entry<PackExt, File> tmpEntry : tmpExts.entrySet()) {
- File tmpExt = tmpEntry.getValue();
- tmpExt.setReadOnly();
-
- File realExt = nameFor(
- id, "." + tmpEntry.getKey().getExtension()); //$NON-NLS-1$
- try {
- FileUtils.rename(tmpExt, realExt);
- } catch (IOException e) {
- File newExt = new File(realExt.getParentFile(),
- realExt.getName() + ".new"); //$NON-NLS-1$
- if (!tmpExt.renameTo(newExt))
- newExt = tmpExt;
- throw new IOException(MessageFormat.format(
- JGitText.get().panicCantRenameIndexFile, newExt,
- realExt));
- }
- }
- } finally {
- if (delete) {
- if (tmpPack.exists())
- tmpPack.delete();
- for (File tmpExt : tmpExts.values()) {
- if (tmpExt.exists())
- tmpExt.delete();
+ FileUtils.rename(tmpPack, realPack, StandardCopyOption.ATOMIC_MOVE);
+ for (Map.Entry<PackExt, File> tmpEntry : tmpExts.entrySet()) {
+ File tmpExt = tmpEntry.getValue();
+ tmpExt.setReadOnly();
+
+ File realExt = nameFor(id,
+ "." + tmpEntry.getKey().getExtension()); //$NON-NLS-1$
+ try {
+ FileUtils.rename(tmpExt, realExt,
+ StandardCopyOption.ATOMIC_MOVE);
+ } catch (IOException e) {
+ File newExt = new File(realExt.getParentFile(),
+ realExt.getName() + ".new"); //$NON-NLS-1$
+ try {
+ FileUtils.rename(tmpExt, newExt,
+ StandardCopyOption.ATOMIC_MOVE);
+ } catch (IOException e2) {
+ newExt = tmpExt;
+ e = e2;
}
+ throw new IOException(MessageFormat.format(
+ JGitText.get().panicCantRenameIndexFile, newExt,
+ realExt), e);
}
}
+
return repo.getObjectDatabase().openPack(realPack);
} finally {
if (tmpPack != null && tmpPack.exists())
@@ -998,12 +1011,4 @@ public class GC {
this.expire = expire;
expireAgeMillis = -1;
}
-
- private static ObjectIdSet objectIdSet(final PackIndex idx) {
- return new ObjectIdSet() {
- public boolean contains(AnyObjectId objectId) {
- return idx.hasObject(objectId);
- }
- };
- }
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LazyObjectIdSetFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LazyObjectIdSetFile.java
new file mode 100644
index 0000000000..1e2617c0e3
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LazyObjectIdSetFile.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2015, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.storage.file;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.MutableObjectId;
+import org.eclipse.jgit.lib.ObjectIdOwnerMap;
+import org.eclipse.jgit.lib.ObjectIdSet;
+
+/** Lazily loads a set of ObjectIds, one per line. */
+public class LazyObjectIdSetFile implements ObjectIdSet {
+ private final File src;
+ private ObjectIdOwnerMap<Entry> set;
+
+ /**
+ * Create a new lazy set from a file.
+ *
+ * @param src
+ * the source file.
+ */
+ public LazyObjectIdSetFile(File src) {
+ this.src = src;
+ }
+
+ @Override
+ public boolean contains(AnyObjectId objectId) {
+ if (set == null) {
+ set = load();
+ }
+ return set.contains(objectId);
+ }
+
+ private ObjectIdOwnerMap<Entry> load() {
+ ObjectIdOwnerMap<Entry> r = new ObjectIdOwnerMap<>();
+ try (FileInputStream fin = new FileInputStream(src);
+ Reader rin = new InputStreamReader(fin, UTF_8);
+ BufferedReader br = new BufferedReader(rin)) {
+ MutableObjectId id = new MutableObjectId();
+ for (String line; (line = br.readLine()) != null;) {
+ id.fromString(line);
+ if (!r.contains(id)) {
+ r.add(new Entry(id));
+ }
+ }
+ } catch (IOException e) {
+ // Ignore IO errors accessing the lazy set.
+ }
+ return r;
+ }
+
+ static class Entry extends ObjectIdOwnerMap.Entry {
+ Entry(AnyObjectId id) {
+ super(id);
+ }
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java
index e23ca741b8..ce9677a62d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java
@@ -54,6 +54,7 @@ import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
+import java.nio.file.StandardCopyOption;
import java.text.MessageFormat;
import org.eclipse.jgit.errors.LockFailedException;
@@ -128,8 +129,6 @@ public class LockFile {
private FileSnapshot commitSnapshot;
- private final FS fs;
-
/**
* Create a new lock for any file.
*
@@ -138,11 +137,24 @@ public class LockFile {
* @param fs
* the file system abstraction which will be necessary to perform
* certain file system operations.
+ * @deprecated use {@link LockFile#LockFile(File)} instead
*/
+ @Deprecated
public LockFile(final File f, final FS fs) {
ref = f;
lck = getLockFile(ref);
- this.fs = fs;
+ }
+
+ /**
+ * Create a new lock for any file.
+ *
+ * @param f
+ * the file that will be locked.
+ * @since 4.2
+ */
+ public LockFile(final File f) {
+ ref = f;
+ lck = getLockFile(ref);
}
/**
@@ -441,56 +453,14 @@ public class LockFile {
}
saveStatInformation();
- if (lck.renameTo(ref)) {
+ try {
+ FileUtils.rename(lck, ref, StandardCopyOption.ATOMIC_MOVE);
haveLck = false;
return true;
+ } catch (IOException e) {
+ unlock();
+ return false;
}
- if (!ref.exists() || deleteRef()) {
- if (renameLock()) {
- haveLck = false;
- return true;
- }
- }
- unlock();
- return false;
- }
-
- private boolean deleteRef() {
- if (!fs.retryFailedLockFileCommit())
- return ref.delete();
-
- // File deletion fails on windows if another thread is
- // concurrently reading the same file. So try a few times.
- //
- for (int attempts = 0; attempts < 10; attempts++) {
- if (ref.delete())
- return true;
- try {
- Thread.sleep(100);
- } catch (InterruptedException e) {
- return false;
- }
- }
- return false;
- }
-
- private boolean renameLock() {
- if (!fs.retryFailedLockFileCommit())
- return lck.renameTo(ref);
-
- // File renaming fails on windows if another thread is
- // concurrently reading the same file. So try a few times.
- //
- for (int attempts = 0; attempts < 10; attempts++) {
- if (lck.renameTo(ref))
- return true;
- try {
- Thread.sleep(100);
- } catch (InterruptedException e) {
- return false;
- }
- }
- return false;
}
private void saveStatInformation() {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java
index bd1d488d94..ea80528518 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java
@@ -52,6 +52,9 @@ import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
+import java.nio.file.AtomicMoveNotSupportedException;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
@@ -608,10 +611,16 @@ public class ObjectDirectory extends FileObjectDatabase {
FileUtils.delete(tmp, FileUtils.RETRY);
return InsertLooseObjectResult.EXISTS_LOOSE;
}
- if (tmp.renameTo(dst)) {
+ try {
+ Files.move(tmp.toPath(), dst.toPath(),
+ StandardCopyOption.ATOMIC_MOVE);
dst.setReadOnly();
unpackedObjectCache.add(id);
return InsertLooseObjectResult.INSERTED;
+ } catch (AtomicMoveNotSupportedException e) {
+ LOG.error(e.getMessage(), e);
+ } catch (IOException e) {
+ // ignore
}
// Maybe the directory doesn't exist yet as the object
@@ -619,10 +628,16 @@ public class ObjectDirectory extends FileObjectDatabase {
// try the rename first as the directory likely does exist.
//
FileUtils.mkdir(dst.getParentFile(), true);
- if (tmp.renameTo(dst)) {
+ try {
+ Files.move(tmp.toPath(), dst.toPath(),
+ StandardCopyOption.ATOMIC_MOVE);
dst.setReadOnly();
unpackedObjectCache.add(id);
return InsertLooseObjectResult.INSERTED;
+ } catch (AtomicMoveNotSupportedException e) {
+ LOG.error(e.getMessage(), e);
+ } catch (IOException e) {
+ LOG.debug(e.getMessage(), e);
}
if (!createDuplicate && has(id)) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryPackParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryPackParser.java
index 1c076ee099..2e6c245ea1 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryPackParser.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryPackParser.java
@@ -50,6 +50,7 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
+import java.nio.file.StandardCopyOption;
import java.security.MessageDigest;
import java.text.MessageFormat;
import java.util.Arrays;
@@ -476,20 +477,25 @@ public class ObjectDirectoryPackParser extends PackParser {
}
}
- if (!tmpPack.renameTo(finalPack)) {
+ try {
+ FileUtils.rename(tmpPack, finalPack,
+ StandardCopyOption.ATOMIC_MOVE);
+ } catch (IOException e) {
cleanupTemporaryFiles();
keep.unlock();
throw new IOException(MessageFormat.format(
- JGitText.get().cannotMovePackTo, finalPack));
+ JGitText.get().cannotMovePackTo, finalPack), e);
}
- if (!tmpIdx.renameTo(finalIdx)) {
+ try {
+ FileUtils.rename(tmpIdx, finalIdx, StandardCopyOption.ATOMIC_MOVE);
+ } catch (IOException e) {
cleanupTemporaryFiles();
keep.unlock();
if (!finalPack.delete())
finalPack.deleteOnExit();
throw new IOException(MessageFormat.format(
- JGitText.get().cannotMoveIndexTo, finalIdx));
+ JGitText.get().cannotMoveIndexTo, finalIdx), e);
}
try {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndex.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndex.java
index 0040aea713..f36bd4d70c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndex.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndex.java
@@ -60,6 +60,7 @@ import org.eclipse.jgit.lib.AbbreviatedObjectId;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.MutableObjectId;
import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdSet;
import org.eclipse.jgit.util.IO;
import org.eclipse.jgit.util.NB;
@@ -72,7 +73,8 @@ import org.eclipse.jgit.util.NB;
* by ObjectId.
* </p>
*/
-public abstract class PackIndex implements Iterable<PackIndex.MutableEntry> {
+public abstract class PackIndex
+ implements Iterable<PackIndex.MutableEntry>, ObjectIdSet {
/**
* Open an existing pack <code>.idx</code> file for reading.
* <p>
@@ -166,6 +168,11 @@ public abstract class PackIndex implements Iterable<PackIndex.MutableEntry> {
return findOffset(id) != -1;
}
+ @Override
+ public boolean contains(AnyObjectId id) {
+ return findOffset(id) != -1;
+ }
+
/**
* Provide iterator that gives access to index entries. Note, that iterator
* returns reference to mutable object, the same reference in each call -
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java
index 69f7e97071..2c8e5f9d11 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java
@@ -73,6 +73,7 @@ import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
+import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.errors.InvalidObjectIdException;
import org.eclipse.jgit.errors.LockFailedException;
import org.eclipse.jgit.errors.MissingObjectException;
@@ -715,16 +716,20 @@ public class RefDirectory extends RefDatabase {
*/
private Ref peeledPackedRef(Ref f)
throws MissingObjectException, IOException {
- if (f.getStorage().isPacked() && f.isPeeled())
+ if (f.getStorage().isPacked() && f.isPeeled()) {
return f;
- if (!f.isPeeled())
+ }
+ if (!f.isPeeled()) {
f = peel(f);
- if (f.getPeeledObjectId() != null)
+ }
+ ObjectId peeledObjectId = f.getPeeledObjectId();
+ if (peeledObjectId != null) {
return new ObjectIdRef.PeeledTag(PACKED, f.getName(),
- f.getObjectId(), f.getPeeledObjectId());
- else
+ f.getObjectId(), peeledObjectId);
+ } else {
return new ObjectIdRef.PeeledNonTag(PACKED, f.getName(),
f.getObjectId());
+ }
}
void log(final RefUpdate update, final String msg, final boolean deref)
@@ -985,7 +990,7 @@ public class RefDirectory extends RefDatabase {
try {
id = ObjectId.fromString(buf, 0);
if (ref != null && !ref.isSymbolic()
- && ref.getTarget().getObjectId().equals(id)) {
+ && id.equals(ref.getTarget().getObjectId())) {
assert(currentSnapshot != null);
currentSnapshot.setClean(otherSnapshot);
return ref;
@@ -1103,8 +1108,8 @@ public class RefDirectory extends RefDatabase {
implements LooseRef {
private final FileSnapshot snapShot;
- LoosePeeledTag(FileSnapshot snapshot, String refName, ObjectId id,
- ObjectId p) {
+ LoosePeeledTag(FileSnapshot snapshot, @NonNull String refName,
+ @NonNull ObjectId id, @NonNull ObjectId p) {
super(LOOSE, refName, id, p);
this.snapShot = snapshot;
}
@@ -1122,7 +1127,8 @@ public class RefDirectory extends RefDatabase {
implements LooseRef {
private final FileSnapshot snapShot;
- LooseNonTag(FileSnapshot snapshot, String refName, ObjectId id) {
+ LooseNonTag(FileSnapshot snapshot, @NonNull String refName,
+ @NonNull ObjectId id) {
super(LOOSE, refName, id);
this.snapShot = snapshot;
}
@@ -1140,7 +1146,8 @@ public class RefDirectory extends RefDatabase {
implements LooseRef {
private FileSnapshot snapShot;
- LooseUnpeeled(FileSnapshot snapShot, String refName, ObjectId id) {
+ LooseUnpeeled(FileSnapshot snapShot, @NonNull String refName,
+ @NonNull ObjectId id) {
super(LOOSE, refName, id);
this.snapShot = snapShot;
}
@@ -1149,13 +1156,24 @@ public class RefDirectory extends RefDatabase {
return snapShot;
}
+ @NonNull
+ @Override
+ public ObjectId getObjectId() {
+ ObjectId id = super.getObjectId();
+ assert id != null; // checked in constructor
+ return id;
+ }
+
public LooseRef peel(ObjectIdRef newLeaf) {
- if (newLeaf.getPeeledObjectId() != null)
+ ObjectId peeledObjectId = newLeaf.getPeeledObjectId();
+ ObjectId objectId = getObjectId();
+ if (peeledObjectId != null) {
return new LoosePeeledTag(snapShot, getName(),
- getObjectId(), newLeaf.getPeeledObjectId());
- else
+ objectId, peeledObjectId);
+ } else {
return new LooseNonTag(snapShot, getName(),
- getObjectId());
+ objectId);
+ }
}
}
@@ -1163,7 +1181,8 @@ public class RefDirectory extends RefDatabase {
LooseRef {
private final FileSnapshot snapShot;
- LooseSymbolicRef(FileSnapshot snapshot, String refName, Ref target) {
+ LooseSymbolicRef(FileSnapshot snapshot, @NonNull String refName,
+ @NonNull Ref target) {
super(refName, target);
this.snapShot = snapshot;
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectoryRename.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectoryRename.java
index ba4a63d7fe..4b803a5144 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectoryRename.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectoryRename.java
@@ -46,6 +46,8 @@ package org.eclipse.jgit.internal.storage.file;
import java.io.File;
import java.io.IOException;
+import java.nio.file.AtomicMoveNotSupportedException;
+import java.nio.file.StandardCopyOption;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
@@ -54,6 +56,8 @@ import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.RefUpdate.Result;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.util.FileUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
* Rename any reference stored by {@link RefDirectory}.
@@ -66,6 +70,9 @@ import org.eclipse.jgit.util.FileUtils;
* directory that happens to match the source name.
*/
class RefDirectoryRename extends RefRename {
+ private static final Logger LOG = LoggerFactory
+ .getLogger(RefDirectoryRename.class);
+
private final RefDirectory refdb;
/**
@@ -201,13 +208,25 @@ class RefDirectoryRename extends RefRename {
}
private static boolean rename(File src, File dst) {
- if (src.renameTo(dst))
+ try {
+ FileUtils.rename(src, dst, StandardCopyOption.ATOMIC_MOVE);
return true;
+ } catch (AtomicMoveNotSupportedException e) {
+ LOG.error(e.getMessage(), e);
+ } catch (IOException e) {
+ // ignore
+ }
File dir = dst.getParentFile();
if ((dir.exists() || !dir.mkdirs()) && !dir.isDirectory())
return false;
- return src.renameTo(dst);
+ try {
+ FileUtils.rename(src, dst, StandardCopyOption.ATOMIC_MOVE);
+ return true;
+ } catch (IOException e) {
+ LOG.error(e.getMessage(), e);
+ return false;
+ }
}
private boolean linkHEAD(RefUpdate target) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriter.java
index 19b6b080da..525f9aecc7 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriter.java
@@ -80,6 +80,7 @@ import java.util.zip.CheckedOutputStream;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
+import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.LargeObjectException;
@@ -99,6 +100,7 @@ import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectIdOwnerMap;
+import org.eclipse.jgit.lib.ObjectIdSet;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.ProgressMonitor;
@@ -161,17 +163,8 @@ import org.eclipse.jgit.util.TemporaryBuffer;
public class PackWriter implements AutoCloseable {
private static final int PACK_VERSION_GENERATED = 2;
- /** A collection of object ids. */
- public interface ObjectIdSet {
- /**
- * Returns true if the objectId is contained within the collection.
- *
- * @param objectId
- * the objectId to find
- * @return whether the collection contains the objectId.
- */
- boolean contains(AnyObjectId objectId);
- }
+ /** Empty set of objects for {@code preparePack()}. */
+ public static Set<ObjectId> NONE = Collections.emptySet();
private static final Map<WeakReference<PackWriter>, Boolean> instances =
new ConcurrentHashMap<WeakReference<PackWriter>, Boolean>();
@@ -681,7 +674,7 @@ public class PackWriter implements AutoCloseable {
* @throws IOException
* when some I/O problem occur during reading objects.
*/
- public void preparePack(final Iterator<RevObject> objectsSource)
+ public void preparePack(@NonNull Iterator<RevObject> objectsSource)
throws IOException {
while (objectsSource.hasNext()) {
addObject(objectsSource.next());
@@ -704,16 +697,18 @@ public class PackWriter implements AutoCloseable {
* progress during object enumeration.
* @param want
* collection of objects to be marked as interesting (start
- * points of graph traversal).
+ * points of graph traversal). Must not be {@code null}.
* @param have
* collection of objects to be marked as uninteresting (end
- * points of graph traversal).
+ * points of graph traversal). Pass {@link #NONE} if all objects
+ * reachable from {@code want} are desired, such as when serving
+ * a clone.
* @throws IOException
* when some I/O problem occur during reading objects.
*/
public void preparePack(ProgressMonitor countingMonitor,
- Set<? extends ObjectId> want,
- Set<? extends ObjectId> have) throws IOException {
+ @NonNull Set<? extends ObjectId> want,
+ @NonNull Set<? extends ObjectId> have) throws IOException {
ObjectWalk ow;
if (shallowPack)
ow = new DepthWalk.ObjectWalk(reader, depth);
@@ -740,17 +735,19 @@ public class PackWriter implements AutoCloseable {
* ObjectWalk to perform enumeration.
* @param interestingObjects
* collection of objects to be marked as interesting (start
- * points of graph traversal).
+ * points of graph traversal). Must not be {@code null}.
* @param uninterestingObjects
* collection of objects to be marked as uninteresting (end
- * points of graph traversal).
+ * points of graph traversal). Pass {@link #NONE} if all objects
+ * reachable from {@code want} are desired, such as when serving
+ * a clone.
* @throws IOException
* when some I/O problem occur during reading objects.
*/
public void preparePack(ProgressMonitor countingMonitor,
- ObjectWalk walk,
- final Set<? extends ObjectId> interestingObjects,
- final Set<? extends ObjectId> uninterestingObjects)
+ @NonNull ObjectWalk walk,
+ @NonNull Set<? extends ObjectId> interestingObjects,
+ @NonNull Set<? extends ObjectId> uninterestingObjects)
throws IOException {
if (countingMonitor == null)
countingMonitor = NullProgressMonitor.INSTANCE;
@@ -1551,6 +1548,8 @@ public class PackWriter implements AutoCloseable {
if (zbuf != null) {
out.writeHeader(otp, otp.getCachedSize());
out.write(zbuf);
+ typeStats.cntDeltas++;
+ typeStats.deltaBytes += out.length() - otp.getOffset();
return;
}
}
@@ -1606,17 +1605,12 @@ public class PackWriter implements AutoCloseable {
out.write(packcsum);
}
- private void findObjectsToPack(final ProgressMonitor countingMonitor,
- final ObjectWalk walker, final Set<? extends ObjectId> want,
- Set<? extends ObjectId> have)
- throws MissingObjectException, IOException,
- IncorrectObjectTypeException {
+ private void findObjectsToPack(@NonNull ProgressMonitor countingMonitor,
+ @NonNull ObjectWalk walker, @NonNull Set<? extends ObjectId> want,
+ @NonNull Set<? extends ObjectId> have) throws IOException {
final long countingStart = System.currentTimeMillis();
beginPhase(PackingPhase.COUNTING, countingMonitor, ProgressMonitor.UNKNOWN);
- if (have == null)
- have = Collections.emptySet();
-
stats.interestingObjects = Collections.unmodifiableSet(new HashSet<ObjectId>(want));
stats.uninterestingObjects = Collections.unmodifiableSet(new HashSet<ObjectId>(have));
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/AlwaysFailUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/AlwaysFailUpdate.java
new file mode 100644
index 0000000000..12ef8734c4
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/AlwaysFailUpdate.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.storage.reftree;
+
+import java.io.IOException;
+
+import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+
+/** Update that always rejects with {@code LOCK_FAILURE}. */
+class AlwaysFailUpdate extends RefUpdate {
+ private final RefTreeDatabase refdb;
+
+ AlwaysFailUpdate(RefTreeDatabase refdb, String name) {
+ super(new ObjectIdRef.Unpeeled(Ref.Storage.NEW, name, null));
+ this.refdb = refdb;
+ setCheckConflicting(false);
+ }
+
+ @Override
+ protected RefDatabase getRefDatabase() {
+ return refdb;
+ }
+
+ @Override
+ protected Repository getRepository() {
+ return refdb.getRepository();
+ }
+
+ @Override
+ protected boolean tryLock(boolean deref) throws IOException {
+ return false;
+ }
+
+ @Override
+ protected void unlock() {
+ // No locks are held here.
+ }
+
+ @Override
+ protected Result doUpdate(Result desiredResult) {
+ return Result.LOCK_FAILURE;
+ }
+
+ @Override
+ protected Result doDelete(Result desiredResult) {
+ return Result.LOCK_FAILURE;
+ }
+
+ @Override
+ protected Result doLink(String target) {
+ return Result.LOCK_FAILURE;
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/Command.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/Command.java
new file mode 100644
index 0000000000..dd08375f21
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/Command.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.storage.reftree;
+
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+import static org.eclipse.jgit.lib.Constants.encode;
+import static org.eclipse.jgit.lib.FileMode.TYPE_GITLINK;
+import static org.eclipse.jgit.lib.FileMode.TYPE_SYMLINK;
+import static org.eclipse.jgit.lib.Ref.Storage.NETWORK;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
+
+import java.io.IOException;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.dircache.DirCacheEntry;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevTag;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.transport.ReceiveCommand.Result;
+
+/**
+ * Command to create, update or delete an entry inside a {@link RefTree}.
+ * <p>
+ * Unlike {@link ReceiveCommand} (which can only update a reference to an
+ * {@link ObjectId}), a RefTree Command can also create, modify or delete
+ * symbolic references to a target reference.
+ * <p>
+ * RefTree Commands may wrap a {@code ReceiveCommand} to allow callers to
+ * process an existing ReceiveCommand against a RefTree.
+ * <p>
+ * Commands should be passed into {@link RefTree#apply(java.util.Collection)}
+ * for processing.
+ */
+public class Command {
+ /**
+ * Set unprocessed commands as failed due to transaction aborted.
+ * <p>
+ * If a command is still {@link Result#NOT_ATTEMPTED} it will be set to
+ * {@link Result#REJECTED_OTHER_REASON}. If {@code why} is non-null its
+ * contents will be used as the message for the first command status.
+ *
+ * @param commands
+ * commands to mark as failed.
+ * @param why
+ * optional message to set on the first aborted command.
+ */
+ public static void abort(Iterable<Command> commands, @Nullable String why) {
+ if (why == null || why.isEmpty()) {
+ why = JGitText.get().transactionAborted;
+ }
+ for (Command c : commands) {
+ if (c.getResult() == NOT_ATTEMPTED) {
+ c.setResult(REJECTED_OTHER_REASON, why);
+ why = JGitText.get().transactionAborted;
+ }
+ }
+ }
+
+ private final Ref oldRef;
+ private final Ref newRef;
+ private final ReceiveCommand cmd;
+ private Result result;
+
+ /**
+ * Create a command to create, update or delete a reference.
+ * <p>
+ * At least one of {@code oldRef} or {@code newRef} must be supplied.
+ *
+ * @param oldRef
+ * expected value. Null if the ref should not exist.
+ * @param newRef
+ * desired value, must be peeled if not null and not symbolic.
+ * Null to delete the ref.
+ */
+ public Command(@Nullable Ref oldRef, @Nullable Ref newRef) {
+ this.oldRef = oldRef;
+ this.newRef = newRef;
+ this.cmd = null;
+ this.result = NOT_ATTEMPTED;
+
+ if (oldRef == null && newRef == null) {
+ throw new IllegalArgumentException();
+ }
+ if (newRef != null && !newRef.isPeeled() && !newRef.isSymbolic()) {
+ throw new IllegalArgumentException();
+ }
+ if (oldRef != null && newRef != null
+ && !oldRef.getName().equals(newRef.getName())) {
+ throw new IllegalArgumentException();
+ }
+ }
+
+ /**
+ * Construct a RefTree command wrapped around a ReceiveCommand.
+ *
+ * @param rw
+ * walk instance to peel the {@code newId}.
+ * @param cmd
+ * command received from a push client.
+ * @throws MissingObjectException
+ * {@code oldId} or {@code newId} is missing.
+ * @throws IOException
+ * {@code oldId} or {@code newId} cannot be peeled.
+ */
+ public Command(RevWalk rw, ReceiveCommand cmd)
+ throws MissingObjectException, IOException {
+ this.oldRef = toRef(rw, cmd.getOldId(), cmd.getRefName(), false);
+ this.newRef = toRef(rw, cmd.getNewId(), cmd.getRefName(), true);
+ this.cmd = cmd;
+ }
+
+ static Ref toRef(RevWalk rw, ObjectId id, String name,
+ boolean mustExist) throws MissingObjectException, IOException {
+ if (ObjectId.zeroId().equals(id)) {
+ return null;
+ }
+
+ try {
+ RevObject o = rw.parseAny(id);
+ if (o instanceof RevTag) {
+ RevObject p = rw.peel(o);
+ return new ObjectIdRef.PeeledTag(NETWORK, name, id, p.copy());
+ }
+ return new ObjectIdRef.PeeledNonTag(NETWORK, name, id);
+ } catch (MissingObjectException e) {
+ if (mustExist) {
+ throw e;
+ }
+ return new ObjectIdRef.Unpeeled(NETWORK, name, id);
+ }
+ }
+
+ /** @return name of the reference affected by this command. */
+ public String getRefName() {
+ if (cmd != null) {
+ return cmd.getRefName();
+ } else if (newRef != null) {
+ return newRef.getName();
+ }
+ return oldRef.getName();
+ }
+
+ /**
+ * Set the result of this command.
+ *
+ * @param result
+ * the command result.
+ */
+ public void setResult(Result result) {
+ setResult(result, null);
+ }
+
+ /**
+ * Set the result of this command.
+ *
+ * @param result
+ * the command result.
+ * @param why
+ * optional message explaining the result status.
+ */
+ public void setResult(Result result, @Nullable String why) {
+ if (cmd != null) {
+ cmd.setResult(result, why);
+ } else {
+ this.result = result;
+ }
+ }
+
+ /** @return result of executing this command. */
+ public Result getResult() {
+ return cmd != null ? cmd.getResult() : result;
+ }
+
+ /** @return optional message explaining command failure. */
+ @Nullable
+ public String getMessage() {
+ return cmd != null ? cmd.getMessage() : null;
+ }
+
+ /**
+ * Old peeled reference.
+ *
+ * @return the old reference; null if the command is creating the reference.
+ */
+ @Nullable
+ public Ref getOldRef() {
+ return oldRef;
+ }
+
+ /**
+ * New peeled reference.
+ *
+ * @return the new reference; null if the command is deleting the reference.
+ */
+ @Nullable
+ public Ref getNewRef() {
+ return newRef;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder s = new StringBuilder();
+ append(s, oldRef, "CREATE"); //$NON-NLS-1$
+ s.append(' ');
+ append(s, newRef, "DELETE"); //$NON-NLS-1$
+ s.append(' ').append(getRefName());
+ s.append(' ').append(getResult());
+ if (getMessage() != null) {
+ s.append(' ').append(getMessage());
+ }
+ return s.toString();
+ }
+
+ private static void append(StringBuilder s, Ref r, String nullName) {
+ if (r == null) {
+ s.append(nullName);
+ } else if (r.isSymbolic()) {
+ s.append(r.getTarget().getName());
+ } else {
+ ObjectId id = r.getObjectId();
+ if (id != null) {
+ s.append(id.name());
+ }
+ }
+ }
+
+ /**
+ * Check the entry is consistent with either the old or the new ref.
+ *
+ * @param entry
+ * current entry; null if the entry does not exist.
+ * @return true if entry matches {@link #getOldRef()} or
+ * {@link #getNewRef()}; otherwise false.
+ */
+ boolean checkRef(@Nullable DirCacheEntry entry) {
+ if (entry != null && entry.getRawMode() == 0) {
+ entry = null;
+ }
+ return check(entry, oldRef) || check(entry, newRef);
+ }
+
+ private static boolean check(@Nullable DirCacheEntry cur,
+ @Nullable Ref exp) {
+ if (cur == null) {
+ // Does not exist, ok if oldRef does not exist.
+ return exp == null;
+ } else if (exp == null) {
+ // Expected to not exist, but currently exists, fail.
+ return false;
+ }
+
+ if (exp.isSymbolic()) {
+ String dst = exp.getTarget().getName();
+ return cur.getRawMode() == TYPE_SYMLINK
+ && cur.getObjectId().equals(symref(dst));
+ }
+
+ return cur.getRawMode() == TYPE_GITLINK
+ && cur.getObjectId().equals(exp.getObjectId());
+ }
+
+ static ObjectId symref(String s) {
+ @SuppressWarnings("resource")
+ ObjectInserter.Formatter fmt = new ObjectInserter.Formatter();
+ return fmt.idFor(OBJ_BLOB, encode(s));
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTree.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTree.java
new file mode 100644
index 0000000000..85690c8ca5
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTree.java
@@ -0,0 +1,411 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.storage.reftree;
+
+import static org.eclipse.jgit.lib.Constants.HEAD;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+import static org.eclipse.jgit.lib.Constants.R_REFS;
+import static org.eclipse.jgit.lib.Constants.encode;
+import static org.eclipse.jgit.lib.FileMode.GITLINK;
+import static org.eclipse.jgit.lib.FileMode.SYMLINK;
+import static org.eclipse.jgit.lib.FileMode.TYPE_GITLINK;
+import static org.eclipse.jgit.lib.FileMode.TYPE_SYMLINK;
+import static org.eclipse.jgit.lib.Ref.Storage.NEW;
+import static org.eclipse.jgit.lib.Ref.Storage.PACKED;
+import static org.eclipse.jgit.lib.RefDatabase.MAX_SYMBOLIC_REF_DEPTH;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.LOCK_FAILURE;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.dircache.DirCache;
+import org.eclipse.jgit.dircache.DirCacheBuilder;
+import org.eclipse.jgit.dircache.DirCacheEditor;
+import org.eclipse.jgit.dircache.DirCacheEditor.DeletePath;
+import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
+import org.eclipse.jgit.dircache.DirCacheEntry;
+import org.eclipse.jgit.errors.CorruptObjectException;
+import org.eclipse.jgit.errors.DirCacheNameConflictException;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.SymbolicRef;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.util.RawParseUtils;
+
+/**
+ * Tree of references in the reference graph.
+ * <p>
+ * The root corresponds to the {@code "refs/"} subdirectory, for example the
+ * default reference {@code "refs/heads/master"} is stored at path
+ * {@code "heads/master"} in a {@code RefTree}.
+ * <p>
+ * Normal references are stored as {@link FileMode#GITLINK} tree entries. The
+ * ObjectId in the tree entry is the ObjectId the reference refers to.
+ * <p>
+ * Symbolic references are stored as {@link FileMode#SYMLINK} entries, with the
+ * blob storing the name of the target reference.
+ * <p>
+ * Annotated tags also store the peeled object using a {@code GITLINK} entry
+ * with the suffix <code>" ^"</code> (space carrot), for example
+ * {@code "tags/v1.0"} stores the annotated tag object, while
+ * <code>"tags/v1.0 ^"</code> stores the commit the tag annotates.
+ * <p>
+ * {@code HEAD} is a special case and stored as {@code "..HEAD"}.
+ */
+public class RefTree {
+ /** Suffix applied to GITLINK to indicate its the peeled value of a tag. */
+ public static final String PEELED_SUFFIX = " ^"; //$NON-NLS-1$
+ static final String ROOT_DOTDOT = ".."; //$NON-NLS-1$
+
+ /**
+ * Create an empty reference tree.
+ *
+ * @return a new empty reference tree.
+ */
+ public static RefTree newEmptyTree() {
+ return new RefTree(DirCache.newInCore());
+ }
+
+ /**
+ * Load a reference tree.
+ *
+ * @param reader
+ * reader to scan the reference tree with.
+ * @param tree
+ * the tree to read.
+ * @return the ref tree read from the commit.
+ * @throws IOException
+ * the repository cannot be accessed through the reader.
+ * @throws CorruptObjectException
+ * a tree object is corrupt and cannot be read.
+ * @throws IncorrectObjectTypeException
+ * a tree object wasn't actually a tree.
+ * @throws MissingObjectException
+ * a reference tree object doesn't exist.
+ */
+ public static RefTree read(ObjectReader reader, RevTree tree)
+ throws MissingObjectException, IncorrectObjectTypeException,
+ CorruptObjectException, IOException {
+ return new RefTree(DirCache.read(reader, tree));
+ }
+
+ private DirCache contents;
+ private Map<ObjectId, String> pendingBlobs;
+
+ private RefTree(DirCache dc) {
+ this.contents = dc;
+ }
+
+ /**
+ * Read one reference.
+ * <p>
+ * References are always returned peeled ({@link Ref#isPeeled()} is true).
+ * If the reference points to an annotated tag, the returned reference will
+ * be peeled and contain {@link Ref#getPeeledObjectId()}.
+ * <p>
+ * If the reference is a symbolic reference and the chain depth is less than
+ * {@link org.eclipse.jgit.lib.RefDatabase#MAX_SYMBOLIC_REF_DEPTH} the
+ * returned reference is resolved. If the chain depth is longer, the
+ * symbolic reference is returned without resolving.
+ *
+ * @param reader
+ * to access objects necessary to read the requested reference.
+ * @param name
+ * name of the reference to read.
+ * @return the reference; null if it does not exist.
+ * @throws IOException
+ * cannot read a symbolic reference target.
+ */
+ @Nullable
+ public Ref exactRef(ObjectReader reader, String name) throws IOException {
+ Ref r = readRef(reader, name);
+ if (r == null) {
+ return null;
+ } else if (r.isSymbolic()) {
+ return resolve(reader, r, 0);
+ }
+
+ DirCacheEntry p = contents.getEntry(peeledPath(name));
+ if (p != null && p.getRawMode() == TYPE_GITLINK) {
+ return new ObjectIdRef.PeeledTag(PACKED, r.getName(),
+ r.getObjectId(), p.getObjectId());
+ }
+ return r;
+ }
+
+ private Ref readRef(ObjectReader reader, String name) throws IOException {
+ DirCacheEntry e = contents.getEntry(refPath(name));
+ return e != null ? toRef(reader, e, name) : null;
+ }
+
+ private Ref toRef(ObjectReader reader, DirCacheEntry e, String name)
+ throws IOException {
+ int mode = e.getRawMode();
+ if (mode == TYPE_GITLINK) {
+ ObjectId id = e.getObjectId();
+ return new ObjectIdRef.PeeledNonTag(PACKED, name, id);
+ }
+
+ if (mode == TYPE_SYMLINK) {
+ ObjectId id = e.getObjectId();
+ String n = pendingBlobs != null ? pendingBlobs.get(id) : null;
+ if (n == null) {
+ byte[] bin = reader.open(id, OBJ_BLOB).getCachedBytes();
+ n = RawParseUtils.decode(bin);
+ }
+ Ref dst = new ObjectIdRef.Unpeeled(NEW, n, null);
+ return new SymbolicRef(name, dst);
+ }
+
+ return null; // garbage file or something; not a reference.
+ }
+
+ private Ref resolve(ObjectReader reader, Ref ref, int depth)
+ throws IOException {
+ if (ref.isSymbolic() && depth < MAX_SYMBOLIC_REF_DEPTH) {
+ Ref r = readRef(reader, ref.getTarget().getName());
+ if (r == null) {
+ return ref;
+ }
+ Ref dst = resolve(reader, r, depth + 1);
+ return new SymbolicRef(ref.getName(), dst);
+ }
+ return ref;
+ }
+
+ /**
+ * Attempt a batch of commands against this RefTree.
+ * <p>
+ * The batch is applied atomically, either all commands apply at once, or
+ * they all reject and the RefTree is left unmodified.
+ * <p>
+ * On success (when this method returns {@code true}) the command results
+ * are left as-is (probably {@code NOT_ATTEMPTED}). Result fields are set
+ * only when this method returns {@code false} to indicate failure.
+ *
+ * @param cmdList
+ * to apply. All commands should still have result NOT_ATTEMPTED.
+ * @return true if the commands applied; false if they were rejected.
+ */
+ public boolean apply(Collection<Command> cmdList) {
+ try {
+ DirCacheEditor ed = contents.editor();
+ for (Command cmd : cmdList) {
+ if (!isValidRef(cmd)) {
+ cmd.setResult(REJECTED_OTHER_REASON,
+ JGitText.get().funnyRefname);
+ Command.abort(cmdList, null);
+ return false;
+ }
+ apply(ed, cmd);
+ }
+ ed.finish();
+ return true;
+ } catch (DirCacheNameConflictException e) {
+ String r1 = refName(e.getPath1());
+ String r2 = refName(e.getPath2());
+ for (Command cmd : cmdList) {
+ if (r1.equals(cmd.getRefName())
+ || r2.equals(cmd.getRefName())) {
+ cmd.setResult(LOCK_FAILURE);
+ break;
+ }
+ }
+ Command.abort(cmdList, null);
+ return false;
+ } catch (LockFailureException e) {
+ Command.abort(cmdList, null);
+ return false;
+ }
+ }
+
+ private static boolean isValidRef(Command cmd) {
+ String n = cmd.getRefName();
+ return HEAD.equals(n) || Repository.isValidRefName(n);
+ }
+
+ private void apply(DirCacheEditor ed, final Command cmd) {
+ String path = refPath(cmd.getRefName());
+ Ref oldRef = cmd.getOldRef();
+ final Ref newRef = cmd.getNewRef();
+
+ if (newRef == null) {
+ checkRef(contents.getEntry(path), cmd);
+ ed.add(new DeletePath(path));
+ cleanupPeeledRef(ed, oldRef);
+ return;
+ }
+
+ if (newRef.isSymbolic()) {
+ final String dst = newRef.getTarget().getName();
+ ed.add(new PathEdit(path) {
+ @Override
+ public void apply(DirCacheEntry ent) {
+ checkRef(ent, cmd);
+ ObjectId id = Command.symref(dst);
+ ent.setFileMode(SYMLINK);
+ ent.setObjectId(id);
+ if (pendingBlobs == null) {
+ pendingBlobs = new HashMap<>(4);
+ }
+ pendingBlobs.put(id, dst);
+ }
+ }.setReplace(false));
+ cleanupPeeledRef(ed, oldRef);
+ return;
+ }
+
+ ed.add(new PathEdit(path) {
+ @Override
+ public void apply(DirCacheEntry ent) {
+ checkRef(ent, cmd);
+ ent.setFileMode(GITLINK);
+ ent.setObjectId(newRef.getObjectId());
+ }
+ }.setReplace(false));
+
+ if (newRef.getPeeledObjectId() != null) {
+ ed.add(new PathEdit(peeledPath(newRef.getName())) {
+ @Override
+ public void apply(DirCacheEntry ent) {
+ ent.setFileMode(GITLINK);
+ ent.setObjectId(newRef.getPeeledObjectId());
+ }
+ }.setReplace(false));
+ } else {
+ cleanupPeeledRef(ed, oldRef);
+ }
+ }
+
+ private static void checkRef(@Nullable DirCacheEntry ent, Command cmd) {
+ if (!cmd.checkRef(ent)) {
+ cmd.setResult(LOCK_FAILURE);
+ throw new LockFailureException();
+ }
+ }
+
+ private static void cleanupPeeledRef(DirCacheEditor ed, Ref ref) {
+ if (ref != null && !ref.isSymbolic()
+ && (!ref.isPeeled() || ref.getPeeledObjectId() != null)) {
+ ed.add(new DeletePath(peeledPath(ref.getName())));
+ }
+ }
+
+ /**
+ * Convert a path name in a RefTree to the reference name known by Git.
+ *
+ * @param path
+ * name read from the RefTree structure, for example
+ * {@code "heads/master"}.
+ * @return reference name for the path, {@code "refs/heads/master"}.
+ */
+ public static String refName(String path) {
+ if (path.startsWith(ROOT_DOTDOT)) {
+ return path.substring(2);
+ }
+ return R_REFS + path;
+ }
+
+ static String refPath(String name) {
+ if (name.startsWith(R_REFS)) {
+ return name.substring(R_REFS.length());
+ }
+ return ROOT_DOTDOT + name;
+ }
+
+ private static String peeledPath(String name) {
+ return refPath(name) + PEELED_SUFFIX;
+ }
+
+ /**
+ * Write this reference tree.
+ *
+ * @param inserter
+ * inserter to use when writing trees to the object database.
+ * Caller is responsible for flushing the inserter before trying
+ * to read the objects, or exposing them through a reference.
+ * @return the top level tree.
+ * @throws IOException
+ * a tree could not be written.
+ */
+ public ObjectId writeTree(ObjectInserter inserter) throws IOException {
+ if (pendingBlobs != null) {
+ for (String s : pendingBlobs.values()) {
+ inserter.insert(OBJ_BLOB, encode(s));
+ }
+ pendingBlobs = null;
+ }
+ return contents.writeTree(inserter);
+ }
+
+ /** @return a deep copy of this RefTree. */
+ public RefTree copy() {
+ RefTree r = new RefTree(DirCache.newInCore());
+ DirCacheBuilder b = r.contents.builder();
+ for (int i = 0; i < contents.getEntryCount(); i++) {
+ b.add(new DirCacheEntry(contents.getEntry(i)));
+ }
+ b.finish();
+ if (pendingBlobs != null) {
+ r.pendingBlobs = new HashMap<>(pendingBlobs);
+ }
+ return r;
+ }
+
+ private static class LockFailureException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeBatch.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeBatch.java
new file mode 100644
index 0000000000..a55a9f51e7
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeBatch.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.storage.reftree;
+
+import static org.eclipse.jgit.lib.Constants.OBJ_TREE;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_NONFASTFORWARD;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
+import static org.eclipse.jgit.transport.ReceiveCommand.Type.UPDATE;
+import static org.eclipse.jgit.transport.ReceiveCommand.Type.UPDATE_NONFASTFORWARD;
+
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+/** Batch update a {@link RefTreeDatabase}. */
+class RefTreeBatch extends BatchRefUpdate {
+ private final RefTreeDatabase refdb;
+ private Ref src;
+ private ObjectId parentCommitId;
+ private ObjectId parentTreeId;
+ private RefTree tree;
+ private PersonIdent author;
+ private ObjectId newCommitId;
+
+ RefTreeBatch(RefTreeDatabase refdb) {
+ super(refdb);
+ this.refdb = refdb;
+ }
+
+ @Override
+ public void execute(RevWalk rw, ProgressMonitor monitor)
+ throws IOException {
+ List<Command> todo = new ArrayList<>(getCommands().size());
+ for (ReceiveCommand c : getCommands()) {
+ if (!isAllowNonFastForwards()) {
+ if (c.getType() == UPDATE) {
+ c.updateType(rw);
+ }
+ if (c.getType() == UPDATE_NONFASTFORWARD) {
+ c.setResult(REJECTED_NONFASTFORWARD);
+ ReceiveCommand.abort(getCommands());
+ return;
+ }
+ }
+ todo.add(new Command(rw, c));
+ }
+ init(rw);
+ execute(rw, todo);
+ }
+
+ void init(RevWalk rw) throws IOException {
+ src = refdb.getBootstrap().exactRef(refdb.getTxnCommitted());
+ if (src != null && src.getObjectId() != null) {
+ RevCommit c = rw.parseCommit(src.getObjectId());
+ parentCommitId = c;
+ parentTreeId = c.getTree();
+ tree = RefTree.read(rw.getObjectReader(), c.getTree());
+ } else {
+ parentCommitId = ObjectId.zeroId();
+ parentTreeId = new ObjectInserter.Formatter()
+ .idFor(OBJ_TREE, new byte[] {});
+ tree = RefTree.newEmptyTree();
+ }
+ }
+
+ @Nullable
+ Ref exactRef(ObjectReader reader, String name) throws IOException {
+ return tree.exactRef(reader, name);
+ }
+
+ /**
+ * Execute an update from {@link RefTreeUpdate} or {@link RefTreeRename}.
+ *
+ * @param rw
+ * current RevWalk handling the update or rename.
+ * @param todo
+ * commands to execute. Must never be a bootstrap reference name.
+ * @throws IOException
+ * the storage system is unable to read or write data.
+ */
+ void execute(RevWalk rw, List<Command> todo) throws IOException {
+ for (Command c : todo) {
+ if (c.getResult() != NOT_ATTEMPTED) {
+ Command.abort(todo, null);
+ return;
+ }
+ if (refdb.conflictsWithBootstrap(c.getRefName())) {
+ c.setResult(REJECTED_OTHER_REASON, MessageFormat
+ .format(JGitText.get().invalidRefName, c.getRefName()));
+ Command.abort(todo, null);
+ return;
+ }
+ }
+
+ if (apply(todo) && newCommitId != null) {
+ commit(rw, todo);
+ }
+ }
+
+ private boolean apply(List<Command> todo) throws IOException {
+ if (!tree.apply(todo)) {
+ // apply set rejection information on commands.
+ return false;
+ }
+
+ Repository repo = refdb.getRepository();
+ try (ObjectInserter ins = repo.newObjectInserter()) {
+ CommitBuilder b = new CommitBuilder();
+ b.setTreeId(tree.writeTree(ins));
+ if (parentTreeId.equals(b.getTreeId())) {
+ for (Command c : todo) {
+ c.setResult(OK);
+ }
+ return true;
+ }
+ if (!parentCommitId.equals(ObjectId.zeroId())) {
+ b.setParentId(parentCommitId);
+ }
+
+ author = getRefLogIdent();
+ if (author == null) {
+ author = new PersonIdent(repo);
+ }
+ b.setAuthor(author);
+ b.setCommitter(author);
+ b.setMessage(getRefLogMessage());
+ newCommitId = ins.insert(b);
+ ins.flush();
+ }
+ return true;
+ }
+
+ private void commit(RevWalk rw, List<Command> todo) throws IOException {
+ ReceiveCommand commit = new ReceiveCommand(
+ parentCommitId, newCommitId,
+ refdb.getTxnCommitted());
+ updateBootstrap(rw, commit);
+
+ if (commit.getResult() == OK) {
+ for (Command c : todo) {
+ c.setResult(OK);
+ }
+ } else {
+ Command.abort(todo, commit.getResult().name());
+ }
+ }
+
+ private void updateBootstrap(RevWalk rw, ReceiveCommand commit)
+ throws IOException {
+ BatchRefUpdate u = refdb.getBootstrap().newBatchUpdate();
+ u.setAllowNonFastForwards(true);
+ u.setPushCertificate(getPushCertificate());
+ if (isRefLogDisabled()) {
+ u.disableRefLog();
+ } else {
+ u.setRefLogIdent(author);
+ u.setRefLogMessage(getRefLogMessage(), false);
+ }
+ u.addCommand(commit);
+ u.execute(rw, NullProgressMonitor.INSTANCE);
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeDatabase.java
new file mode 100644
index 0000000000..df93ce88af
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeDatabase.java
@@ -0,0 +1,372 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.storage.reftree;
+
+import static org.eclipse.jgit.lib.Constants.HEAD;
+import static org.eclipse.jgit.lib.Ref.Storage.LOOSE;
+import static org.eclipse.jgit.lib.Ref.Storage.PACKED;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Ref.Storage;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.RefRename;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.SymbolicRef;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevTag;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.util.RefList;
+import org.eclipse.jgit.util.RefMap;
+
+/**
+ * Reference database backed by a {@link RefTree}.
+ * <p>
+ * The storage for RefTreeDatabase has two parts. The main part is a native Git
+ * tree object stored under the {@code refs/txn} namespace. To avoid cycles,
+ * references to {@code refs/txn} are not stored in that tree object, but
+ * instead in a "bootstrap" layer, which is a separate {@link RefDatabase} such
+ * as {@link org.eclipse.jgit.internal.storage.file.RefDirectory} using local
+ * reference files inside of {@code $GIT_DIR/refs}.
+ */
+public class RefTreeDatabase extends RefDatabase {
+ private final Repository repo;
+ private final RefDatabase bootstrap;
+ private final String txnCommitted;
+
+ @Nullable
+ private final String txnNamespace;
+ private volatile Scanner.Result refs;
+
+ /**
+ * Create a RefTreeDb for a repository.
+ *
+ * @param repo
+ * the repository using references in this database.
+ * @param bootstrap
+ * bootstrap reference database storing the references that
+ * anchor the {@link RefTree}.
+ */
+ public RefTreeDatabase(Repository repo, RefDatabase bootstrap) {
+ Config cfg = repo.getConfig();
+ String committed = cfg.getString("reftree", null, "committedRef"); //$NON-NLS-1$ //$NON-NLS-2$
+ if (committed == null || committed.isEmpty()) {
+ committed = "refs/txn/committed"; //$NON-NLS-1$
+ }
+
+ this.repo = repo;
+ this.bootstrap = bootstrap;
+ this.txnNamespace = initNamespace(committed);
+ this.txnCommitted = committed;
+ }
+
+ /**
+ * Create a RefTreeDb for a repository.
+ *
+ * @param repo
+ * the repository using references in this database.
+ * @param bootstrap
+ * bootstrap reference database storing the references that
+ * anchor the {@link RefTree}.
+ * @param txnCommitted
+ * name of the bootstrap reference holding the committed RefTree.
+ */
+ public RefTreeDatabase(Repository repo, RefDatabase bootstrap,
+ String txnCommitted) {
+ this.repo = repo;
+ this.bootstrap = bootstrap;
+ this.txnNamespace = initNamespace(txnCommitted);
+ this.txnCommitted = txnCommitted;
+ }
+
+ private static String initNamespace(String committed) {
+ int s = committed.lastIndexOf('/');
+ if (s < 0) {
+ return null;
+ }
+ return committed.substring(0, s + 1); // Keep trailing '/'.
+ }
+
+ Repository getRepository() {
+ return repo;
+ }
+
+ /**
+ * @return the bootstrap reference database, which must be used to access
+ * {@link #getTxnCommitted()}, {@link #getTxnNamespace()}.
+ */
+ public RefDatabase getBootstrap() {
+ return bootstrap;
+ }
+
+ /** @return name of bootstrap reference anchoring committed RefTree. */
+ public String getTxnCommitted() {
+ return txnCommitted;
+ }
+
+ /**
+ * @return namespace used by bootstrap layer, e.g. {@code refs/txn/}.
+ * Always ends in {@code '/'}.
+ */
+ @Nullable
+ public String getTxnNamespace() {
+ return txnNamespace;
+ }
+
+ @Override
+ public void create() throws IOException {
+ bootstrap.create();
+ }
+
+ @Override
+ public boolean performsAtomicTransactions() {
+ return true;
+ }
+
+ @Override
+ public void refresh() {
+ bootstrap.refresh();
+ }
+
+ @Override
+ public void close() {
+ refs = null;
+ bootstrap.close();
+ }
+
+ @Override
+ public Ref getRef(String name) throws IOException {
+ String[] needle = new String[SEARCH_PATH.length];
+ for (int i = 0; i < SEARCH_PATH.length; i++) {
+ needle[i] = SEARCH_PATH[i] + name;
+ }
+ return firstExactRef(needle);
+ }
+
+ @Override
+ public Ref exactRef(String name) throws IOException {
+ if (!repo.isBare() && name.indexOf('/') < 0 && !HEAD.equals(name)) {
+ // Pass through names like MERGE_HEAD, ORIG_HEAD, FETCH_HEAD.
+ return bootstrap.exactRef(name);
+ } else if (conflictsWithBootstrap(name)) {
+ return null;
+ }
+
+ boolean partial = false;
+ Ref src = bootstrap.exactRef(txnCommitted);
+ Scanner.Result c = refs;
+ if (c == null || !c.refTreeId.equals(idOf(src))) {
+ c = Scanner.scanRefTree(repo, src, prefixOf(name), false);
+ partial = true;
+ }
+
+ Ref r = c.all.get(name);
+ if (r != null && r.isSymbolic()) {
+ r = c.sym.get(name);
+ if (partial && r.getObjectId() == null) {
+ // Attempting exactRef("HEAD") with partial scan will leave
+ // an unresolved symref as its target e.g. refs/heads/master
+ // was not read by the partial scan. Scan everything instead.
+ return getRefs(ALL).get(name);
+ }
+ }
+ return r;
+ }
+
+ private static String prefixOf(String name) {
+ int s = name.lastIndexOf('/');
+ if (s >= 0) {
+ return name.substring(0, s);
+ }
+ return ""; //$NON-NLS-1$
+ }
+
+ @Override
+ public Map<String, Ref> getRefs(String prefix) throws IOException {
+ if (!prefix.isEmpty() && prefix.charAt(prefix.length() - 1) != '/') {
+ return new HashMap<>(0);
+ }
+
+ Ref src = bootstrap.exactRef(txnCommitted);
+ Scanner.Result c = refs;
+ if (c == null || !c.refTreeId.equals(idOf(src))) {
+ c = Scanner.scanRefTree(repo, src, prefix, true);
+ if (prefix.isEmpty()) {
+ refs = c;
+ }
+ }
+ return new RefMap(prefix, RefList.<Ref> emptyList(), c.all, c.sym);
+ }
+
+ private static ObjectId idOf(@Nullable Ref src) {
+ return src != null && src.getObjectId() != null
+ ? src.getObjectId()
+ : ObjectId.zeroId();
+ }
+
+ @Override
+ public List<Ref> getAdditionalRefs() throws IOException {
+ Collection<Ref> txnRefs;
+ if (txnNamespace != null) {
+ txnRefs = bootstrap.getRefs(txnNamespace).values();
+ } else {
+ Ref r = bootstrap.exactRef(txnCommitted);
+ if (r != null && r.getObjectId() != null) {
+ txnRefs = Collections.singleton(r);
+ } else {
+ txnRefs = Collections.emptyList();
+ }
+ }
+
+ List<Ref> otherRefs = bootstrap.getAdditionalRefs();
+ List<Ref> all = new ArrayList<>(txnRefs.size() + otherRefs.size());
+ all.addAll(txnRefs);
+ all.addAll(otherRefs);
+ return all;
+ }
+
+ @Override
+ public Ref peel(Ref ref) throws IOException {
+ Ref i = ref.getLeaf();
+ ObjectId id = i.getObjectId();
+ if (i.isPeeled() || id == null) {
+ return ref;
+ }
+ try (RevWalk rw = new RevWalk(repo)) {
+ RevObject obj = rw.parseAny(id);
+ if (obj instanceof RevTag) {
+ ObjectId p = rw.peel(obj).copy();
+ i = new ObjectIdRef.PeeledTag(PACKED, i.getName(), id, p);
+ } else {
+ i = new ObjectIdRef.PeeledNonTag(PACKED, i.getName(), id);
+ }
+ }
+ return recreate(ref, i);
+ }
+
+ private static Ref recreate(Ref old, Ref leaf) {
+ if (old.isSymbolic()) {
+ Ref dst = recreate(old.getTarget(), leaf);
+ return new SymbolicRef(old.getName(), dst);
+ }
+ return leaf;
+ }
+
+ @Override
+ public boolean isNameConflicting(String name) throws IOException {
+ return conflictsWithBootstrap(name)
+ || !getConflictingNames(name).isEmpty();
+ }
+
+ @Override
+ public BatchRefUpdate newBatchUpdate() {
+ return new RefTreeBatch(this);
+ }
+
+ @Override
+ public RefUpdate newUpdate(String name, boolean detach) throws IOException {
+ if (!repo.isBare() && name.indexOf('/') < 0 && !HEAD.equals(name)) {
+ return bootstrap.newUpdate(name, detach);
+ }
+ if (conflictsWithBootstrap(name)) {
+ return new AlwaysFailUpdate(this, name);
+ }
+
+ Ref r = exactRef(name);
+ if (r == null) {
+ r = new ObjectIdRef.Unpeeled(Storage.NEW, name, null);
+ }
+
+ boolean detaching = detach && r.isSymbolic();
+ if (detaching) {
+ r = new ObjectIdRef.Unpeeled(LOOSE, name, r.getObjectId());
+ }
+
+ RefTreeUpdate u = new RefTreeUpdate(this, r);
+ if (detaching) {
+ u.setDetachingSymbolicRef();
+ }
+ return u;
+ }
+
+ @Override
+ public RefRename newRename(String fromName, String toName)
+ throws IOException {
+ RefUpdate from = newUpdate(fromName, true);
+ RefUpdate to = newUpdate(toName, true);
+ return new RefTreeRename(this, from, to);
+ }
+
+ boolean conflictsWithBootstrap(String name) {
+ if (txnNamespace != null && name.startsWith(txnNamespace)) {
+ return true;
+ } else if (txnCommitted.equals(name)) {
+ return true;
+ }
+
+ if (name.indexOf('/') < 0 && !HEAD.equals(name)) {
+ return true;
+ }
+
+ if (name.length() > txnCommitted.length()
+ && name.charAt(txnCommitted.length()) == '/'
+ && name.startsWith(txnCommitted)) {
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeNames.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeNames.java
new file mode 100644
index 0000000000..c53d6deb21
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeNames.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.storage.reftree;
+
+import org.eclipse.jgit.lib.RefDatabase;
+
+/** Magic reference name logic for RefTrees. */
+public class RefTreeNames {
+ /**
+ * Suffix used on a {@link RefTreeDatabase#getTxnNamespace()} for user data.
+ * <p>
+ * A {@link RefTreeDatabase}'s namespace may include a subspace (e.g.
+ * {@code "refs/txn/stage/"}) containing commit objects from the usual user
+ * portion of the repository (e.g. {@code "refs/heads/"}). These should be
+ * packed by the garbage collector alongside other user content rather than
+ * with the RefTree.
+ */
+ private static final String STAGE = "stage/"; //$NON-NLS-1$
+
+ /**
+ * Determine if the reference is likely to be a RefTree.
+ *
+ * @param refdb
+ * database instance.
+ * @param ref
+ * reference name.
+ * @return {@code true} if the reference is a RefTree.
+ */
+ public static boolean isRefTree(RefDatabase refdb, String ref) {
+ if (refdb instanceof RefTreeDatabase) {
+ RefTreeDatabase b = (RefTreeDatabase) refdb;
+ if (ref.equals(b.getTxnCommitted())) {
+ return true;
+ }
+
+ String namespace = b.getTxnNamespace();
+ if (namespace != null
+ && ref.startsWith(namespace)
+ && !ref.startsWith(namespace + STAGE)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private RefTreeNames() {
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeRename.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeRename.java
new file mode 100644
index 0000000000..5fd7ecdd79
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeRename.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.storage.reftree;
+
+import static org.eclipse.jgit.lib.Constants.HEAD;
+import static org.eclipse.jgit.lib.RefUpdate.Result.REJECTED;
+import static org.eclipse.jgit.lib.RefUpdate.Result.RENAMED;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefRename;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.RefUpdate.Result;
+import org.eclipse.jgit.lib.SymbolicRef;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+/** Single reference rename to {@link RefTreeDatabase}. */
+class RefTreeRename extends RefRename {
+ private final RefTreeDatabase refdb;
+
+ RefTreeRename(RefTreeDatabase refdb, RefUpdate src, RefUpdate dst) {
+ super(src, dst);
+ this.refdb = refdb;
+ }
+
+ @Override
+ protected Result doRename() throws IOException {
+ try (RevWalk rw = new RevWalk(refdb.getRepository())) {
+ RefTreeBatch batch = new RefTreeBatch(refdb);
+ batch.setRefLogIdent(getRefLogIdent());
+ batch.setRefLogMessage(getRefLogMessage(), false);
+ batch.init(rw);
+
+ Ref head = batch.exactRef(rw.getObjectReader(), HEAD);
+ Ref oldRef = batch.exactRef(rw.getObjectReader(), source.getName());
+ if (oldRef == null) {
+ return REJECTED;
+ }
+
+ Ref newRef = asNew(oldRef);
+ List<Command> mv = new ArrayList<>(3);
+ mv.add(new Command(oldRef, null));
+ mv.add(new Command(null, newRef));
+ if (head != null && head.isSymbolic()
+ && head.getTarget().getName().equals(oldRef.getName())) {
+ mv.add(new Command(
+ head,
+ new SymbolicRef(head.getName(), newRef)));
+ }
+ batch.execute(rw, mv);
+ return RefTreeUpdate.translate(mv.get(1).getResult(), RENAMED);
+ }
+ }
+
+ private Ref asNew(Ref src) {
+ String name = destination.getName();
+ if (src.isSymbolic()) {
+ return new SymbolicRef(name, src.getTarget());
+ }
+
+ ObjectId peeled = src.getPeeledObjectId();
+ if (peeled != null) {
+ return new ObjectIdRef.PeeledTag(
+ src.getStorage(),
+ name,
+ src.getObjectId(),
+ peeled);
+ }
+
+ return new ObjectIdRef.PeeledNonTag(
+ src.getStorage(),
+ name,
+ src.getObjectId());
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeUpdate.java
new file mode 100644
index 0000000000..8829c1156a
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeUpdate.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.storage.reftree;
+
+import static org.eclipse.jgit.lib.Ref.Storage.LOOSE;
+import static org.eclipse.jgit.lib.Ref.Storage.NEW;
+
+import java.io.IOException;
+import java.util.Collections;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.SymbolicRef;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevTag;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+/** Single reference update to {@link RefTreeDatabase}. */
+class RefTreeUpdate extends RefUpdate {
+ private final RefTreeDatabase refdb;
+ private RevWalk rw;
+ private RefTreeBatch batch;
+ private Ref oldRef;
+
+ RefTreeUpdate(RefTreeDatabase refdb, Ref ref) {
+ super(ref);
+ this.refdb = refdb;
+ setCheckConflicting(false); // Done automatically by doUpdate.
+ }
+
+ @Override
+ protected RefDatabase getRefDatabase() {
+ return refdb;
+ }
+
+ @Override
+ protected Repository getRepository() {
+ return refdb.getRepository();
+ }
+
+ @Override
+ protected boolean tryLock(boolean deref) throws IOException {
+ rw = new RevWalk(getRepository());
+ batch = new RefTreeBatch(refdb);
+ batch.init(rw);
+ oldRef = batch.exactRef(rw.getObjectReader(), getName());
+ if (oldRef != null && oldRef.getObjectId() != null) {
+ setOldObjectId(oldRef.getObjectId());
+ } else if (oldRef == null && getExpectedOldObjectId() != null) {
+ setOldObjectId(ObjectId.zeroId());
+ }
+ return true;
+ }
+
+ @Override
+ protected void unlock() {
+ batch = null;
+ if (rw != null) {
+ rw.close();
+ rw = null;
+ }
+ }
+
+ @Override
+ protected Result doUpdate(Result desiredResult) throws IOException {
+ return run(newRef(getName(), getNewObjectId()), desiredResult);
+ }
+
+ private Ref newRef(String name, ObjectId id)
+ throws MissingObjectException, IOException {
+ RevObject o = rw.parseAny(id);
+ if (o instanceof RevTag) {
+ RevObject p = rw.peel(o);
+ return new ObjectIdRef.PeeledTag(LOOSE, name, id, p.copy());
+ }
+ return new ObjectIdRef.PeeledNonTag(LOOSE, name, id);
+ }
+
+ @Override
+ protected Result doDelete(Result desiredResult) throws IOException {
+ return run(null, desiredResult);
+ }
+
+ @Override
+ protected Result doLink(String target) throws IOException {
+ Ref dst = new ObjectIdRef.Unpeeled(NEW, target, null);
+ SymbolicRef n = new SymbolicRef(getName(), dst);
+ Result desiredResult = getRef().getStorage() == NEW
+ ? Result.NEW
+ : Result.FORCED;
+ return run(n, desiredResult);
+ }
+
+ private Result run(@Nullable Ref newRef, Result desiredResult)
+ throws IOException {
+ Command c = new Command(oldRef, newRef);
+ batch.setRefLogIdent(getRefLogIdent());
+ batch.setRefLogMessage(getRefLogMessage(), isRefLogIncludingResult());
+ batch.execute(rw, Collections.singletonList(c));
+ return translate(c.getResult(), desiredResult);
+ }
+
+ static Result translate(ReceiveCommand.Result r, Result desiredResult) {
+ switch (r) {
+ case OK:
+ return desiredResult;
+
+ case LOCK_FAILURE:
+ return Result.LOCK_FAILURE;
+
+ case NOT_ATTEMPTED:
+ return Result.NOT_ATTEMPTED;
+
+ case REJECTED_MISSING_OBJECT:
+ return Result.IO_FAILURE;
+
+ case REJECTED_CURRENT_BRANCH:
+ return Result.REJECTED_CURRENT_BRANCH;
+
+ case REJECTED_OTHER_REASON:
+ case REJECTED_NOCREATE:
+ case REJECTED_NODELETE:
+ case REJECTED_NONFASTFORWARD:
+ default:
+ return Result.REJECTED;
+ }
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/Scanner.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/Scanner.java
new file mode 100644
index 0000000000..d383abf316
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/Scanner.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.storage.reftree;
+
+import static org.eclipse.jgit.lib.RefDatabase.MAX_SYMBOLIC_REF_DEPTH;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+import static org.eclipse.jgit.lib.Constants.R_REFS;
+import static org.eclipse.jgit.lib.Constants.encode;
+import static org.eclipse.jgit.lib.FileMode.TYPE_GITLINK;
+import static org.eclipse.jgit.lib.FileMode.TYPE_SYMLINK;
+import static org.eclipse.jgit.lib.FileMode.TYPE_TREE;
+import static org.eclipse.jgit.lib.Ref.Storage.NEW;
+import static org.eclipse.jgit.lib.Ref.Storage.PACKED;
+
+import java.io.IOException;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.SymbolicRef;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.AbstractTreeIterator;
+import org.eclipse.jgit.treewalk.CanonicalTreeParser;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.util.Paths;
+import org.eclipse.jgit.util.RawParseUtils;
+import org.eclipse.jgit.util.RefList;
+
+/** A tree parser that extracts references from a {@link RefTree}. */
+class Scanner {
+ private static final int MAX_SYMLINK_BYTES = 10 << 10;
+ private static final byte[] BINARY_R_REFS = encode(R_REFS);
+ private static final byte[] REFS_DOT_DOT = encode("refs/.."); //$NON-NLS-1$
+
+ static class Result {
+ final ObjectId refTreeId;
+ final RefList<Ref> all;
+ final RefList<Ref> sym;
+
+ Result(ObjectId id, RefList<Ref> all, RefList<Ref> sym) {
+ this.refTreeId = id;
+ this.all = all;
+ this.sym = sym;
+ }
+ }
+
+ /**
+ * Scan a {@link RefTree} and parse entries into {@link Ref} instances.
+ *
+ * @param repo
+ * source repository containing the commit and tree objects that
+ * make up the RefTree.
+ * @param src
+ * bootstrap reference such as {@code refs/txn/committed} to read
+ * the reference tree tip from. The current ObjectId will be
+ * included in {@link Result#refTreeId}.
+ * @param prefix
+ * if non-empty a reference prefix to scan only a subdirectory.
+ * For example {@code prefix = "refs/heads/"} will limit the scan
+ * to only the {@code "heads"} directory of the RefTree, avoiding
+ * other directories like {@code "tags"}. Empty string reads all
+ * entries in the RefTree.
+ * @param recursive
+ * if true recurse into subdirectories of the reference tree;
+ * false to read only one level. Callers may use false during an
+ * implementation of {@code exactRef(String)} where only one
+ * reference is needed out of a specific subtree.
+ * @return sorted list of references after parsing.
+ * @throws IOException
+ * tree cannot be accessed from the repository.
+ */
+ static Result scanRefTree(Repository repo, @Nullable Ref src, String prefix,
+ boolean recursive) throws IOException {
+ RefList.Builder<Ref> all = new RefList.Builder<>();
+ RefList.Builder<Ref> sym = new RefList.Builder<>();
+
+ ObjectId srcId;
+ if (src != null && src.getObjectId() != null) {
+ try (ObjectReader reader = repo.newObjectReader()) {
+ srcId = src.getObjectId();
+ scan(reader, srcId, prefix, recursive, all, sym);
+ }
+ } else {
+ srcId = ObjectId.zeroId();
+ }
+
+ RefList<Ref> aList = all.toRefList();
+ for (int idx = 0; idx < sym.size();) {
+ Ref s = sym.get(idx);
+ Ref r = resolve(s, 0, aList);
+ if (r != null) {
+ sym.set(idx++, r);
+ } else {
+ // Remove broken symbolic reference, they don't exist.
+ sym.remove(idx);
+ int rm = aList.find(s.getName());
+ if (0 <= rm) {
+ aList = aList.remove(rm);
+ }
+ }
+ }
+ return new Result(srcId, aList, sym.toRefList());
+ }
+
+ private static void scan(ObjectReader reader, AnyObjectId srcId,
+ String prefix, boolean recursive,
+ RefList.Builder<Ref> all, RefList.Builder<Ref> sym)
+ throws IncorrectObjectTypeException, IOException {
+ CanonicalTreeParser p = createParserAtPath(reader, srcId, prefix);
+ if (p == null) {
+ return;
+ }
+
+ while (!p.eof()) {
+ int mode = p.getEntryRawMode();
+ if (mode == TYPE_TREE) {
+ if (recursive) {
+ p = p.createSubtreeIterator(reader);
+ } else {
+ p = p.next();
+ }
+ continue;
+ }
+
+ if (!curElementHasPeelSuffix(p)) {
+ Ref r = toRef(reader, mode, p);
+ if (r != null) {
+ all.add(r);
+ if (r.isSymbolic()) {
+ sym.add(r);
+ }
+ }
+ } else if (mode == TYPE_GITLINK) {
+ peel(all, p);
+ }
+ p = p.next();
+ }
+ }
+
+ private static CanonicalTreeParser createParserAtPath(ObjectReader reader,
+ AnyObjectId srcId, String prefix) throws IOException {
+ ObjectId root = toTree(reader, srcId);
+ if (prefix.isEmpty()) {
+ return new CanonicalTreeParser(BINARY_R_REFS, reader, root);
+ }
+
+ String dir = RefTree.refPath(Paths.stripTrailingSeparator(prefix));
+ TreeWalk tw = TreeWalk.forPath(reader, dir, root);
+ if (tw == null || !tw.isSubtree()) {
+ return null;
+ }
+
+ ObjectId id = tw.getObjectId(0);
+ return new CanonicalTreeParser(encode(prefix), reader, id);
+ }
+
+ private static Ref resolve(Ref ref, int depth, RefList<Ref> refs)
+ throws IOException {
+ if (!ref.isSymbolic()) {
+ return ref;
+ } else if (MAX_SYMBOLIC_REF_DEPTH <= depth) {
+ return null;
+ }
+
+ Ref r = refs.get(ref.getTarget().getName());
+ if (r == null) {
+ return ref;
+ }
+
+ Ref dst = resolve(r, depth + 1, refs);
+ if (dst == null) {
+ return null;
+ }
+ return new SymbolicRef(ref.getName(), dst);
+ }
+
+ @SuppressWarnings("resource")
+ private static RevTree toTree(ObjectReader reader, AnyObjectId id)
+ throws IOException {
+ return new RevWalk(reader).parseTree(id);
+ }
+
+ private static boolean curElementHasPeelSuffix(AbstractTreeIterator itr) {
+ int n = itr.getEntryPathLength();
+ byte[] c = itr.getEntryPathBuffer();
+ return n > 2 && c[n - 2] == ' ' && c[n - 1] == '^';
+ }
+
+ private static void peel(RefList.Builder<Ref> all, CanonicalTreeParser p) {
+ String name = refName(p, true);
+ for (int idx = all.size() - 1; 0 <= idx; idx--) {
+ Ref r = all.get(idx);
+ int cmp = r.getName().compareTo(name);
+ if (cmp == 0) {
+ all.set(idx, new ObjectIdRef.PeeledTag(PACKED, r.getName(),
+ r.getObjectId(), p.getEntryObjectId()));
+ break;
+ } else if (cmp < 0) {
+ // Stray peeled name without matching base name; skip entry.
+ break;
+ }
+ }
+ }
+
+ private static Ref toRef(ObjectReader reader, int mode,
+ CanonicalTreeParser p) throws IOException {
+ if (mode == TYPE_GITLINK) {
+ String name = refName(p, false);
+ ObjectId id = p.getEntryObjectId();
+ return new ObjectIdRef.PeeledNonTag(PACKED, name, id);
+
+ } else if (mode == TYPE_SYMLINK) {
+ ObjectId id = p.getEntryObjectId();
+ byte[] bin = reader.open(id, OBJ_BLOB)
+ .getCachedBytes(MAX_SYMLINK_BYTES);
+ String dst = RawParseUtils.decode(bin);
+ Ref trg = new ObjectIdRef.Unpeeled(NEW, dst, null);
+ String name = refName(p, false);
+ return new SymbolicRef(name, trg);
+ }
+ return null;
+ }
+
+ private static String refName(CanonicalTreeParser p, boolean peel) {
+ byte[] buf = p.getEntryPathBuffer();
+ int len = p.getEntryPathLength();
+ if (peel) {
+ len -= 2;
+ }
+ int ptr = 0;
+ if (RawParseUtils.match(buf, ptr, REFS_DOT_DOT) > 0) {
+ ptr = 7;
+ }
+ return RawParseUtils.decode(buf, ptr, len);
+ }
+
+ private Scanner() {
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java
index 45dd7ee1ac..670f9a9e14 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java
@@ -109,7 +109,8 @@ public class BaseRepositoryBuilder<B extends BaseRepositoryBuilder, R extends Re
int pathStart = 8;
int lineEnd = RawParseUtils.nextLF(content, pathStart);
- if (content[lineEnd - 1] == '\n')
+ while (content[lineEnd - 1] == '\n' ||
+ (content[lineEnd - 1] == '\r' && SystemReader.getInstance().isWindows()))
lineEnd--;
if (lineEnd == pathStart)
throw new IOException(MessageFormat.format(
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BlobBasedConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BlobBasedConfig.java
index cbb2f5b856..7d52991df0 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BlobBasedConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BlobBasedConfig.java
@@ -79,7 +79,15 @@ public class BlobBasedConfig extends Config {
public BlobBasedConfig(Config base, final byte[] blob)
throws ConfigInvalidException {
super(base);
- fromText(RawParseUtils.decode(blob));
+ final String decoded;
+ if (blob.length >= 3 && blob[0] == (byte) 0xEF
+ && blob[1] == (byte) 0xBB && blob[2] == (byte) 0xBF) {
+ decoded = RawParseUtils.decode(RawParseUtils.UTF8_CHARSET,
+ blob, 3, blob.length);
+ } else {
+ decoded = RawParseUtils.decode(blob);
+ }
+ fromText(decoded);
}
/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileTreeEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileTreeEntry.java
deleted file mode 100644
index 6811417ee0..0000000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileTreeEntry.java
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org>
- * and other copyright owners as documented in the project's IP log.
- *
- * This program and the accompanying materials are made available
- * under the terms of the Eclipse Distribution License v1.0 which
- * accompanies this distribution, is reproduced below, and is
- * available at http://www.eclipse.org/org/documents/edl-v10.php
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or
- * without modification, are permitted provided that the following
- * conditions are met:
- *
- * - Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * - Redistributions in binary form must reproduce the above
- * copyright notice, this list of conditions and the following
- * disclaimer in the documentation and/or other materials provided
- * with the distribution.
- *
- * - Neither the name of the Eclipse Foundation, Inc. nor the
- * names of its contributors may be used to endorse or promote
- * products derived from this software without specific prior
- * written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
- * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
- * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
- * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
- * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
- * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
- * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
- * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package org.eclipse.jgit.lib;
-
-import java.io.IOException;
-
-/**
- * A representation of a file (blob) object in a {@link Tree}.
- *
- * @deprecated To look up information about a single path, use
- * {@link org.eclipse.jgit.treewalk.TreeWalk#forPath(Repository, String, org.eclipse.jgit.revwalk.RevTree)}.
- * To lookup information about multiple paths at once, use a
- * {@link org.eclipse.jgit.treewalk.TreeWalk} and obtain the current entry's
- * information from its getter methods.
- */
-@Deprecated
-public class FileTreeEntry extends TreeEntry {
- private FileMode mode;
-
- /**
- * Constructor for a File (blob) object.
- *
- * @param parent
- * The {@link Tree} holding this object (or null)
- * @param id
- * the SHA-1 of the blob (or null for a yet unhashed file)
- * @param nameUTF8
- * raw object name in the parent tree
- * @param execute
- * true if the executable flag is set
- */
- public FileTreeEntry(final Tree parent, final ObjectId id,
- final byte[] nameUTF8, final boolean execute) {
- super(parent, id, nameUTF8);
- setExecutable(execute);
- }
-
- public FileMode getMode() {
- return mode;
- }
-
- /**
- * @return true if this file is executable
- */
- public boolean isExecutable() {
- return getMode().equals(FileMode.EXECUTABLE_FILE);
- }
-
- /**
- * @param execute set/reset the executable flag
- */
- public void setExecutable(final boolean execute) {
- mode = execute ? FileMode.EXECUTABLE_FILE : FileMode.REGULAR_FILE;
- }
-
- /**
- * @return an {@link ObjectLoader} that will return the data
- * @throws IOException
- */
- public ObjectLoader openReader() throws IOException {
- return getRepository().open(getId(), Constants.OBJ_BLOB);
- }
-
- public String toString() {
- final StringBuilder r = new StringBuilder();
- r.append(ObjectId.toString(getId()));
- r.append(' ');
- r.append(isExecutable() ? 'X' : 'F');
- r.append(' ');
- r.append(getFullName());
- return r.toString();
- }
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java
index a7a67a8812..0b5efd77d4 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java
@@ -44,21 +44,58 @@
package org.eclipse.jgit.lib;
-import static org.eclipse.jgit.util.RawParseUtils.match;
+import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH;
+import static org.eclipse.jgit.lib.Constants.OBJECT_ID_STRING_LENGTH;
+import static org.eclipse.jgit.lib.Constants.OBJ_BAD;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT;
+import static org.eclipse.jgit.lib.Constants.OBJ_TAG;
+import static org.eclipse.jgit.lib.Constants.OBJ_TREE;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.BAD_DATE;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.BAD_EMAIL;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.BAD_OBJECT_SHA1;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.BAD_PARENT_SHA1;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.BAD_TIMEZONE;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.BAD_TREE_SHA1;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.BAD_UTF8;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.DUPLICATE_ENTRIES;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.EMPTY_NAME;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.FULL_PATHNAME;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.HAS_DOT;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.HAS_DOTDOT;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.HAS_DOTGIT;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.MISSING_AUTHOR;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.MISSING_COMMITTER;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.MISSING_EMAIL;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.MISSING_OBJECT;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.MISSING_SPACE_BEFORE_DATE;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.MISSING_TAG_ENTRY;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.MISSING_TREE;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.MISSING_TYPE_ENTRY;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.NULL_SHA1;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.TREE_NOT_SORTED;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.UNKNOWN_TYPE;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.WIN32_BAD_NAME;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.ZERO_PADDED_FILEMODE;
+import static org.eclipse.jgit.util.Paths.compare;
+import static org.eclipse.jgit.util.Paths.compareSameName;
import static org.eclipse.jgit.util.RawParseUtils.nextLF;
import static org.eclipse.jgit.util.RawParseUtils.parseBase10;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
import java.text.MessageFormat;
+import java.text.Normalizer;
+import java.util.EnumSet;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.annotations.Nullable;
import org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.util.MutableInteger;
import org.eclipse.jgit.util.RawParseUtils;
+import org.eclipse.jgit.util.StringUtils;
/**
* Verifies that an object is formatted correctly.
@@ -99,31 +136,135 @@ public class ObjectChecker {
/** Header "tagger " */
public static final byte[] tagger = Constants.encodeASCII("tagger "); //$NON-NLS-1$
- private final MutableObjectId tempId = new MutableObjectId();
-
- private final MutableInteger ptrout = new MutableInteger();
+ /**
+ * Potential issues identified by the checker.
+ *
+ * @since 4.2
+ */
+ public enum ErrorType {
+ // @formatter:off
+ // These names match git-core so that fsck section keys also match.
+ /***/ NULL_SHA1,
+ /***/ DUPLICATE_ENTRIES,
+ /***/ TREE_NOT_SORTED,
+ /***/ ZERO_PADDED_FILEMODE,
+ /***/ EMPTY_NAME,
+ /***/ FULL_PATHNAME,
+ /***/ HAS_DOT,
+ /***/ HAS_DOTDOT,
+ /***/ HAS_DOTGIT,
+ /***/ BAD_OBJECT_SHA1,
+ /***/ BAD_PARENT_SHA1,
+ /***/ BAD_TREE_SHA1,
+ /***/ MISSING_AUTHOR,
+ /***/ MISSING_COMMITTER,
+ /***/ MISSING_OBJECT,
+ /***/ MISSING_TREE,
+ /***/ MISSING_TYPE_ENTRY,
+ /***/ MISSING_TAG_ENTRY,
+ /***/ BAD_DATE,
+ /***/ BAD_EMAIL,
+ /***/ BAD_TIMEZONE,
+ /***/ MISSING_EMAIL,
+ /***/ MISSING_SPACE_BEFORE_DATE,
+ /***/ UNKNOWN_TYPE,
+
+ // These are unique to JGit.
+ /***/ WIN32_BAD_NAME,
+ /***/ BAD_UTF8;
+ // @formatter:on
+
+ /** @return camelCaseVersion of the name. */
+ public String getMessageId() {
+ String n = name();
+ StringBuilder r = new StringBuilder(n.length());
+ for (int i = 0; i < n.length(); i++) {
+ char c = n.charAt(i);
+ if (c != '_') {
+ r.append(StringUtils.toLowerCase(c));
+ } else {
+ r.append(n.charAt(++i));
+ }
+ }
+ return r.toString();
+ }
+ }
- private boolean allowZeroMode;
+ private final MutableObjectId tempId = new MutableObjectId();
+ private final MutableInteger bufPtr = new MutableInteger();
+ private EnumSet<ErrorType> errors = EnumSet.allOf(ErrorType.class);
+ private ObjectIdSet skipList;
private boolean allowInvalidPersonIdent;
private boolean windows;
private boolean macosx;
/**
+ * Enable accepting specific malformed (but not horribly broken) objects.
+ *
+ * @param objects
+ * collection of object names known to be broken in a non-fatal
+ * way that should be ignored by the checker.
+ * @return {@code this}
+ * @since 4.2
+ */
+ public ObjectChecker setSkipList(@Nullable ObjectIdSet objects) {
+ skipList = objects;
+ return this;
+ }
+
+ /**
+ * Configure error types to be ignored across all objects.
+ *
+ * @param ids
+ * error types to ignore. The caller's set is copied.
+ * @return {@code this}
+ * @since 4.2
+ */
+ public ObjectChecker setIgnore(@Nullable Set<ErrorType> ids) {
+ errors = EnumSet.allOf(ErrorType.class);
+ if (ids != null) {
+ errors.removeAll(ids);
+ }
+ return this;
+ }
+
+ /**
+ * Add message type to be ignored across all objects.
+ *
+ * @param id
+ * error type to ignore.
+ * @param ignore
+ * true to ignore this error; false to treat the error as an
+ * error and throw.
+ * @return {@code this}
+ * @since 4.2
+ */
+ public ObjectChecker setIgnore(ErrorType id, boolean ignore) {
+ if (ignore) {
+ errors.remove(id);
+ } else {
+ errors.add(id);
+ }
+ return this;
+ }
+
+ /**
* Enable accepting leading zero mode in tree entries.
* <p>
* Some broken Git libraries generated leading zeros in the mode part of
* tree entries. This is technically incorrect but gracefully allowed by
* git-core. JGit rejects such trees by default, but may need to accept
* them on broken histories.
+ * <p>
+ * Same as {@code setIgnore(ZERO_PADDED_FILEMODE, allow)}.
*
* @param allow allow leading zero mode.
* @return {@code this}.
* @since 3.4
*/
public ObjectChecker setAllowLeadingZeroFileMode(boolean allow) {
- allowZeroMode = allow;
- return this;
+ return setIgnore(ZERO_PADDED_FILEMODE, allow);
}
/**
@@ -184,62 +325,117 @@ public class ObjectChecker {
* @throws CorruptObjectException
* if an error is identified.
*/
- public void check(final int objType, final byte[] raw)
+ public void check(int objType, byte[] raw)
+ throws CorruptObjectException {
+ check(idFor(objType, raw), objType, raw);
+ }
+
+ /**
+ * Check an object for parsing errors.
+ *
+ * @param id
+ * identify of the object being checked.
+ * @param objType
+ * type of the object. Must be a valid object type code in
+ * {@link Constants}.
+ * @param raw
+ * the raw data which comprises the object. This should be in the
+ * canonical format (that is the format used to generate the
+ * ObjectId of the object). The array is never modified.
+ * @throws CorruptObjectException
+ * if an error is identified.
+ * @since 4.2
+ */
+ public void check(@Nullable AnyObjectId id, int objType, byte[] raw)
throws CorruptObjectException {
switch (objType) {
- case Constants.OBJ_COMMIT:
- checkCommit(raw);
+ case OBJ_COMMIT:
+ checkCommit(id, raw);
break;
- case Constants.OBJ_TAG:
- checkTag(raw);
+ case OBJ_TAG:
+ checkTag(id, raw);
break;
- case Constants.OBJ_TREE:
- checkTree(raw);
+ case OBJ_TREE:
+ checkTree(id, raw);
break;
- case Constants.OBJ_BLOB:
+ case OBJ_BLOB:
checkBlob(raw);
break;
default:
- throw new CorruptObjectException(MessageFormat.format(
+ report(UNKNOWN_TYPE, id, MessageFormat.format(
JGitText.get().corruptObjectInvalidType2,
Integer.valueOf(objType)));
}
}
- private int id(final byte[] raw, final int ptr) {
+ private boolean checkId(byte[] raw) {
+ int p = bufPtr.value;
try {
- tempId.fromString(raw, ptr);
- return ptr + Constants.OBJECT_ID_STRING_LENGTH;
+ tempId.fromString(raw, p);
} catch (IllegalArgumentException e) {
- return -1;
+ bufPtr.value = nextLF(raw, p);
+ return false;
+ }
+
+ p += OBJECT_ID_STRING_LENGTH;
+ if (raw[p] == '\n') {
+ bufPtr.value = p + 1;
+ return true;
}
+ bufPtr.value = nextLF(raw, p);
+ return false;
}
- private int personIdent(final byte[] raw, int ptr) {
- if (allowInvalidPersonIdent)
- return nextLF(raw, ptr) - 1;
+ private void checkPersonIdent(byte[] raw, @Nullable AnyObjectId id)
+ throws CorruptObjectException {
+ if (allowInvalidPersonIdent) {
+ bufPtr.value = nextLF(raw, bufPtr.value);
+ return;
+ }
- final int emailB = nextLF(raw, ptr, '<');
- if (emailB == ptr || raw[emailB - 1] != '<')
- return -1;
+ final int emailB = nextLF(raw, bufPtr.value, '<');
+ if (emailB == bufPtr.value || raw[emailB - 1] != '<') {
+ report(MISSING_EMAIL, id, JGitText.get().corruptObjectMissingEmail);
+ bufPtr.value = nextLF(raw, bufPtr.value);
+ return;
+ }
final int emailE = nextLF(raw, emailB, '>');
- if (emailE == emailB || raw[emailE - 1] != '>')
- return -1;
- if (emailE == raw.length || raw[emailE] != ' ')
- return -1;
+ if (emailE == emailB || raw[emailE - 1] != '>') {
+ report(BAD_EMAIL, id, JGitText.get().corruptObjectBadEmail);
+ bufPtr.value = nextLF(raw, bufPtr.value);
+ return;
+ }
+ if (emailE == raw.length || raw[emailE] != ' ') {
+ report(MISSING_SPACE_BEFORE_DATE, id,
+ JGitText.get().corruptObjectBadDate);
+ bufPtr.value = nextLF(raw, bufPtr.value);
+ return;
+ }
+
+ parseBase10(raw, emailE + 1, bufPtr); // when
+ if (emailE + 1 == bufPtr.value || bufPtr.value == raw.length
+ || raw[bufPtr.value] != ' ') {
+ report(BAD_DATE, id, JGitText.get().corruptObjectBadDate);
+ bufPtr.value = nextLF(raw, bufPtr.value);
+ return;
+ }
- parseBase10(raw, emailE + 1, ptrout); // when
- ptr = ptrout.value;
- if (emailE + 1 == ptr)
- return -1;
- if (ptr == raw.length || raw[ptr] != ' ')
- return -1;
+ int p = bufPtr.value + 1;
+ parseBase10(raw, p, bufPtr); // tz offset
+ if (p == bufPtr.value) {
+ report(BAD_TIMEZONE, id, JGitText.get().corruptObjectBadTimezone);
+ bufPtr.value = nextLF(raw, bufPtr.value);
+ return;
+ }
- parseBase10(raw, ptr + 1, ptrout); // tz offset
- if (ptr + 1 == ptrout.value)
- return -1;
- return ptrout.value;
+ p = bufPtr.value;
+ if (raw[p] == '\n') {
+ bufPtr.value = p + 1;
+ } else {
+ report(BAD_TIMEZONE, id, JGitText.get().corruptObjectBadTimezone);
+ bufPtr.value = nextLF(raw, p);
+ }
}
/**
@@ -250,36 +446,50 @@ public class ObjectChecker {
* @throws CorruptObjectException
* if any error was detected.
*/
- public void checkCommit(final byte[] raw) throws CorruptObjectException {
- int ptr = 0;
+ public void checkCommit(byte[] raw) throws CorruptObjectException {
+ checkCommit(idFor(OBJ_COMMIT, raw), raw);
+ }
- if ((ptr = match(raw, ptr, tree)) < 0)
- throw new CorruptObjectException(
- JGitText.get().corruptObjectNotreeHeader);
- if ((ptr = id(raw, ptr)) < 0 || raw[ptr++] != '\n')
- throw new CorruptObjectException(
- JGitText.get().corruptObjectInvalidTree);
+ /**
+ * Check a commit for errors.
+ *
+ * @param id
+ * identity of the object being checked.
+ * @param raw
+ * the commit data. The array is never modified.
+ * @throws CorruptObjectException
+ * if any error was detected.
+ * @since 4.2
+ */
+ public void checkCommit(@Nullable AnyObjectId id, byte[] raw)
+ throws CorruptObjectException {
+ bufPtr.value = 0;
- while (match(raw, ptr, parent) >= 0) {
- ptr += parent.length;
- if ((ptr = id(raw, ptr)) < 0 || raw[ptr++] != '\n')
- throw new CorruptObjectException(
+ if (!match(raw, tree)) {
+ report(MISSING_TREE, id, JGitText.get().corruptObjectNotreeHeader);
+ } else if (!checkId(raw)) {
+ report(BAD_TREE_SHA1, id, JGitText.get().corruptObjectInvalidTree);
+ }
+
+ while (match(raw, parent)) {
+ if (!checkId(raw)) {
+ report(BAD_PARENT_SHA1, id,
JGitText.get().corruptObjectInvalidParent);
+ }
}
- if ((ptr = match(raw, ptr, author)) < 0)
- throw new CorruptObjectException(
- JGitText.get().corruptObjectNoAuthor);
- if ((ptr = personIdent(raw, ptr)) < 0 || raw[ptr++] != '\n')
- throw new CorruptObjectException(
- JGitText.get().corruptObjectInvalidAuthor);
+ if (match(raw, author)) {
+ checkPersonIdent(raw, id);
+ } else {
+ report(MISSING_AUTHOR, id, JGitText.get().corruptObjectNoAuthor);
+ }
- if ((ptr = match(raw, ptr, committer)) < 0)
- throw new CorruptObjectException(
+ if (match(raw, committer)) {
+ checkPersonIdent(raw, id);
+ } else {
+ report(MISSING_COMMITTER, id,
JGitText.get().corruptObjectNoCommitter);
- if ((ptr = personIdent(raw, ptr)) < 0 || raw[ptr++] != '\n')
- throw new CorruptObjectException(
- JGitText.get().corruptObjectInvalidCommitter);
+ }
}
/**
@@ -290,50 +500,47 @@ public class ObjectChecker {
* @throws CorruptObjectException
* if any error was detected.
*/
- public void checkTag(final byte[] raw) throws CorruptObjectException {
- int ptr = 0;
+ public void checkTag(byte[] raw) throws CorruptObjectException {
+ checkTag(idFor(OBJ_TAG, raw), raw);
+ }
- if ((ptr = match(raw, ptr, object)) < 0)
- throw new CorruptObjectException(
+ /**
+ * Check an annotated tag for errors.
+ *
+ * @param id
+ * identity of the object being checked.
+ * @param raw
+ * the tag data. The array is never modified.
+ * @throws CorruptObjectException
+ * if any error was detected.
+ * @since 4.2
+ */
+ public void checkTag(@Nullable AnyObjectId id, byte[] raw)
+ throws CorruptObjectException {
+ bufPtr.value = 0;
+ if (!match(raw, object)) {
+ report(MISSING_OBJECT, id,
JGitText.get().corruptObjectNoObjectHeader);
- if ((ptr = id(raw, ptr)) < 0 || raw[ptr++] != '\n')
- throw new CorruptObjectException(
+ } else if (!checkId(raw)) {
+ report(BAD_OBJECT_SHA1, id,
JGitText.get().corruptObjectInvalidObject);
+ }
- if ((ptr = match(raw, ptr, type)) < 0)
- throw new CorruptObjectException(
+ if (!match(raw, type)) {
+ report(MISSING_TYPE_ENTRY, id,
JGitText.get().corruptObjectNoTypeHeader);
- ptr = nextLF(raw, ptr);
+ }
+ bufPtr.value = nextLF(raw, bufPtr.value);
- if ((ptr = match(raw, ptr, tag)) < 0)
- throw new CorruptObjectException(
+ if (!match(raw, tag)) {
+ report(MISSING_TAG_ENTRY, id,
JGitText.get().corruptObjectNoTagHeader);
- ptr = nextLF(raw, ptr);
-
- if ((ptr = match(raw, ptr, tagger)) > 0) {
- if ((ptr = personIdent(raw, ptr)) < 0 || raw[ptr++] != '\n')
- throw new CorruptObjectException(
- JGitText.get().corruptObjectInvalidTagger);
}
- }
-
- private static int lastPathChar(final int mode) {
- return FileMode.TREE.equals(mode) ? '/' : '\0';
- }
+ bufPtr.value = nextLF(raw, bufPtr.value);
- private static int pathCompare(final byte[] raw, int aPos, final int aEnd,
- final int aMode, int bPos, final int bEnd, final int bMode) {
- while (aPos < aEnd && bPos < bEnd) {
- final int cmp = (raw[aPos++] & 0xff) - (raw[bPos++] & 0xff);
- if (cmp != 0)
- return cmp;
+ if (match(raw, tagger)) {
+ checkPersonIdent(raw, id);
}
-
- if (aPos < aEnd)
- return (raw[aPos] & 0xff) - lastPathChar(bMode);
- if (bPos < bEnd)
- return lastPathChar(aMode) - (raw[bPos] & 0xff);
- return 0;
}
private static boolean duplicateName(final byte[] raw,
@@ -363,8 +570,9 @@ public class ObjectChecker {
if (nextNamePos + 1 == nextPtr)
return false;
- final int cmp = pathCompare(raw, thisNamePos, thisNameEnd,
- FileMode.TREE.getBits(), nextNamePos, nextPtr - 1, nextMode);
+ int cmp = compareSameName(
+ raw, thisNamePos, thisNameEnd,
+ raw, nextNamePos, nextPtr - 1, nextMode);
if (cmp < 0)
return false;
else if (cmp == 0)
@@ -382,7 +590,23 @@ public class ObjectChecker {
* @throws CorruptObjectException
* if any error was detected.
*/
- public void checkTree(final byte[] raw) throws CorruptObjectException {
+ public void checkTree(byte[] raw) throws CorruptObjectException {
+ checkTree(idFor(OBJ_TREE, raw), raw);
+ }
+
+ /**
+ * Check a canonical formatted tree for errors.
+ *
+ * @param id
+ * identity of the object being checked.
+ * @param raw
+ * the raw tree data. The array is never modified.
+ * @throws CorruptObjectException
+ * if any error was detected.
+ * @since 4.2
+ */
+ public void checkTree(@Nullable AnyObjectId id, byte[] raw)
+ throws CorruptObjectException {
final int sz = raw.length;
int ptr = 0;
int lastNameB = 0, lastNameE = 0, lastMode = 0;
@@ -393,74 +617,90 @@ public class ObjectChecker {
while (ptr < sz) {
int thisMode = 0;
for (;;) {
- if (ptr == sz)
+ if (ptr == sz) {
throw new CorruptObjectException(
JGitText.get().corruptObjectTruncatedInMode);
+ }
final byte c = raw[ptr++];
if (' ' == c)
break;
- if (c < '0' || c > '7')
+ if (c < '0' || c > '7') {
throw new CorruptObjectException(
JGitText.get().corruptObjectInvalidModeChar);
- if (thisMode == 0 && c == '0' && !allowZeroMode)
- throw new CorruptObjectException(
+ }
+ if (thisMode == 0 && c == '0') {
+ report(ZERO_PADDED_FILEMODE, id,
JGitText.get().corruptObjectInvalidModeStartsZero);
+ }
thisMode <<= 3;
thisMode += c - '0';
}
- if (FileMode.fromBits(thisMode).getObjectType() == Constants.OBJ_BAD)
+ if (FileMode.fromBits(thisMode).getObjectType() == OBJ_BAD) {
throw new CorruptObjectException(MessageFormat.format(
JGitText.get().corruptObjectInvalidMode2,
Integer.valueOf(thisMode)));
+ }
final int thisNameB = ptr;
- ptr = scanPathSegment(raw, ptr, sz);
- if (ptr == sz || raw[ptr] != 0)
+ ptr = scanPathSegment(raw, ptr, sz, id);
+ if (ptr == sz || raw[ptr] != 0) {
throw new CorruptObjectException(
JGitText.get().corruptObjectTruncatedInName);
- checkPathSegment2(raw, thisNameB, ptr);
+ }
+ checkPathSegment2(raw, thisNameB, ptr, id);
if (normalized != null) {
- if (!normalized.add(normalize(raw, thisNameB, ptr)))
- throw new CorruptObjectException(
+ if (!normalized.add(normalize(raw, thisNameB, ptr))) {
+ report(DUPLICATE_ENTRIES, id,
JGitText.get().corruptObjectDuplicateEntryNames);
- } else if (duplicateName(raw, thisNameB, ptr))
- throw new CorruptObjectException(
+ }
+ } else if (duplicateName(raw, thisNameB, ptr)) {
+ report(DUPLICATE_ENTRIES, id,
JGitText.get().corruptObjectDuplicateEntryNames);
+ }
if (lastNameB != 0) {
- final int cmp = pathCompare(raw, lastNameB, lastNameE,
- lastMode, thisNameB, ptr, thisMode);
- if (cmp > 0)
- throw new CorruptObjectException(
+ int cmp = compare(
+ raw, lastNameB, lastNameE, lastMode,
+ raw, thisNameB, ptr, thisMode);
+ if (cmp > 0) {
+ report(TREE_NOT_SORTED, id,
JGitText.get().corruptObjectIncorrectSorting);
+ }
}
lastNameB = thisNameB;
lastNameE = ptr;
lastMode = thisMode;
- ptr += 1 + Constants.OBJECT_ID_LENGTH;
- if (ptr > sz)
+ ptr += 1 + OBJECT_ID_LENGTH;
+ if (ptr > sz) {
throw new CorruptObjectException(
JGitText.get().corruptObjectTruncatedInObjectId);
+ }
+ if (ObjectId.zeroId().compareTo(raw, ptr - OBJECT_ID_LENGTH) == 0) {
+ report(NULL_SHA1, id, JGitText.get().corruptObjectZeroId);
+ }
}
}
- private int scanPathSegment(byte[] raw, int ptr, int end)
- throws CorruptObjectException {
+ private int scanPathSegment(byte[] raw, int ptr, int end,
+ @Nullable AnyObjectId id) throws CorruptObjectException {
for (; ptr < end; ptr++) {
byte c = raw[ptr];
- if (c == 0)
+ if (c == 0) {
return ptr;
- if (c == '/')
- throw new CorruptObjectException(
+ }
+ if (c == '/') {
+ report(FULL_PATHNAME, id,
JGitText.get().corruptObjectNameContainsSlash);
+ }
if (windows && isInvalidOnWindows(c)) {
- if (c > 31)
+ if (c > 31) {
throw new CorruptObjectException(String.format(
JGitText.get().corruptObjectNameContainsChar,
Byte.valueOf(c)));
+ }
throw new CorruptObjectException(String.format(
JGitText.get().corruptObjectNameContainsByte,
Integer.valueOf(c & 0xff)));
@@ -469,6 +709,26 @@ public class ObjectChecker {
return ptr;
}
+ @SuppressWarnings("resource")
+ @Nullable
+ private ObjectId idFor(int objType, byte[] raw) {
+ if (skipList != null) {
+ return new ObjectInserter.Formatter().idFor(objType, raw);
+ }
+ return null;
+ }
+
+ private void report(@NonNull ErrorType err, @Nullable AnyObjectId id,
+ String why) throws CorruptObjectException {
+ if (errors.contains(err)
+ && (id == null || skipList == null || !skipList.contains(id))) {
+ if (id != null) {
+ throw new CorruptObjectException(err, id, why);
+ }
+ throw new CorruptObjectException(why);
+ }
+ }
+
/**
* Check tree path entry for validity.
* <p>
@@ -519,73 +779,82 @@ public class ObjectChecker {
*/
public void checkPathSegment(byte[] raw, int ptr, int end)
throws CorruptObjectException {
- int e = scanPathSegment(raw, ptr, end);
+ int e = scanPathSegment(raw, ptr, end, null);
if (e < end && raw[e] == 0)
throw new CorruptObjectException(
JGitText.get().corruptObjectNameContainsNullByte);
- checkPathSegment2(raw, ptr, end);
+ checkPathSegment2(raw, ptr, end, null);
}
- private void checkPathSegment2(byte[] raw, int ptr, int end)
- throws CorruptObjectException {
- if (ptr == end)
- throw new CorruptObjectException(
- JGitText.get().corruptObjectNameZeroLength);
+ private void checkPathSegment2(byte[] raw, int ptr, int end,
+ @Nullable AnyObjectId id) throws CorruptObjectException {
+ if (ptr == end) {
+ report(EMPTY_NAME, id, JGitText.get().corruptObjectNameZeroLength);
+ return;
+ }
+
if (raw[ptr] == '.') {
switch (end - ptr) {
case 1:
- throw new CorruptObjectException(
- JGitText.get().corruptObjectNameDot);
+ report(HAS_DOT, id, JGitText.get().corruptObjectNameDot);
+ break;
case 2:
- if (raw[ptr + 1] == '.')
- throw new CorruptObjectException(
+ if (raw[ptr + 1] == '.') {
+ report(HAS_DOTDOT, id,
JGitText.get().corruptObjectNameDotDot);
+ }
break;
case 4:
- if (isGit(raw, ptr + 1))
- throw new CorruptObjectException(String.format(
+ if (isGit(raw, ptr + 1)) {
+ report(HAS_DOTGIT, id, String.format(
JGitText.get().corruptObjectInvalidName,
RawParseUtils.decode(raw, ptr, end)));
+ }
break;
default:
- if (end - ptr > 4 && isNormalizedGit(raw, ptr + 1, end))
- throw new CorruptObjectException(String.format(
+ if (end - ptr > 4 && isNormalizedGit(raw, ptr + 1, end)) {
+ report(HAS_DOTGIT, id, String.format(
JGitText.get().corruptObjectInvalidName,
RawParseUtils.decode(raw, ptr, end)));
+ }
}
} else if (isGitTilde1(raw, ptr, end)) {
- throw new CorruptObjectException(String.format(
+ report(HAS_DOTGIT, id, String.format(
JGitText.get().corruptObjectInvalidName,
RawParseUtils.decode(raw, ptr, end)));
}
-
- if (macosx && isMacHFSGit(raw, ptr, end))
- throw new CorruptObjectException(String.format(
+ if (macosx && isMacHFSGit(raw, ptr, end, id)) {
+ report(HAS_DOTGIT, id, String.format(
JGitText.get().corruptObjectInvalidNameIgnorableUnicode,
RawParseUtils.decode(raw, ptr, end)));
+ }
if (windows) {
// Windows ignores space and dot at end of file name.
- if (raw[end - 1] == ' ' || raw[end - 1] == '.')
- throw new CorruptObjectException(String.format(
+ if (raw[end - 1] == ' ' || raw[end - 1] == '.') {
+ report(WIN32_BAD_NAME, id, String.format(
JGitText.get().corruptObjectInvalidNameEnd,
Character.valueOf(((char) raw[end - 1]))));
- if (end - ptr >= 3)
- checkNotWindowsDevice(raw, ptr, end);
+ }
+ if (end - ptr >= 3) {
+ checkNotWindowsDevice(raw, ptr, end, id);
+ }
}
}
// Mac's HFS+ folds permutations of ".git" and Unicode ignorable characters
// to ".git" therefore we should prevent such names
- private static boolean isMacHFSGit(byte[] raw, int ptr, int end)
- throws CorruptObjectException {
+ private boolean isMacHFSGit(byte[] raw, int ptr, int end,
+ @Nullable AnyObjectId id) throws CorruptObjectException {
boolean ignorable = false;
byte[] git = new byte[] { '.', 'g', 'i', 't' };
int g = 0;
while (ptr < end) {
switch (raw[ptr]) {
case (byte) 0xe2: // http://www.utf8-chartable.de/unicode-utf8-table.pl?start=8192
- checkTruncatedIgnorableUTF8(raw, ptr, end);
+ if (!checkTruncatedIgnorableUTF8(raw, ptr, end, id)) {
+ return false;
+ }
switch (raw[ptr + 1]) {
case (byte) 0x80:
switch (raw[ptr + 2]) {
@@ -622,7 +891,9 @@ public class ObjectChecker {
return false;
}
case (byte) 0xef: // http://www.utf8-chartable.de/unicode-utf8-table.pl?start=65024
- checkTruncatedIgnorableUTF8(raw, ptr, end);
+ if (!checkTruncatedIgnorableUTF8(raw, ptr, end, id)) {
+ return false;
+ }
// U+FEFF 0xefbbbf ZERO WIDTH NO-BREAK SPACE
if ((raw[ptr + 1] == (byte) 0xbb)
&& (raw[ptr + 2] == (byte) 0xbf)) {
@@ -643,12 +914,15 @@ public class ObjectChecker {
return false;
}
- private static void checkTruncatedIgnorableUTF8(byte[] raw, int ptr, int end)
- throws CorruptObjectException {
- if ((ptr + 2) >= end)
- throw new CorruptObjectException(MessageFormat.format(
+ private boolean checkTruncatedIgnorableUTF8(byte[] raw, int ptr, int end,
+ @Nullable AnyObjectId id) throws CorruptObjectException {
+ if ((ptr + 2) >= end) {
+ report(BAD_UTF8, id, MessageFormat.format(
JGitText.get().corruptObjectInvalidNameInvalidUtf8,
toHexString(raw, ptr, end)));
+ return false;
+ }
+ return true;
}
private static String toHexString(byte[] raw, int ptr, int end) {
@@ -658,33 +932,36 @@ public class ObjectChecker {
return b.toString();
}
- private static void checkNotWindowsDevice(byte[] raw, int ptr, int end)
- throws CorruptObjectException {
+ private void checkNotWindowsDevice(byte[] raw, int ptr, int end,
+ @Nullable AnyObjectId id) throws CorruptObjectException {
switch (toLower(raw[ptr])) {
case 'a': // AUX
if (end - ptr >= 3
&& toLower(raw[ptr + 1]) == 'u'
&& toLower(raw[ptr + 2]) == 'x'
- && (end - ptr == 3 || raw[ptr + 3] == '.'))
- throw new CorruptObjectException(
+ && (end - ptr == 3 || raw[ptr + 3] == '.')) {
+ report(WIN32_BAD_NAME, id,
JGitText.get().corruptObjectInvalidNameAux);
+ }
break;
case 'c': // CON, COM[1-9]
if (end - ptr >= 3
&& toLower(raw[ptr + 2]) == 'n'
&& toLower(raw[ptr + 1]) == 'o'
- && (end - ptr == 3 || raw[ptr + 3] == '.'))
- throw new CorruptObjectException(
+ && (end - ptr == 3 || raw[ptr + 3] == '.')) {
+ report(WIN32_BAD_NAME, id,
JGitText.get().corruptObjectInvalidNameCon);
+ }
if (end - ptr >= 4
&& toLower(raw[ptr + 2]) == 'm'
&& toLower(raw[ptr + 1]) == 'o'
&& isPositiveDigit(raw[ptr + 3])
- && (end - ptr == 4 || raw[ptr + 4] == '.'))
- throw new CorruptObjectException(String.format(
+ && (end - ptr == 4 || raw[ptr + 4] == '.')) {
+ report(WIN32_BAD_NAME, id, String.format(
JGitText.get().corruptObjectInvalidNameCom,
Character.valueOf(((char) raw[ptr + 3]))));
+ }
break;
case 'l': // LPT[1-9]
@@ -692,28 +969,31 @@ public class ObjectChecker {
&& toLower(raw[ptr + 1]) == 'p'
&& toLower(raw[ptr + 2]) == 't'
&& isPositiveDigit(raw[ptr + 3])
- && (end - ptr == 4 || raw[ptr + 4] == '.'))
- throw new CorruptObjectException(String.format(
+ && (end - ptr == 4 || raw[ptr + 4] == '.')) {
+ report(WIN32_BAD_NAME, id, String.format(
JGitText.get().corruptObjectInvalidNameLpt,
Character.valueOf(((char) raw[ptr + 3]))));
+ }
break;
case 'n': // NUL
if (end - ptr >= 3
&& toLower(raw[ptr + 1]) == 'u'
&& toLower(raw[ptr + 2]) == 'l'
- && (end - ptr == 3 || raw[ptr + 3] == '.'))
- throw new CorruptObjectException(
+ && (end - ptr == 3 || raw[ptr + 3] == '.')) {
+ report(WIN32_BAD_NAME, id,
JGitText.get().corruptObjectInvalidNameNul);
+ }
break;
case 'p': // PRN
if (end - ptr >= 3
&& toLower(raw[ptr + 1]) == 'r'
&& toLower(raw[ptr + 2]) == 'n'
- && (end - ptr == 3 || raw[ptr + 3] == '.'))
- throw new CorruptObjectException(
+ && (end - ptr == 3 || raw[ptr + 3] == '.')) {
+ report(WIN32_BAD_NAME, id,
JGitText.get().corruptObjectInvalidNamePrn);
+ }
break;
}
}
@@ -766,6 +1046,15 @@ public class ObjectChecker {
return false;
}
+ private boolean match(byte[] b, byte[] src) {
+ int r = RawParseUtils.match(b, bufPtr.value, src);
+ if (r < 0) {
+ return false;
+ }
+ bufPtr.value = r;
+ return true;
+ }
+
private static char toLower(byte b) {
if ('A' <= b && b <= 'Z')
return (char) (b + ('a' - 'A'));
@@ -790,58 +1079,6 @@ public class ObjectChecker {
private String normalize(byte[] raw, int ptr, int end) {
String n = RawParseUtils.decode(raw, ptr, end).toLowerCase(Locale.US);
- return macosx ? Normalizer.normalize(n) : n;
- }
-
- private static class Normalizer {
- // TODO Simplify invocation to Normalizer after dropping Java 5.
- private static final Method normalize;
- private static final Object nfc;
- static {
- Method method;
- Object formNfc;
- try {
- Class<?> formClazz = Class.forName("java.text.Normalizer$Form"); //$NON-NLS-1$
- formNfc = formClazz.getField("NFC").get(null); //$NON-NLS-1$
- method = Class.forName("java.text.Normalizer") //$NON-NLS-1$
- .getMethod("normalize", CharSequence.class, formClazz); //$NON-NLS-1$
- } catch (ClassNotFoundException e) {
- method = null;
- formNfc = null;
- } catch (NoSuchFieldException e) {
- method = null;
- formNfc = null;
- } catch (NoSuchMethodException e) {
- method = null;
- formNfc = null;
- } catch (SecurityException e) {
- method = null;
- formNfc = null;
- } catch (IllegalArgumentException e) {
- method = null;
- formNfc = null;
- } catch (IllegalAccessException e) {
- method = null;
- formNfc = null;
- }
- normalize = method;
- nfc = formNfc;
- }
-
- static String normalize(String in) {
- if (normalize == null)
- return in;
- try {
- return (String) normalize.invoke(null, in, nfc);
- } catch (IllegalAccessException e) {
- return in;
- } catch (InvocationTargetException e) {
- if (e.getCause() instanceof RuntimeException)
- throw (RuntimeException) e.getCause();
- if (e.getCause() instanceof Error)
- throw (Error) e.getCause();
- return in;
- }
- }
+ return macosx ? Normalizer.normalize(n, Normalizer.Form.NFC) : n;
}
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdOwnerMap.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdOwnerMap.java
index 95b16d9176..442261cbd5 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdOwnerMap.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdOwnerMap.java
@@ -67,8 +67,8 @@ import java.util.NoSuchElementException;
* @param <V>
* type of subclass of ObjectId that will be stored in the map.
*/
-public class ObjectIdOwnerMap<V extends ObjectIdOwnerMap.Entry> implements
- Iterable<V> {
+public class ObjectIdOwnerMap<V extends ObjectIdOwnerMap.Entry>
+ implements Iterable<V>, ObjectIdSet {
/** Size of the initial directory, will grow as necessary. */
private static final int INITIAL_DIRECTORY = 1024;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdRef.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdRef.java
index f481c772dc..c286f5e463 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdRef.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdRef.java
@@ -44,6 +44,9 @@
package org.eclipse.jgit.lib;
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.annotations.Nullable;
+
/** A {@link Ref} that points directly at an {@link ObjectId}. */
public abstract class ObjectIdRef implements Ref {
/** Any reference whose peeled value is not yet known. */
@@ -56,13 +59,15 @@ public abstract class ObjectIdRef implements Ref {
* @param name
* name of this ref.
* @param id
- * current value of the ref. May be null to indicate a ref
- * that does not exist yet.
+ * current value of the ref. May be {@code null} to indicate
+ * a ref that does not exist yet.
*/
- public Unpeeled(Storage st, String name, ObjectId id) {
+ public Unpeeled(@NonNull Storage st, @NonNull String name,
+ @Nullable ObjectId id) {
super(st, name, id);
}
+ @Nullable
public ObjectId getPeeledObjectId() {
return null;
}
@@ -88,11 +93,13 @@ public abstract class ObjectIdRef implements Ref {
* @param p
* the first non-tag object that tag {@code id} points to.
*/
- public PeeledTag(Storage st, String name, ObjectId id, ObjectId p) {
+ public PeeledTag(@NonNull Storage st, @NonNull String name,
+ @Nullable ObjectId id, @NonNull ObjectId p) {
super(st, name, id);
peeledObjectId = p;
}
+ @NonNull
public ObjectId getPeeledObjectId() {
return peeledObjectId;
}
@@ -112,13 +119,15 @@ public abstract class ObjectIdRef implements Ref {
* @param name
* name of this ref.
* @param id
- * current value of the ref. May be null to indicate a ref
- * that does not exist yet.
+ * current value of the ref. May be {@code null} to indicate
+ * a ref that does not exist yet.
*/
- public PeeledNonTag(Storage st, String name, ObjectId id) {
+ public PeeledNonTag(@NonNull Storage st, @NonNull String name,
+ @Nullable ObjectId id) {
super(st, name, id);
}
+ @Nullable
public ObjectId getPeeledObjectId() {
return null;
}
@@ -142,15 +151,17 @@ public abstract class ObjectIdRef implements Ref {
* @param name
* name of this ref.
* @param id
- * current value of the ref. May be null to indicate a ref that
- * does not exist yet.
+ * current value of the ref. May be {@code null} to indicate a
+ * ref that does not exist yet.
*/
- protected ObjectIdRef(Storage st, String name, ObjectId id) {
+ protected ObjectIdRef(@NonNull Storage st, @NonNull String name,
+ @Nullable ObjectId id) {
this.name = name;
this.storage = st;
this.objectId = id;
}
+ @NonNull
public String getName() {
return name;
}
@@ -159,22 +170,27 @@ public abstract class ObjectIdRef implements Ref {
return false;
}
+ @NonNull
public Ref getLeaf() {
return this;
}
+ @NonNull
public Ref getTarget() {
return this;
}
+ @Nullable
public ObjectId getObjectId() {
return objectId;
}
+ @NonNull
public Storage getStorage() {
return storage;
}
+ @NonNull
@Override
public String toString() {
StringBuilder r = new StringBuilder();
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/SymlinkTreeEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdSet.java
index c7e41bce04..0b5848463c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/SymlinkTreeEntry.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdSet.java
@@ -1,6 +1,5 @@
/*
- * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org>
+ * Copyright (C) 2015, Google Inc.
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
@@ -45,41 +44,21 @@
package org.eclipse.jgit.lib;
/**
- * A tree entry representing a symbolic link.
+ * Simple set of ObjectIds.
+ * <p>
+ * Usually backed by a read-only data structure such as
+ * {@link org.eclipse.jgit.internal.storage.file.PackIndex}. Mutable types like
+ * {@link ObjectIdOwnerMap} also implement the interface by checking keys.
*
- * Note. Java cannot really handle these as file system objects.
- *
- * @deprecated To look up information about a single path, use
- * {@link org.eclipse.jgit.treewalk.TreeWalk#forPath(Repository, String, org.eclipse.jgit.revwalk.RevTree)}.
- * To lookup information about multiple paths at once, use a
- * {@link org.eclipse.jgit.treewalk.TreeWalk} and obtain the current entry's
- * information from its getter methods.
+ * @since 4.2
*/
-@Deprecated
-public class SymlinkTreeEntry extends TreeEntry {
-
+public interface ObjectIdSet {
/**
- * Construct a {@link SymlinkTreeEntry} with the specified name and SHA-1 in
- * the specified parent
+ * Returns true if the objectId is contained within the collection.
*
- * @param parent
- * @param id
- * @param nameUTF8
+ * @param objectId
+ * the objectId to find
+ * @return whether the collection contains the objectId.
*/
- public SymlinkTreeEntry(final Tree parent, final ObjectId id,
- final byte[] nameUTF8) {
- super(parent, id, nameUTF8);
- }
-
- public FileMode getMode() {
- return FileMode.SYMLINK;
- }
-
- public String toString() {
- final StringBuilder r = new StringBuilder();
- r.append(ObjectId.toString(getId()));
- r.append(" S "); //$NON-NLS-1$
- r.append(getFullName());
- return r.toString();
- }
+ boolean contains(AnyObjectId objectId);
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdSubclassMap.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdSubclassMap.java
index 48aa109e7c..faed64bfe7 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdSubclassMap.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdSubclassMap.java
@@ -60,7 +60,8 @@ import java.util.NoSuchElementException;
* @param <V>
* type of subclass of ObjectId that will be stored in the map.
*/
-public class ObjectIdSubclassMap<V extends ObjectId> implements Iterable<V> {
+public class ObjectIdSubclassMap<V extends ObjectId>
+ implements Iterable<V>, ObjectIdSet {
private static final int INITIAL_TABLE_SIZE = 2048;
int size;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Ref.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Ref.java
index f119c44fe2..a78a90fe58 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Ref.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Ref.java
@@ -43,6 +43,9 @@
package org.eclipse.jgit.lib;
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.annotations.Nullable;
+
/**
* Pairing of a name and the {@link ObjectId} it currently has.
* <p>
@@ -126,6 +129,7 @@ public interface Ref {
*
* @return name of this ref.
*/
+ @NonNull
public String getName();
/**
@@ -156,6 +160,7 @@ public interface Ref {
*
* @return the reference that actually stores the ObjectId value.
*/
+ @NonNull
public abstract Ref getLeaf();
/**
@@ -170,22 +175,27 @@ public interface Ref {
*
* @return the target reference, or {@code this}.
*/
+ @NonNull
public abstract Ref getTarget();
/**
* Cached value of this ref.
*
- * @return the value of this ref at the last time we read it.
+ * @return the value of this ref at the last time we read it. May be
+ * {@code null} to indicate a ref that does not exist yet or a
+ * symbolic ref pointing to an unborn branch.
*/
+ @Nullable
public abstract ObjectId getObjectId();
/**
* Cached value of <code>ref^{}</code> (the ref peeled to commit).
*
* @return if this ref is an annotated tag the id of the commit (or tree or
- * blob) that the annotated tag refers to; null if this ref does not
- * refer to an annotated tag.
+ * blob) that the annotated tag refers to; {@code null} if this ref
+ * does not refer to an annotated tag.
*/
+ @Nullable
public abstract ObjectId getPeeledObjectId();
/**
@@ -201,5 +211,6 @@ public interface Ref {
*
* @return type of ref.
*/
+ @NonNull
public abstract Storage getStorage();
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java
index 986666f2f4..c0c3862c8b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java
@@ -82,8 +82,10 @@ public abstract class RefDatabase {
* <p>
* If the reference is nested deeper than this depth, the implementation
* should either fail, or at least claim the reference does not exist.
+ *
+ * @since 4.2
*/
- protected static final int MAX_SYMBOLIC_REF_DEPTH = 5;
+ public static final int MAX_SYMBOLIC_REF_DEPTH = 5;
/** Magic value for {@link #getRefs(String)} to return all references. */
public static final String ALL = "";//$NON-NLS-1$
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefWriter.java
index 747fa62b50..3a02b22813 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefWriter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefWriter.java
@@ -119,13 +119,20 @@ public abstract class RefWriter {
continue;
}
- r.getObjectId().copyTo(tmp, w);
+ ObjectId objectId = r.getObjectId();
+ if (objectId == null) {
+ // Symrefs to unborn branches aren't advertised in the info/refs
+ // file.
+ continue;
+ }
+ objectId.copyTo(tmp, w);
w.write('\t');
w.write(r.getName());
w.write('\n');
- if (r.getPeeledObjectId() != null) {
- r.getPeeledObjectId().copyTo(tmp, w);
+ ObjectId peeledObjectId = r.getPeeledObjectId();
+ if (peeledObjectId != null) {
+ peeledObjectId.copyTo(tmp, w);
w.write('\t');
w.write(r.getName());
w.write("^{}\n"); //$NON-NLS-1$
@@ -167,14 +174,21 @@ public abstract class RefWriter {
if (r.getStorage() != Ref.Storage.PACKED)
continue;
- r.getObjectId().copyTo(tmp, w);
+ ObjectId objectId = r.getObjectId();
+ if (objectId == null) {
+ // A packed ref cannot be a symref, let alone a symref
+ // to an unborn branch.
+ throw new NullPointerException();
+ }
+ objectId.copyTo(tmp, w);
w.write(' ');
w.write(r.getName());
w.write('\n');
- if (r.getPeeledObjectId() != null) {
+ ObjectId peeledObjectId = r.getPeeledObjectId();
+ if (peeledObjectId != null) {
w.write('^');
- r.getPeeledObjectId().copyTo(tmp, w);
+ peeledObjectId.copyTo(tmp, w);
w.write('\n');
}
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java
index 49a970d03a..f8266133a6 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java
@@ -911,12 +911,16 @@ public abstract class Repository implements AutoCloseable {
@Nullable
public String getFullBranch() throws IOException {
Ref head = getRef(Constants.HEAD);
- if (head == null)
+ if (head == null) {
return null;
- if (head.isSymbolic())
+ }
+ if (head.isSymbolic()) {
return head.getTarget().getName();
- if (head.getObjectId() != null)
- return head.getObjectId().name();
+ }
+ ObjectId objectId = head.getObjectId();
+ if (objectId != null) {
+ return objectId.name();
+ }
return null;
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/SymbolicRef.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/SymbolicRef.java
index 43b1510f94..eeab921a7a 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/SymbolicRef.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/SymbolicRef.java
@@ -43,6 +43,9 @@
package org.eclipse.jgit.lib;
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.annotations.Nullable;
+
/**
* A reference that indirectly points at another {@link Ref}.
* <p>
@@ -62,11 +65,12 @@ public class SymbolicRef implements Ref {
* @param target
* the ref we reference and derive our value from.
*/
- public SymbolicRef(String refName, Ref target) {
+ public SymbolicRef(@NonNull String refName, @NonNull Ref target) {
this.name = refName;
this.target = target;
}
+ @NonNull
public String getName() {
return name;
}
@@ -75,6 +79,7 @@ public class SymbolicRef implements Ref {
return true;
}
+ @NonNull
public Ref getLeaf() {
Ref dst = getTarget();
while (dst.isSymbolic())
@@ -82,18 +87,22 @@ public class SymbolicRef implements Ref {
return dst;
}
+ @NonNull
public Ref getTarget() {
return target;
}
+ @Nullable
public ObjectId getObjectId() {
return getLeaf().getObjectId();
}
+ @NonNull
public Storage getStorage() {
return Storage.LOOSE;
}
+ @Nullable
public ObjectId getPeeledObjectId() {
return getLeaf().getPeeledObjectId();
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Tree.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Tree.java
deleted file mode 100644
index 43bd489dc0..0000000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Tree.java
+++ /dev/null
@@ -1,601 +0,0 @@
-/*
- * Copyright (C) 2007, Robin Rosenberg <me@lathund.dewire.com>
- * Copyright (C) 2007-2008, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org>
- * and other copyright owners as documented in the project's IP log.
- *
- * This program and the accompanying materials are made available
- * under the terms of the Eclipse Distribution License v1.0 which
- * accompanies this distribution, is reproduced below, and is
- * available at http://www.eclipse.org/org/documents/edl-v10.php
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or
- * without modification, are permitted provided that the following
- * conditions are met:
- *
- * - Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * - Redistributions in binary form must reproduce the above
- * copyright notice, this list of conditions and the following
- * disclaimer in the documentation and/or other materials provided
- * with the distribution.
- *
- * - Neither the name of the Eclipse Foundation, Inc. nor the
- * names of its contributors may be used to endorse or promote
- * products derived from this software without specific prior
- * written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
- * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
- * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
- * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
- * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
- * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
- * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
- * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package org.eclipse.jgit.lib;
-
-import java.io.IOException;
-import java.text.MessageFormat;
-
-import org.eclipse.jgit.errors.CorruptObjectException;
-import org.eclipse.jgit.errors.EntryExistsException;
-import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.errors.ObjectWritingException;
-import org.eclipse.jgit.internal.JGitText;
-import org.eclipse.jgit.util.RawParseUtils;
-
-/**
- * A representation of a Git tree entry. A Tree is a directory in Git.
- *
- * @deprecated To look up information about a single path, use
- * {@link org.eclipse.jgit.treewalk.TreeWalk#forPath(Repository, String, org.eclipse.jgit.revwalk.RevTree)}.
- * To lookup information about multiple paths at once, use a
- * {@link org.eclipse.jgit.treewalk.TreeWalk} and obtain the current entry's
- * information from its getter methods.
- */
-@Deprecated
-public class Tree extends TreeEntry {
- private static final TreeEntry[] EMPTY_TREE = {};
-
- /**
- * Compare two names represented as bytes. Since git treats names of trees and
- * blobs differently we have one parameter that represents a '/' for trees. For
- * other objects the value should be NUL. The names are compare by their positive
- * byte value (0..255).
- *
- * A blob and a tree with the same name will not compare equal.
- *
- * @param a name
- * @param b name
- * @param lasta '/' if a is a tree, else NUL
- * @param lastb '/' if b is a tree, else NUL
- *
- * @return &lt; 0 if a is sorted before b, 0 if they are the same, else b
- */
- public static final int compareNames(final byte[] a, final byte[] b, final int lasta,final int lastb) {
- return compareNames(a, b, 0, b.length, lasta, lastb);
- }
-
- private static final int compareNames(final byte[] a, final byte[] nameUTF8,
- final int nameStart, final int nameEnd, final int lasta, int lastb) {
- int j,k;
- for (j = 0, k = nameStart; j < a.length && k < nameEnd; j++, k++) {
- final int aj = a[j] & 0xff;
- final int bk = nameUTF8[k] & 0xff;
- if (aj < bk)
- return -1;
- else if (aj > bk)
- return 1;
- }
- if (j < a.length) {
- int aj = a[j]&0xff;
- if (aj < lastb)
- return -1;
- else if (aj > lastb)
- return 1;
- else
- if (j == a.length - 1)
- return 0;
- else
- return -1;
- }
- if (k < nameEnd) {
- int bk = nameUTF8[k] & 0xff;
- if (lasta < bk)
- return -1;
- else if (lasta > bk)
- return 1;
- else
- if (k == nameEnd - 1)
- return 0;
- else
- return 1;
- }
- if (lasta < lastb)
- return -1;
- else if (lasta > lastb)
- return 1;
-
- final int namelength = nameEnd - nameStart;
- if (a.length == namelength)
- return 0;
- else if (a.length < namelength)
- return -1;
- else
- return 1;
- }
-
- private static final byte[] substring(final byte[] s, final int nameStart,
- final int nameEnd) {
- if (nameStart == 0 && nameStart == s.length)
- return s;
- final byte[] n = new byte[nameEnd - nameStart];
- System.arraycopy(s, nameStart, n, 0, n.length);
- return n;
- }
-
- private static final int binarySearch(final TreeEntry[] entries,
- final byte[] nameUTF8, final int nameUTF8last, final int nameStart, final int nameEnd) {
- if (entries.length == 0)
- return -1;
- int high = entries.length;
- int low = 0;
- do {
- final int mid = (low + high) >>> 1;
- final int cmp = compareNames(entries[mid].getNameUTF8(), nameUTF8,
- nameStart, nameEnd, TreeEntry.lastChar(entries[mid]), nameUTF8last);
- if (cmp < 0)
- low = mid + 1;
- else if (cmp == 0)
- return mid;
- else
- high = mid;
- } while (low < high);
- return -(low + 1);
- }
-
- private final Repository db;
-
- private TreeEntry[] contents;
-
- /**
- * Constructor for a new Tree
- *
- * @param repo The repository that owns the Tree.
- */
- public Tree(final Repository repo) {
- super(null, null, null);
- db = repo;
- contents = EMPTY_TREE;
- }
-
- /**
- * Construct a Tree object with known content and hash value
- *
- * @param repo
- * @param myId
- * @param raw
- * @throws IOException
- */
- public Tree(final Repository repo, final ObjectId myId, final byte[] raw)
- throws IOException {
- super(null, myId, null);
- db = repo;
- readTree(raw);
- }
-
- /**
- * Construct a new Tree under another Tree
- *
- * @param parent
- * @param nameUTF8
- */
- public Tree(final Tree parent, final byte[] nameUTF8) {
- super(parent, null, nameUTF8);
- db = parent.getRepository();
- contents = EMPTY_TREE;
- }
-
- /**
- * Construct a Tree with a known SHA-1 under another tree. Data is not yet
- * specified and will have to be loaded on demand.
- *
- * @param parent
- * @param id
- * @param nameUTF8
- */
- public Tree(final Tree parent, final ObjectId id, final byte[] nameUTF8) {
- super(parent, id, nameUTF8);
- db = parent.getRepository();
- }
-
- public FileMode getMode() {
- return FileMode.TREE;
- }
-
- /**
- * @return true if this Tree is the top level Tree.
- */
- public boolean isRoot() {
- return getParent() == null;
- }
-
- public Repository getRepository() {
- return db;
- }
-
- /**
- * @return true of the data of this Tree is loaded
- */
- public boolean isLoaded() {
- return contents != null;
- }
-
- /**
- * Forget the in-memory data for this tree.
- */
- public void unload() {
- if (isModified())
- throw new IllegalStateException(JGitText.get().cannotUnloadAModifiedTree);
- contents = null;
- }
-
- /**
- * Adds a new or existing file with the specified name to this tree.
- * Trees are added if necessary as the name may contain '/':s.
- *
- * @param name Name
- * @return a {@link FileTreeEntry} for the added file.
- * @throws IOException
- */
- public FileTreeEntry addFile(final String name) throws IOException {
- return addFile(Repository.gitInternalSlash(Constants.encode(name)), 0);
- }
-
- /**
- * Adds a new or existing file with the specified name to this tree.
- * Trees are added if necessary as the name may contain '/':s.
- *
- * @param s an array containing the name
- * @param offset when the name starts in the tree.
- *
- * @return a {@link FileTreeEntry} for the added file.
- * @throws IOException
- */
- public FileTreeEntry addFile(final byte[] s, final int offset)
- throws IOException {
- int slash;
- int p;
-
- for (slash = offset; slash < s.length && s[slash] != '/'; slash++) {
- // search for path component terminator
- }
-
- ensureLoaded();
- byte xlast = slash<s.length ? (byte)'/' : 0;
- p = binarySearch(contents, s, xlast, offset, slash);
- if (p >= 0 && slash < s.length && contents[p] instanceof Tree)
- return ((Tree) contents[p]).addFile(s, slash + 1);
-
- final byte[] newName = substring(s, offset, slash);
- if (p >= 0)
- throw new EntryExistsException(RawParseUtils.decode(newName));
- else if (slash < s.length) {
- final Tree t = new Tree(this, newName);
- insertEntry(p, t);
- return t.addFile(s, slash + 1);
- } else {
- final FileTreeEntry f = new FileTreeEntry(this, null, newName,
- false);
- insertEntry(p, f);
- return f;
- }
- }
-
- /**
- * Adds a new or existing Tree with the specified name to this tree.
- * Trees are added if necessary as the name may contain '/':s.
- *
- * @param name Name
- * @return a {@link FileTreeEntry} for the added tree.
- * @throws IOException
- */
- public Tree addTree(final String name) throws IOException {
- return addTree(Repository.gitInternalSlash(Constants.encode(name)), 0);
- }
-
- /**
- * Adds a new or existing Tree with the specified name to this tree.
- * Trees are added if necessary as the name may contain '/':s.
- *
- * @param s an array containing the name
- * @param offset when the name starts in the tree.
- *
- * @return a {@link FileTreeEntry} for the added tree.
- * @throws IOException
- */
- public Tree addTree(final byte[] s, final int offset) throws IOException {
- int slash;
- int p;
-
- for (slash = offset; slash < s.length && s[slash] != '/'; slash++) {
- // search for path component terminator
- }
-
- ensureLoaded();
- p = binarySearch(contents, s, (byte)'/', offset, slash);
- if (p >= 0 && slash < s.length && contents[p] instanceof Tree)
- return ((Tree) contents[p]).addTree(s, slash + 1);
-
- final byte[] newName = substring(s, offset, slash);
- if (p >= 0)
- throw new EntryExistsException(RawParseUtils.decode(newName));
-
- final Tree t = new Tree(this, newName);
- insertEntry(p, t);
- return slash == s.length ? t : t.addTree(s, slash + 1);
- }
-
- /**
- * Add the specified tree entry to this tree.
- *
- * @param e
- * @throws IOException
- */
- public void addEntry(final TreeEntry e) throws IOException {
- final int p;
-
- ensureLoaded();
- p = binarySearch(contents, e.getNameUTF8(), TreeEntry.lastChar(e), 0, e.getNameUTF8().length);
- if (p < 0) {
- e.attachParent(this);
- insertEntry(p, e);
- } else {
- throw new EntryExistsException(e.getName());
- }
- }
-
- private void insertEntry(int p, final TreeEntry e) {
- final TreeEntry[] c = contents;
- final TreeEntry[] n = new TreeEntry[c.length + 1];
- p = -(p + 1);
- for (int k = c.length - 1; k >= p; k--)
- n[k + 1] = c[k];
- n[p] = e;
- for (int k = p - 1; k >= 0; k--)
- n[k] = c[k];
- contents = n;
- setModified();
- }
-
- void removeEntry(final TreeEntry e) {
- final TreeEntry[] c = contents;
- final int p = binarySearch(c, e.getNameUTF8(), TreeEntry.lastChar(e), 0,
- e.getNameUTF8().length);
- if (p >= 0) {
- final TreeEntry[] n = new TreeEntry[c.length - 1];
- for (int k = c.length - 1; k > p; k--)
- n[k - 1] = c[k];
- for (int k = p - 1; k >= 0; k--)
- n[k] = c[k];
- contents = n;
- setModified();
- }
- }
-
- /**
- * @return number of members in this tree
- * @throws IOException
- */
- public int memberCount() throws IOException {
- ensureLoaded();
- return contents.length;
- }
-
- /**
- * Return all members of the tree sorted in Git order.
- *
- * Entries are sorted by the numerical unsigned byte
- * values with (sub)trees having an implicit '/'. An
- * example of a tree with three entries. a:b is an
- * actual file name here.
- *
- * <p>
- * 100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a.b
- * 040000 tree 4277b6e69d25e5efa77c455340557b384a4c018a a
- * 100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 a:b
- *
- * @return all entries in this Tree, sorted.
- * @throws IOException
- */
- public TreeEntry[] members() throws IOException {
- ensureLoaded();
- final TreeEntry[] c = contents;
- if (c.length != 0) {
- final TreeEntry[] r = new TreeEntry[c.length];
- for (int k = c.length - 1; k >= 0; k--)
- r[k] = c[k];
- return r;
- } else
- return c;
- }
-
- private boolean exists(final String s, byte slast) throws IOException {
- return findMember(s, slast) != null;
- }
-
- /**
- * @param path to the tree.
- * @return true if a tree with the specified path can be found under this
- * tree.
- * @throws IOException
- */
- public boolean existsTree(String path) throws IOException {
- return exists(path,(byte)'/');
- }
-
- /**
- * @param path of the non-tree entry.
- * @return true if a blob, symlink, or gitlink with the specified name
- * can be found under this tree.
- * @throws IOException
- */
- public boolean existsBlob(String path) throws IOException {
- return exists(path,(byte)0);
- }
-
- private TreeEntry findMember(final String s, byte slast) throws IOException {
- return findMember(Repository.gitInternalSlash(Constants.encode(s)), slast, 0);
- }
-
- private TreeEntry findMember(final byte[] s, final byte slast, final int offset)
- throws IOException {
- int slash;
- int p;
-
- for (slash = offset; slash < s.length && s[slash] != '/'; slash++) {
- // search for path component terminator
- }
-
- ensureLoaded();
- byte xlast = slash<s.length ? (byte)'/' : slast;
- p = binarySearch(contents, s, xlast, offset, slash);
- if (p >= 0) {
- final TreeEntry r = contents[p];
- if (slash < s.length-1)
- return r instanceof Tree ? ((Tree) r).findMember(s, slast, slash + 1)
- : null;
- return r;
- }
- return null;
- }
-
- /**
- * @param s
- * blob name
- * @return a {@link TreeEntry} representing an object with the specified
- * relative path.
- * @throws IOException
- */
- public TreeEntry findBlobMember(String s) throws IOException {
- return findMember(s,(byte)0);
- }
-
- /**
- * @param s Tree Name
- * @return a Tree with the name s or null
- * @throws IOException
- */
- public TreeEntry findTreeMember(String s) throws IOException {
- return findMember(s,(byte)'/');
- }
-
- private void ensureLoaded() throws IOException, MissingObjectException {
- if (!isLoaded()) {
- ObjectLoader ldr = db.open(getId(), Constants.OBJ_TREE);
- readTree(ldr.getCachedBytes());
- }
- }
-
- private void readTree(final byte[] raw) throws IOException {
- final int rawSize = raw.length;
- int rawPtr = 0;
- TreeEntry[] temp;
- int nextIndex = 0;
-
- while (rawPtr < rawSize) {
- while (rawPtr < rawSize && raw[rawPtr] != 0)
- rawPtr++;
- rawPtr++;
- rawPtr += Constants.OBJECT_ID_LENGTH;
- nextIndex++;
- }
-
- temp = new TreeEntry[nextIndex];
- rawPtr = 0;
- nextIndex = 0;
- while (rawPtr < rawSize) {
- int c = raw[rawPtr++];
- if (c < '0' || c > '7')
- throw new CorruptObjectException(getId(), JGitText.get().corruptObjectInvalidEntryMode);
- int mode = c - '0';
- for (;;) {
- c = raw[rawPtr++];
- if (' ' == c)
- break;
- else if (c < '0' || c > '7')
- throw new CorruptObjectException(getId(), JGitText.get().corruptObjectInvalidMode);
- mode <<= 3;
- mode += c - '0';
- }
-
- int nameLen = 0;
- while (raw[rawPtr + nameLen] != 0)
- nameLen++;
- final byte[] name = new byte[nameLen];
- System.arraycopy(raw, rawPtr, name, 0, nameLen);
- rawPtr += nameLen + 1;
-
- final ObjectId id = ObjectId.fromRaw(raw, rawPtr);
- rawPtr += Constants.OBJECT_ID_LENGTH;
-
- final TreeEntry ent;
- if (FileMode.REGULAR_FILE.equals(mode))
- ent = new FileTreeEntry(this, id, name, false);
- else if (FileMode.EXECUTABLE_FILE.equals(mode))
- ent = new FileTreeEntry(this, id, name, true);
- else if (FileMode.TREE.equals(mode))
- ent = new Tree(this, id, name);
- else if (FileMode.SYMLINK.equals(mode))
- ent = new SymlinkTreeEntry(this, id, name);
- else if (FileMode.GITLINK.equals(mode))
- ent = new GitlinkTreeEntry(this, id, name);
- else
- throw new CorruptObjectException(getId(), MessageFormat.format(
- JGitText.get().corruptObjectInvalidMode2, Integer.toOctalString(mode)));
- temp[nextIndex++] = ent;
- }
-
- contents = temp;
- }
-
- /**
- * Format this Tree in canonical format.
- *
- * @return canonical encoding of the tree object.
- * @throws IOException
- * the tree cannot be loaded, or its not in a writable state.
- */
- public byte[] format() throws IOException {
- TreeFormatter fmt = new TreeFormatter();
- for (TreeEntry e : members()) {
- ObjectId id = e.getId();
- if (id == null)
- throw new ObjectWritingException(MessageFormat.format(JGitText
- .get().objectAtPathDoesNotHaveId, e.getFullName()));
-
- fmt.append(e.getNameUTF8(), e.getMode(), id);
- }
- return fmt.toByteArray();
- }
-
- public String toString() {
- final StringBuilder r = new StringBuilder();
- r.append(ObjectId.toString(getId()));
- r.append(" T "); //$NON-NLS-1$
- r.append(getFullName());
- return r.toString();
- }
-
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TreeEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TreeEntry.java
deleted file mode 100644
index a1ffa68056..0000000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TreeEntry.java
+++ /dev/null
@@ -1,256 +0,0 @@
-/*
- * Copyright (C) 2007-2008, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org>
- * and other copyright owners as documented in the project's IP log.
- *
- * This program and the accompanying materials are made available
- * under the terms of the Eclipse Distribution License v1.0 which
- * accompanies this distribution, is reproduced below, and is
- * available at http://www.eclipse.org/org/documents/edl-v10.php
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or
- * without modification, are permitted provided that the following
- * conditions are met:
- *
- * - Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * - Redistributions in binary form must reproduce the above
- * copyright notice, this list of conditions and the following
- * disclaimer in the documentation and/or other materials provided
- * with the distribution.
- *
- * - Neither the name of the Eclipse Foundation, Inc. nor the
- * names of its contributors may be used to endorse or promote
- * products derived from this software without specific prior
- * written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
- * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
- * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
- * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
- * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
- * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
- * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
- * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package org.eclipse.jgit.lib;
-
-import java.io.IOException;
-
-import org.eclipse.jgit.util.RawParseUtils;
-
-/**
- * This class represents an entry in a tree, like a blob or another tree.
- *
- * @deprecated To look up information about a single path, use
- * {@link org.eclipse.jgit.treewalk.TreeWalk#forPath(Repository, String, org.eclipse.jgit.revwalk.RevTree)}.
- * To lookup information about multiple paths at once, use a
- * {@link org.eclipse.jgit.treewalk.TreeWalk} and obtain the current entry's
- * information from its getter methods.
- */
-@Deprecated
-public abstract class TreeEntry implements Comparable {
- private byte[] nameUTF8;
-
- private Tree parent;
-
- private ObjectId id;
-
- /**
- * Construct a named tree entry.
- *
- * @param myParent
- * @param myId
- * @param myNameUTF8
- */
- protected TreeEntry(final Tree myParent, final ObjectId myId,
- final byte[] myNameUTF8) {
- nameUTF8 = myNameUTF8;
- parent = myParent;
- id = myId;
- }
-
- /**
- * @return parent of this tree.
- */
- public Tree getParent() {
- return parent;
- }
-
- /**
- * Delete this entry.
- */
- public void delete() {
- getParent().removeEntry(this);
- detachParent();
- }
-
- /**
- * Detach this entry from it's parent.
- */
- public void detachParent() {
- parent = null;
- }
-
- void attachParent(final Tree p) {
- parent = p;
- }
-
- /**
- * @return the repository owning this entry.
- */
- public Repository getRepository() {
- return getParent().getRepository();
- }
-
- /**
- * @return the raw byte name of this entry.
- */
- public byte[] getNameUTF8() {
- return nameUTF8;
- }
-
- /**
- * @return the name of this entry.
- */
- public String getName() {
- if (nameUTF8 != null)
- return RawParseUtils.decode(nameUTF8);
- return null;
- }
-
- /**
- * Rename this entry.
- *
- * @param n The new name
- * @throws IOException
- */
- public void rename(final String n) throws IOException {
- rename(Constants.encode(n));
- }
-
- /**
- * Rename this entry.
- *
- * @param n The new name
- * @throws IOException
- */
- public void rename(final byte[] n) throws IOException {
- final Tree t = getParent();
- if (t != null) {
- delete();
- }
- nameUTF8 = n;
- if (t != null) {
- t.addEntry(this);
- }
- }
-
- /**
- * @return true if this entry is new or modified since being loaded.
- */
- public boolean isModified() {
- return getId() == null;
- }
-
- /**
- * Mark this entry as modified.
- */
- public void setModified() {
- setId(null);
- }
-
- /**
- * @return SHA-1 of this tree entry (null for new unhashed entries)
- */
- public ObjectId getId() {
- return id;
- }
-
- /**
- * Set (update) the SHA-1 of this entry. Invalidates the id's of all
- * entries above this entry as they will have to be recomputed.
- *
- * @param n SHA-1 for this entry.
- */
- public void setId(final ObjectId n) {
- // If we have a parent and our id is being cleared or changed then force
- // the parent's id to become unset as it depends on our id.
- //
- final Tree p = getParent();
- if (p != null && id != n) {
- if ((id == null && n != null) || (id != null && n == null)
- || !id.equals(n)) {
- p.setId(null);
- }
- }
-
- id = n;
- }
-
- /**
- * @return repository relative name of this entry
- */
- public String getFullName() {
- final StringBuilder r = new StringBuilder();
- appendFullName(r);
- return r.toString();
- }
-
- /**
- * @return repository relative name of the entry
- * FIXME better encoding
- */
- public byte[] getFullNameUTF8() {
- return getFullName().getBytes();
- }
-
- public int compareTo(final Object o) {
- if (this == o)
- return 0;
- if (o instanceof TreeEntry)
- return Tree.compareNames(nameUTF8, ((TreeEntry) o).nameUTF8, lastChar(this), lastChar((TreeEntry)o));
- return -1;
- }
-
- /**
- * Helper for accessing tree/blob methods.
- *
- * @param treeEntry
- * @return '/' for Tree entries and NUL for non-treeish objects.
- */
- final public static int lastChar(TreeEntry treeEntry) {
- if (!(treeEntry instanceof Tree))
- return '\0';
- else
- return '/';
- }
-
- /**
- * @return mode (type of object)
- */
- public abstract FileMode getMode();
-
- private void appendFullName(final StringBuilder r) {
- final TreeEntry p = getParent();
- final String n = getName();
- if (p != null) {
- p.appendFullName(r);
- if (r.length() > 0) {
- r.append('/');
- }
- }
- if (n != null) {
- r.append(n);
- }
- }
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java
index 191f3d8366..82cbf368c7 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java
@@ -46,6 +46,7 @@ import java.util.ArrayList;
import java.util.List;
import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.util.ChangeIdUtil;
@@ -76,22 +77,22 @@ public class MergeMessageFormatter {
List<String> commits = new ArrayList<String>();
List<String> others = new ArrayList<String>();
for (Ref ref : refsToMerge) {
- if (ref.getName().startsWith(Constants.R_HEADS))
+ if (ref.getName().startsWith(Constants.R_HEADS)) {
branches.add("'" + Repository.shortenRefName(ref.getName()) //$NON-NLS-1$
+ "'"); //$NON-NLS-1$
-
- else if (ref.getName().startsWith(Constants.R_REMOTES))
+ } else if (ref.getName().startsWith(Constants.R_REMOTES)) {
remoteBranches.add("'" //$NON-NLS-1$
+ Repository.shortenRefName(ref.getName()) + "'"); //$NON-NLS-1$
-
- else if (ref.getName().startsWith(Constants.R_TAGS))
+ } else if (ref.getName().startsWith(Constants.R_TAGS)) {
tags.add("'" + Repository.shortenRefName(ref.getName()) + "'"); //$NON-NLS-1$ //$NON-NLS-2$
-
- else if (ref.getName().equals(ref.getObjectId().getName()))
- commits.add("'" + ref.getName() + "'"); //$NON-NLS-1$ //$NON-NLS-2$
-
- else
- others.add(ref.getName());
+ } else {
+ ObjectId objectId = ref.getObjectId();
+ if (objectId != null && ref.getName().equals(objectId.getName())) {
+ commits.add("'" + ref.getName() + "'"); //$NON-NLS-1$ //$NON-NLS-2$
+ } else {
+ others.add(ref.getName());
+ }
+ }
}
List<String> listings = new ArrayList<String>();
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/Merger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/Merger.java
index 983bf5c91c..bee2d03523 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/Merger.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/Merger.java
@@ -1,5 +1,6 @@
/*
* Copyright (C) 2008-2013, Google Inc.
+ * Copyright (C) 2016, Laurent Delaigue <laurent.delaigue@obeo.fr>
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
@@ -51,9 +52,11 @@ import org.eclipse.jgit.errors.NoMergeBaseException;
import org.eclipse.jgit.errors.NoMergeBaseException.MergeBaseFailureReason;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.ProgressMonitor;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevObject;
@@ -88,6 +91,13 @@ public abstract class Merger {
protected RevTree[] sourceTrees;
/**
+ * A progress monitor.
+ *
+ * @since 4.2
+ */
+ protected ProgressMonitor monitor = NullProgressMonitor.INSTANCE;
+
+ /**
* Create a new merge instance for a repository.
*
* @param local
@@ -290,4 +300,20 @@ public abstract class Merger {
* @return resulting tree, if {@link #merge(AnyObjectId[])} returned true.
*/
public abstract ObjectId getResultTreeId();
+
+ /**
+ * Set a progress monitor.
+ *
+ * @param monitor
+ * Monitor to use, can be null to indicate no progress reporting
+ * is desired.
+ * @since 4.2
+ */
+ public void setProgressMonitor(ProgressMonitor monitor) {
+ if (monitor == null) {
+ this.monitor = NullProgressMonitor.INSTANCE;
+ } else {
+ this.monitor = monitor;
+ }
+ }
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/notes/NonNoteEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NonNoteEntry.java
index 6a2d44bca9..362328a963 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/notes/NonNoteEntry.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NonNoteEntry.java
@@ -47,6 +47,7 @@ import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.TreeFormatter;
+import org.eclipse.jgit.util.Paths;
/** A tree entry found in a note branch that isn't a valid note. */
class NonNoteEntry extends ObjectId {
@@ -74,27 +75,8 @@ class NonNoteEntry extends ObjectId {
}
int pathCompare(byte[] bBuf, int bPos, int bLen, FileMode bMode) {
- return pathCompare(name, 0, name.length, mode, //
- bBuf, bPos, bLen, bMode);
- }
-
- private static int pathCompare(final byte[] aBuf, int aPos, final int aEnd,
- final FileMode aMode, final byte[] bBuf, int bPos, final int bEnd,
- final FileMode bMode) {
- while (aPos < aEnd && bPos < bEnd) {
- int cmp = (aBuf[aPos++] & 0xff) - (bBuf[bPos++] & 0xff);
- if (cmp != 0)
- return cmp;
- }
-
- if (aPos < aEnd)
- return (aBuf[aPos] & 0xff) - lastPathChar(bMode);
- if (bPos < bEnd)
- return lastPathChar(aMode) - (bBuf[bPos] & 0xff);
- return 0;
- }
-
- private static int lastPathChar(final FileMode mode) {
- return FileMode.TREE.equals(mode.getBits()) ? '/' : '\0';
+ return Paths.compare(
+ name, 0, name.length, mode.getBits(),
+ bBuf, bPos, bLen, bMode.getBits());
}
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommit.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommit.java
index c23e4e3288..e67ada6022 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommit.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommit.java
@@ -44,12 +44,17 @@
package org.eclipse.jgit.revwalk;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
import java.io.IOException;
import java.nio.charset.Charset;
+import java.nio.charset.IllegalCharsetNameException;
+import java.nio.charset.UnsupportedCharsetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import org.eclipse.jgit.annotations.Nullable;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.AnyObjectId;
@@ -441,12 +446,12 @@ public class RevCommit extends RevObject {
* @return decoded commit message as a string. Never null.
*/
public final String getFullMessage() {
- final byte[] raw = buffer;
- final int msgB = RawParseUtils.commitMessage(raw, 0);
- if (msgB < 0)
+ byte[] raw = buffer;
+ int msgB = RawParseUtils.commitMessage(raw, 0);
+ if (msgB < 0) {
return ""; //$NON-NLS-1$
- final Charset enc = RawParseUtils.parseEncoding(raw);
- return RawParseUtils.decode(enc, raw, msgB, raw.length);
+ }
+ return RawParseUtils.decode(guessEncoding(), raw, msgB, raw.length);
}
/**
@@ -465,16 +470,17 @@ public class RevCommit extends RevObject {
* spanned multiple lines. Embedded LFs are converted to spaces.
*/
public final String getShortMessage() {
- final byte[] raw = buffer;
- final int msgB = RawParseUtils.commitMessage(raw, 0);
- if (msgB < 0)
+ byte[] raw = buffer;
+ int msgB = RawParseUtils.commitMessage(raw, 0);
+ if (msgB < 0) {
return ""; //$NON-NLS-1$
+ }
- final Charset enc = RawParseUtils.parseEncoding(raw);
- final int msgE = RawParseUtils.endOfParagraph(raw, msgB);
- String str = RawParseUtils.decode(enc, raw, msgB, msgE);
- if (hasLF(raw, msgB, msgE))
+ int msgE = RawParseUtils.endOfParagraph(raw, msgB);
+ String str = RawParseUtils.decode(guessEncoding(), raw, msgB, msgE);
+ if (hasLF(raw, msgB, msgE)) {
str = StringUtils.replaceLineBreaksWithSpace(str);
+ }
return str;
}
@@ -488,18 +494,49 @@ public class RevCommit extends RevObject {
/**
* Determine the encoding of the commit message buffer.
* <p>
+ * Locates the "encoding" header (if present) and returns its value. Due to
+ * corruption in the wild this may be an invalid encoding name that is not
+ * recognized by any character encoding library.
+ * <p>
+ * If no encoding header is present, null.
+ *
+ * @return the preferred encoding of {@link #getRawBuffer()}; or null.
+ * @since 4.2
+ */
+ @Nullable
+ public final String getEncodingName() {
+ return RawParseUtils.parseEncodingName(buffer);
+ }
+
+ /**
+ * Determine the encoding of the commit message buffer.
+ * <p>
* Locates the "encoding" header (if present) and then returns the proper
* character set to apply to this buffer to evaluate its contents as
* character data.
* <p>
- * If no encoding header is present, {@link Constants#CHARSET} is assumed.
+ * If no encoding header is present {@code UTF-8} is assumed.
*
* @return the preferred encoding of {@link #getRawBuffer()}.
+ * @throws IllegalCharsetNameException
+ * if the character set requested by the encoding header is
+ * malformed and unsupportable.
+ * @throws UnsupportedCharsetException
+ * if the JRE does not support the character set requested by
+ * the encoding header.
*/
public final Charset getEncoding() {
return RawParseUtils.parseEncoding(buffer);
}
+ private Charset guessEncoding() {
+ try {
+ return getEncoding();
+ } catch (IllegalCharsetNameException | UnsupportedCharsetException e) {
+ return UTF_8;
+ }
+ }
+
/**
* Parse the footer lines (e.g. "Signed-off-by") for machine processing.
* <p>
@@ -529,7 +566,7 @@ public class RevCommit extends RevObject {
final int msgB = RawParseUtils.commitMessage(raw, 0);
final ArrayList<FooterLine> r = new ArrayList<FooterLine>(4);
- final Charset enc = getEncoding();
+ final Charset enc = guessEncoding();
for (;;) {
ptr = RawParseUtils.prevLF(raw, ptr);
if (ptr <= msgB)
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java
index bf2785e0d7..81a54bf7ea 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java
@@ -45,8 +45,12 @@
package org.eclipse.jgit.revwalk;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
import java.io.IOException;
import java.nio.charset.Charset;
+import java.nio.charset.IllegalCharsetNameException;
+import java.nio.charset.UnsupportedCharsetException;
import org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -162,7 +166,7 @@ public class RevTag extends RevObject {
int p = pos.value += 4; // "tag "
final int nameEnd = RawParseUtils.nextLF(rawTag, p) - 1;
- tagName = RawParseUtils.decode(Constants.CHARSET, rawTag, p, nameEnd);
+ tagName = RawParseUtils.decode(UTF_8, rawTag, p, nameEnd);
if (walk.isRetainBody())
buffer = rawTag;
@@ -207,12 +211,12 @@ public class RevTag extends RevObject {
* @return decoded tag message as a string. Never null.
*/
public final String getFullMessage() {
- final byte[] raw = buffer;
- final int msgB = RawParseUtils.tagMessage(raw, 0);
- if (msgB < 0)
+ byte[] raw = buffer;
+ int msgB = RawParseUtils.tagMessage(raw, 0);
+ if (msgB < 0) {
return ""; //$NON-NLS-1$
- final Charset enc = RawParseUtils.parseEncoding(raw);
- return RawParseUtils.decode(enc, raw, msgB, raw.length);
+ }
+ return RawParseUtils.decode(guessEncoding(), raw, msgB, raw.length);
}
/**
@@ -231,19 +235,28 @@ public class RevTag extends RevObject {
* multiple lines. Embedded LFs are converted to spaces.
*/
public final String getShortMessage() {
- final byte[] raw = buffer;
- final int msgB = RawParseUtils.tagMessage(raw, 0);
- if (msgB < 0)
+ byte[] raw = buffer;
+ int msgB = RawParseUtils.tagMessage(raw, 0);
+ if (msgB < 0) {
return ""; //$NON-NLS-1$
+ }
- final Charset enc = RawParseUtils.parseEncoding(raw);
- final int msgE = RawParseUtils.endOfParagraph(raw, msgB);
- String str = RawParseUtils.decode(enc, raw, msgB, msgE);
- if (RevCommit.hasLF(raw, msgB, msgE))
+ int msgE = RawParseUtils.endOfParagraph(raw, msgB);
+ String str = RawParseUtils.decode(guessEncoding(), raw, msgB, msgE);
+ if (RevCommit.hasLF(raw, msgB, msgE)) {
str = StringUtils.replaceLineBreaksWithSpace(str);
+ }
return str;
}
+ private Charset guessEncoding() {
+ try {
+ return RawParseUtils.parseEncoding(buffer);
+ } catch (IllegalCharsetNameException | UnsupportedCharsetException e) {
+ return UTF_8;
+ }
+ }
+
/**
* Get a reference to the object this tag was placed on.
* <p>
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java
index 7f9cec734d..aa36aeb1be 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java
@@ -143,7 +143,9 @@ abstract class BasePackConnection extends BaseConnection {
final int timeout = transport.getTimeout();
if (timeout > 0) {
final Thread caller = Thread.currentThread();
- myTimer = new InterruptTimer(caller.getName() + "-Timer"); //$NON-NLS-1$
+ if (myTimer == null) {
+ myTimer = new InterruptTimer(caller.getName() + "-Timer"); //$NON-NLS-1$
+ }
timeoutIn = new TimeoutInputStream(myIn, myTimer);
timeoutOut = new TimeoutOutputStream(myOut, myTimer);
timeoutIn.setTimeout(timeout * 1000);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java
index cf13582db5..754cf361a9 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java
@@ -464,8 +464,12 @@ public abstract class BasePackFetchConnection extends BasePackConnection
final PacketLineOut p = statelessRPC ? pckState : pckOut;
boolean first = true;
for (final Ref r : want) {
+ ObjectId objectId = r.getObjectId();
+ if (objectId == null) {
+ continue;
+ }
try {
- if (walk.parseAny(r.getObjectId()).has(REACHABLE)) {
+ if (walk.parseAny(objectId).has(REACHABLE)) {
// We already have this object. Asking for it is
// not a very good idea.
//
@@ -478,7 +482,7 @@ public abstract class BasePackFetchConnection extends BasePackConnection
final StringBuilder line = new StringBuilder(46);
line.append("want "); //$NON-NLS-1$
- line.append(r.getObjectId().name());
+ line.append(objectId.name());
if (first) {
line.append(enableCapabilities());
first = false;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java
index 0834c359aa..963de35d41 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java
@@ -239,8 +239,11 @@ public abstract class BasePackPushConnection extends BasePackConnection implemen
final StringBuilder sb = new StringBuilder();
ObjectId oldId = rru.getExpectedOldObjectId();
if (oldId == null) {
- Ref adv = getRef(rru.getRemoteName());
- oldId = adv != null ? adv.getObjectId() : ObjectId.zeroId();
+ final Ref advertised = getRef(rru.getRemoteName());
+ oldId = advertised != null ? advertised.getObjectId() : null;
+ if (oldId == null) {
+ oldId = ObjectId.zeroId();
+ }
}
sb.append(oldId.name());
sb.append(' ');
@@ -382,7 +385,8 @@ public abstract class BasePackPushConnection extends BasePackConnection implemen
final int oldTimeout = timeoutIn.getTimeout();
final int sendTime = (int) Math.min(packTransferTime, 28800000L);
try {
- timeoutIn.setTimeout(10 * Math.max(sendTime, oldTimeout));
+ int timeout = 10 * Math.max(sendTime, oldTimeout);
+ timeoutIn.setTimeout((timeout < 0) ? Integer.MAX_VALUE : timeout);
return pckIn.readString();
} finally {
timeoutIn.setTimeout(oldTimeout);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java
index 776a9f695a..a20e652553 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java
@@ -293,18 +293,20 @@ public abstract class BaseReceivePack {
db = into;
walk = new RevWalk(db);
- final ReceiveConfig cfg = db.getConfig().get(ReceiveConfig.KEY);
- objectChecker = cfg.newObjectChecker();
- allowCreates = cfg.allowCreates;
+ TransferConfig tc = db.getConfig().get(TransferConfig.KEY);
+ objectChecker = tc.newReceiveObjectChecker();
+
+ ReceiveConfig rc = db.getConfig().get(ReceiveConfig.KEY);
+ allowCreates = rc.allowCreates;
allowAnyDeletes = true;
- allowBranchDeletes = cfg.allowDeletes;
- allowNonFastForwards = cfg.allowNonFastForwards;
- allowOfsDelta = cfg.allowOfsDelta;
+ allowBranchDeletes = rc.allowDeletes;
+ allowNonFastForwards = rc.allowNonFastForwards;
+ allowOfsDelta = rc.allowOfsDelta;
advertiseRefsHook = AdvertiseRefsHook.DEFAULT;
refFilter = RefFilter.DEFAULT;
advertisedHaves = new HashSet<ObjectId>();
clientShallowCommits = new HashSet<ObjectId>();
- signedPushConfig = cfg.signedPush;
+ signedPushConfig = rc.signedPush;
}
/** Configuration for receive operations. */
@@ -315,32 +317,13 @@ public abstract class BaseReceivePack {
}
};
- final boolean checkReceivedObjects;
- final boolean allowLeadingZeroFileMode;
- final boolean allowInvalidPersonIdent;
- final boolean safeForWindows;
- final boolean safeForMacOS;
-
final boolean allowCreates;
final boolean allowDeletes;
final boolean allowNonFastForwards;
final boolean allowOfsDelta;
-
final SignedPushConfig signedPush;
ReceiveConfig(final Config config) {
- checkReceivedObjects = config.getBoolean(
- "receive", "fsckobjects", //$NON-NLS-1$ //$NON-NLS-2$
- config.getBoolean("transfer", "fsckobjects", false)); //$NON-NLS-1$ //$NON-NLS-2$
- allowLeadingZeroFileMode = checkReceivedObjects
- && config.getBoolean("fsck", "allowLeadingZeroFileMode", false); //$NON-NLS-1$ //$NON-NLS-2$
- allowInvalidPersonIdent = checkReceivedObjects
- && config.getBoolean("fsck", "allowInvalidPersonIdent", false); //$NON-NLS-1$ //$NON-NLS-2$
- safeForWindows = checkReceivedObjects
- && config.getBoolean("fsck", "safeForWindows", false); //$NON-NLS-1$ //$NON-NLS-2$
- safeForMacOS = checkReceivedObjects
- && config.getBoolean("fsck", "safeForMacOS", false); //$NON-NLS-1$ //$NON-NLS-2$
-
allowCreates = true;
allowDeletes = !config.getBoolean("receive", "denydeletes", false); //$NON-NLS-1$ //$NON-NLS-2$
allowNonFastForwards = !config.getBoolean("receive", //$NON-NLS-1$
@@ -349,16 +332,6 @@ public abstract class BaseReceivePack {
true);
signedPush = SignedPushConfig.KEY.parse(config);
}
-
- ObjectChecker newObjectChecker() {
- if (!checkReceivedObjects)
- return null;
- return new ObjectChecker()
- .setAllowLeadingZeroFileMode(allowLeadingZeroFileMode)
- .setAllowInvalidPersonIdent(allowInvalidPersonIdent)
- .setSafeForWindows(safeForWindows)
- .setSafeForMacOS(safeForMacOS);
- }
}
/**
@@ -1372,16 +1345,21 @@ public abstract class BaseReceivePack {
}
}
- if (cmd.getType() == ReceiveCommand.Type.DELETE && ref != null
- && !ObjectId.zeroId().equals(cmd.getOldId())
- && !ref.getObjectId().equals(cmd.getOldId())) {
- // Delete commands can be sent with the old id matching our
- // advertised value, *OR* with the old id being 0{40}. Any
- // other requested old id is invalid.
- //
- cmd.setResult(Result.REJECTED_OTHER_REASON,
- JGitText.get().invalidOldIdSent);
- continue;
+ if (cmd.getType() == ReceiveCommand.Type.DELETE && ref != null) {
+ ObjectId id = ref.getObjectId();
+ if (id == null) {
+ id = ObjectId.zeroId();
+ }
+ if (!ObjectId.zeroId().equals(cmd.getOldId())
+ && !id.equals(cmd.getOldId())) {
+ // Delete commands can be sent with the old id matching our
+ // advertised value, *OR* with the old id being 0{40}. Any
+ // other requested old id is invalid.
+ //
+ cmd.setResult(Result.REJECTED_OTHER_REASON,
+ JGitText.get().invalidOldIdSent);
+ continue;
+ }
}
if (cmd.getType() == ReceiveCommand.Type.UPDATE) {
@@ -1391,8 +1369,15 @@ public abstract class BaseReceivePack {
cmd.setResult(Result.REJECTED_OTHER_REASON, JGitText.get().noSuchRef);
continue;
}
+ ObjectId id = ref.getObjectId();
+ if (id == null) {
+ // We cannot update unborn branch
+ cmd.setResult(Result.REJECTED_OTHER_REASON,
+ JGitText.get().cannotUpdateUnbornBranch);
+ continue;
+ }
- if (!ref.getObjectId().equals(cmd.getOldId())) {
+ if (!id.equals(cmd.getOldId())) {
// A properly functioning client will send the same
// object id we advertised.
//
@@ -1468,10 +1453,7 @@ public abstract class BaseReceivePack {
* @since 3.6
*/
protected void failPendingCommands() {
- for (ReceiveCommand cmd : commands) {
- if (cmd.getResult() == Result.NOT_ATTEMPTED)
- cmd.setResult(Result.REJECTED_OTHER_REASON, JGitText.get().transactionAborted);
- }
+ ReceiveCommand.abort(commands);
}
/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BundleFetchConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BundleFetchConnection.java
index e53c04b535..8038fa4d31 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BundleFetchConnection.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BundleFetchConnection.java
@@ -161,16 +161,23 @@ class BundleFetchConnection extends BaseFetchConnection {
}
private String readLine(final byte[] hdrbuf) throws IOException {
- bin.mark(hdrbuf.length);
- final int cnt = bin.read(hdrbuf);
- int lf = 0;
- while (lf < cnt && hdrbuf[lf] != '\n')
- lf++;
- bin.reset();
- IO.skipFully(bin, lf);
- if (lf < cnt && hdrbuf[lf] == '\n')
- IO.skipFully(bin, 1);
- return RawParseUtils.decode(Constants.CHARSET, hdrbuf, 0, lf);
+ StringBuilder line = new StringBuilder();
+ boolean done = false;
+ while (!done) {
+ bin.mark(hdrbuf.length);
+ final int cnt = bin.read(hdrbuf);
+ int lf = 0;
+ while (lf < cnt && hdrbuf[lf] != '\n')
+ lf++;
+ bin.reset();
+ IO.skipFully(bin, lf);
+ if (lf < cnt && hdrbuf[lf] == '\n') {
+ IO.skipFully(bin, 1);
+ done = true;
+ }
+ line.append(RawParseUtils.decode(Constants.CHARSET, hdrbuf, 0, lf));
+ }
+ return line.toString();
}
public boolean didFetchTestConnectivity() {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ChainingCredentialsProvider.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ChainingCredentialsProvider.java
index 3e0ee2f645..3941d3c552 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ChainingCredentialsProvider.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ChainingCredentialsProvider.java
@@ -113,19 +113,18 @@ public class ChainingCredentialsProvider extends CredentialsProvider {
throws UnsupportedCredentialItem {
for (CredentialsProvider p : credentialProviders) {
if (p.supports(items)) {
- p.get(uri, items);
- if (isAnyNull(items))
+ if (!p.get(uri, items)) {
+ if (p.isInteractive()) {
+ return false; // user cancelled the request
+ }
continue;
+ }
+ if (isAnyNull(items)) {
+ continue;
+ }
return true;
}
}
return false;
}
-
- private boolean isAnyNull(CredentialItem... items) {
- for (CredentialItem i : items)
- if (i == null)
- return true;
- return false;
- }
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Connection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Connection.java
index 0ff9fcea74..da288ec31e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Connection.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Connection.java
@@ -59,8 +59,7 @@ import org.eclipse.jgit.lib.Ref;
*
* @see Transport
*/
-public interface Connection {
-
+public interface Connection extends AutoCloseable {
/**
* Get the complete map of refs advertised as available for fetching or
* pushing.
@@ -108,6 +107,10 @@ public interface Connection {
* <p>
* If additional messages were produced by the remote peer, these should
* still be retained in the connection instance for {@link #getMessages()}.
+ * <p>
+ * {@code AutoClosable.close()} declares that it throws {@link Exception}.
+ * Implementers shouldn't throw checked exceptions. This override narrows
+ * the signature to prevent them from doing so.
*/
public void close();
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/CredentialsProvider.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/CredentialsProvider.java
index 464d0f9ee5..4800f6826f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/CredentialsProvider.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/CredentialsProvider.java
@@ -81,6 +81,20 @@ public abstract class CredentialsProvider {
}
/**
+ * @param items
+ * credential items to check
+ * @return {@code true} if any of the passed items is null, {@code false}
+ * otherwise
+ * @since 4.2
+ */
+ protected static boolean isAnyNull(CredentialItem... items) {
+ for (CredentialItem i : items)
+ if (i == null)
+ return true;
+ return false;
+ }
+
+ /**
* Check if the provider is interactive with the end-user.
*
* An interactive provider may try to open a dialog box, or prompt for input
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Daemon.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Daemon.java
index d9e0b937e8..2593ba556d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Daemon.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Daemon.java
@@ -256,6 +256,16 @@ public class Daemon {
}
/**
+ * Get the factory used to construct per-request ReceivePack.
+ *
+ * @return the factory.
+ * @since 4.2
+ */
+ public ReceivePackFactory<DaemonClient> getReceivePackFactory() {
+ return receivePackFactory;
+ }
+
+ /**
* Set the factory to construct and configure per-request ReceivePack.
*
* @param factory
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java
index 9aae1c37aa..c4b3f83048 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java
@@ -397,11 +397,17 @@ class FetchProcess {
private void expandFetchTags() throws TransportException {
final Map<String, Ref> haveRefs = localRefs();
for (final Ref r : conn.getRefs()) {
- if (!isTag(r))
+ if (!isTag(r)) {
+ continue;
+ }
+ ObjectId id = r.getObjectId();
+ if (id == null) {
continue;
+ }
final Ref local = haveRefs.get(r.getName());
- if (local == null || !r.getObjectId().equals(local.getObjectId()))
+ if (local == null || !id.equals(local.getObjectId())) {
wantTag(r);
+ }
}
}
@@ -413,6 +419,11 @@ class FetchProcess {
private void want(final Ref src, final RefSpec spec)
throws TransportException {
final ObjectId newId = src.getObjectId();
+ if (newId == null) {
+ throw new NullPointerException(MessageFormat.format(
+ JGitText.get().transportProvidedRefWithNoObjectId,
+ src.getName()));
+ }
if (spec.getDestination() != null) {
final TrackingRefUpdate tru = createUpdate(spec, newId);
if (newId.equals(tru.getOldObjectId()))
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/JschSession.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/JschSession.java
index 85109a5bf0..1dfe5d9797 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/JschSession.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/JschSession.java
@@ -149,14 +149,27 @@ public class JschSession implements RemoteSession {
channel.setCommand(commandName);
setupStreams();
channel.connect(timeout > 0 ? timeout * 1000 : 0);
- if (!channel.isConnected())
+ if (!channel.isConnected()) {
+ closeOutputStream();
throw new TransportException(uri,
JGitText.get().connectionFailed);
+ }
} catch (JSchException e) {
+ closeOutputStream();
throw new TransportException(uri, e.getMessage(), e);
}
}
+ private void closeOutputStream() {
+ if (outputStream != null) {
+ try {
+ outputStream.close();
+ } catch (IOException ioe) {
+ // ignore
+ }
+ }
+ }
+
private void setupStreams() throws IOException {
inputStream = channel.getInputStream();
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRCCredentialsProvider.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRCCredentialsProvider.java
index 74909998ce..4037545e9d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRCCredentialsProvider.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRCCredentialsProvider.java
@@ -105,12 +105,11 @@ public class NetRCCredentialsProvider extends CredentialsProvider {
throw new UnsupportedCredentialItem(uri, i.getClass().getName()
+ ":" + i.getPromptText()); //$NON-NLS-1$
}
- return true;
+ return !isAnyNull(items);
}
@Override
public boolean isInteractive() {
return false;
}
-
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackParser.java
index 6e5fc9f009..b96fe885e1 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackParser.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackParser.java
@@ -1049,8 +1049,11 @@ public abstract class PackParser {
final byte[] data) throws IOException {
if (objCheck != null) {
try {
- objCheck.check(type, data);
+ objCheck.check(id, type, data);
} catch (CorruptObjectException e) {
+ if (e.getErrorType() != null) {
+ throw e;
+ }
throw new CorruptObjectException(MessageFormat.format(
JGitText.get().invalidObject,
Constants.typeString(type),
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProgressSpinner.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProgressSpinner.java
new file mode 100644
index 0000000000..ac048a14a9
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProgressSpinner.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2015, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.transport;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A simple spinner connected to an {@code OutputStream}.
+ * <p>
+ * This is class is not thread-safe. The update method may only be used from a
+ * single thread. Updates are sent only as frequently as {@link #update()} is
+ * invoked by the caller, and are capped at no more than 2 times per second by
+ * requiring at least 500 milliseconds between updates.
+ *
+ * @since 4.2
+ */
+public class ProgressSpinner {
+ private static final long MIN_REFRESH_MILLIS = 500;
+ private static final char[] STATES = new char[] { '-', '\\', '|', '/' };
+
+ private final OutputStream out;
+ private String msg;
+ private int state;
+ private boolean write;
+ private boolean shown;
+ private long nextUpdateMillis;
+
+ /**
+ * Initialize a new spinner.
+ *
+ * @param out
+ * where to send output to.
+ */
+ public ProgressSpinner(OutputStream out) {
+ this.out = out;
+ this.write = true;
+ }
+
+ /**
+ * Begin a time consuming task.
+ *
+ * @param title
+ * description of the task, suitable for human viewing.
+ * @param delay
+ * delay to wait before displaying anything at all.
+ * @param delayUnits
+ * unit for {@code delay}.
+ */
+ public void beginTask(String title, long delay, TimeUnit delayUnits) {
+ msg = title;
+ state = 0;
+ shown = false;
+
+ long now = System.currentTimeMillis();
+ if (delay > 0) {
+ nextUpdateMillis = now + delayUnits.toMillis(delay);
+ } else {
+ send(now);
+ }
+ }
+
+ /** Update the spinner if it is showing. */
+ public void update() {
+ long now = System.currentTimeMillis();
+ if (now >= nextUpdateMillis) {
+ send(now);
+ state = (state + 1) % STATES.length;
+ }
+ }
+
+ private void send(long now) {
+ StringBuilder buf = new StringBuilder(msg.length() + 16);
+ buf.append('\r').append(msg).append("... ("); //$NON-NLS-1$
+ buf.append(STATES[state]);
+ buf.append(") "); //$NON-NLS-1$
+ shown = true;
+ write(buf.toString());
+ nextUpdateMillis = now + MIN_REFRESH_MILLIS;
+ }
+
+ /**
+ * Denote the current task completed.
+ *
+ * @param result
+ * text to print after the task's title
+ * {@code "$title ... $result"}.
+ */
+ public void endTask(String result) {
+ if (shown) {
+ write('\r' + msg + "... " + result + "\n"); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+ }
+
+ private void write(String s) {
+ if (write) {
+ try {
+ out.write(s.getBytes(UTF_8));
+ out.flush();
+ } catch (IOException e) {
+ write = false;
+ }
+ }
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java
index 4fd192dbb2..5cea88215a 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java
@@ -188,8 +188,13 @@ class PushProcess {
final Map<String, RemoteRefUpdate> result = new HashMap<String, RemoteRefUpdate>();
for (final RemoteRefUpdate rru : toPush.values()) {
final Ref advertisedRef = connection.getRef(rru.getRemoteName());
- final ObjectId advertisedOld = (advertisedRef == null ? ObjectId
- .zeroId() : advertisedRef.getObjectId());
+ ObjectId advertisedOld = null;
+ if (advertisedRef != null) {
+ advertisedOld = advertisedRef.getObjectId();
+ }
+ if (advertisedOld == null) {
+ advertisedOld = ObjectId.zeroId();
+ }
if (rru.getNewObjectId().equals(advertisedOld)) {
if (rru.isDelete()) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java
index 5702b6d7b9..2b21c4a8fe 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java
@@ -43,6 +43,9 @@
package org.eclipse.jgit.transport;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
+
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
@@ -168,6 +171,25 @@ public class ReceiveCommand {
return filter((Iterable<ReceiveCommand>) commands, want);
}
+ /**
+ * Set unprocessed commands as failed due to transaction aborted.
+ * <p>
+ * If a command is still {@link Result#NOT_ATTEMPTED} it will be set to
+ * {@link Result#REJECTED_OTHER_REASON}.
+ *
+ * @param commands
+ * commands to mark as failed.
+ * @since 4.2
+ */
+ public static void abort(Iterable<ReceiveCommand> commands) {
+ for (ReceiveCommand c : commands) {
+ if (c.getResult() == NOT_ATTEMPTED) {
+ c.setResult(REJECTED_OTHER_REASON,
+ JGitText.get().transactionAborted);
+ }
+ }
+ }
+
private final ObjectId oldId;
private final ObjectId newId;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java
index 66ffc3abe9..0e803bdaf7 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java
@@ -457,10 +457,6 @@ public class RefSpec implements Serializable {
if (i != -1) {
if (s.indexOf('*', i + 1) > i)
return false;
- if (i > 0 && s.charAt(i - 1) != '/')
- return false;
- if (i < s.length() - 1 && s.charAt(i + 1) != '/')
- return false;
}
return true;
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandInputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandInputStream.java
index cf388e2718..fe9f2a3155 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandInputStream.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandInputStream.java
@@ -78,12 +78,8 @@ import org.eclipse.jgit.util.RawParseUtils;
* @see SideBandOutputStream
*/
class SideBandInputStream extends InputStream {
- private static final String PFX_REMOTE = JGitText.get().prefixRemote;
-
static final int CH_DATA = 1;
-
static final int CH_PROGRESS = 2;
-
static final int CH_ERROR = 3;
private static Pattern P_UNBOUNDED = Pattern
@@ -174,7 +170,7 @@ class SideBandInputStream extends InputStream {
continue;
case CH_ERROR:
eof = true;
- throw new TransportException(PFX_REMOTE + readString(available));
+ throw new TransportException(remote(readString(available)));
default:
throw new PackProtocolException(
MessageFormat.format(JGitText.get().invalidChannel,
@@ -241,7 +237,18 @@ class SideBandInputStream extends InputStream {
}
private void beginTask(final int totalWorkUnits) {
- monitor.beginTask(PFX_REMOTE + currentTask, totalWorkUnits);
+ monitor.beginTask(remote(currentTask), totalWorkUnits);
+ }
+
+ private static String remote(String msg) {
+ String prefix = JGitText.get().prefixRemote;
+ StringBuilder r = new StringBuilder(prefix.length() + msg.length() + 1);
+ r.append(prefix);
+ if (prefix.length() > 0 && prefix.charAt(prefix.length() - 1) != ' ') {
+ r.append(' ');
+ }
+ r.append(msg);
+ return r.toString();
}
private String readString(final int len) throws IOException {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java
index f0c513427a..72c9c8b93e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java
@@ -43,12 +43,20 @@
package org.eclipse.jgit.transport;
+import static org.eclipse.jgit.util.StringUtils.equalsIgnoreCase;
+import static org.eclipse.jgit.util.StringUtils.toLowerCase;
+
+import java.io.File;
+import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.internal.storage.file.LazyObjectIdSetFile;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Config.SectionParser;
import org.eclipse.jgit.lib.ObjectChecker;
+import org.eclipse.jgit.lib.ObjectIdSet;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.util.SystemReader;
@@ -58,6 +66,8 @@ import org.eclipse.jgit.util.SystemReader;
* parameters.
*/
public class TransferConfig {
+ private static final String FSCK = "fsck"; //$NON-NLS-1$
+
/** Key for {@link Config#get(SectionParser)}. */
public static final Config.SectionParser<TransferConfig> KEY = new SectionParser<TransferConfig>() {
public TransferConfig parse(final Config cfg) {
@@ -65,8 +75,14 @@ public class TransferConfig {
}
};
- private final boolean checkReceivedObjects;
- private final boolean allowLeadingZeroFileMode;
+ enum FsckMode {
+ ERROR, WARN, IGNORE;
+ }
+
+ private final boolean fetchFsck;
+ private final boolean receiveFsck;
+ private final String fsckSkipList;
+ private final EnumSet<ObjectChecker.ErrorType> ignore;
private final boolean allowInvalidPersonIdent;
private final boolean safeForWindows;
private final boolean safeForMacOS;
@@ -79,20 +95,47 @@ public class TransferConfig {
}
TransferConfig(final Config rc) {
- checkReceivedObjects = rc.getBoolean(
- "fetch", "fsckobjects", //$NON-NLS-1$ //$NON-NLS-2$
- rc.getBoolean("transfer", "fsckobjects", false)); //$NON-NLS-1$ //$NON-NLS-2$
- allowLeadingZeroFileMode = checkReceivedObjects
- && rc.getBoolean("fsck", "allowLeadingZeroFileMode", false); //$NON-NLS-1$ //$NON-NLS-2$
- allowInvalidPersonIdent = checkReceivedObjects
- && rc.getBoolean("fsck", "allowInvalidPersonIdent", false); //$NON-NLS-1$ //$NON-NLS-2$
- safeForWindows = checkReceivedObjects
- && rc.getBoolean("fsck", "safeForWindows", //$NON-NLS-1$ //$NON-NLS-2$
+ boolean fsck = rc.getBoolean("transfer", "fsckobjects", false); //$NON-NLS-1$ //$NON-NLS-2$
+ fetchFsck = rc.getBoolean("fetch", "fsckobjects", fsck); //$NON-NLS-1$ //$NON-NLS-2$
+ receiveFsck = rc.getBoolean("receive", "fsckobjects", fsck); //$NON-NLS-1$ //$NON-NLS-2$
+ fsckSkipList = rc.getString(FSCK, null, "skipList"); //$NON-NLS-1$
+ allowInvalidPersonIdent = rc.getBoolean(FSCK, "allowInvalidPersonIdent", false); //$NON-NLS-1$
+ safeForWindows = rc.getBoolean(FSCK, "safeForWindows", //$NON-NLS-1$
SystemReader.getInstance().isWindows());
- safeForMacOS = checkReceivedObjects
- && rc.getBoolean("fsck", "safeForMacOS", //$NON-NLS-1$ //$NON-NLS-2$
+ safeForMacOS = rc.getBoolean(FSCK, "safeForMacOS", //$NON-NLS-1$
SystemReader.getInstance().isMacOS());
+ ignore = EnumSet.noneOf(ObjectChecker.ErrorType.class);
+ EnumSet<ObjectChecker.ErrorType> set = EnumSet
+ .noneOf(ObjectChecker.ErrorType.class);
+ for (String key : rc.getNames(FSCK)) {
+ if (equalsIgnoreCase(key, "skipList") //$NON-NLS-1$
+ || equalsIgnoreCase(key, "allowLeadingZeroFileMode") //$NON-NLS-1$
+ || equalsIgnoreCase(key, "allowInvalidPersonIdent") //$NON-NLS-1$
+ || equalsIgnoreCase(key, "safeForWindows") //$NON-NLS-1$
+ || equalsIgnoreCase(key, "safeForMacOS")) { //$NON-NLS-1$
+ continue;
+ }
+
+ ObjectChecker.ErrorType id = FsckKeyNameHolder.parse(key);
+ if (id != null) {
+ switch (rc.getEnum(FSCK, null, key, FsckMode.ERROR)) {
+ case ERROR:
+ ignore.remove(id);
+ break;
+ case WARN:
+ case IGNORE:
+ ignore.add(id);
+ break;
+ }
+ set.add(id);
+ }
+ }
+ if (!set.contains(ObjectChecker.ErrorType.ZERO_PADDED_FILEMODE)
+ && rc.getBoolean(FSCK, "allowLeadingZeroFileMode", false)) { //$NON-NLS-1$
+ ignore.add(ObjectChecker.ErrorType.ZERO_PADDED_FILEMODE);
+ }
+
allowTipSha1InWant = rc.getBoolean(
"uploadpack", "allowtipsha1inwant", false); //$NON-NLS-1$ //$NON-NLS-2$
allowReachableSha1InWant = rc.getBoolean(
@@ -105,14 +148,38 @@ public class TransferConfig {
* enabled in the repository configuration.
* @since 3.6
*/
+ @Nullable
public ObjectChecker newObjectChecker() {
- if (!checkReceivedObjects)
+ return newObjectChecker(fetchFsck);
+ }
+
+ /**
+ * @return checker to verify objects pushed into this repository, or null if
+ * checking is not enabled in the repository configuration.
+ * @since 4.2
+ */
+ @Nullable
+ public ObjectChecker newReceiveObjectChecker() {
+ return newObjectChecker(receiveFsck);
+ }
+
+ private ObjectChecker newObjectChecker(boolean check) {
+ if (!check) {
return null;
+ }
return new ObjectChecker()
- .setAllowLeadingZeroFileMode(allowLeadingZeroFileMode)
+ .setIgnore(ignore)
.setAllowInvalidPersonIdent(allowInvalidPersonIdent)
.setSafeForWindows(safeForWindows)
- .setSafeForMacOS(safeForMacOS);
+ .setSafeForMacOS(safeForMacOS)
+ .setSkipList(skipList());
+ }
+
+ private ObjectIdSet skipList() {
+ if (fsckSkipList != null && !fsckSkipList.isEmpty()) {
+ return new LazyObjectIdSetFile(new File(fsckSkipList));
+ }
+ return null;
}
/**
@@ -161,4 +228,34 @@ public class TransferConfig {
}
};
}
+
+ static class FsckKeyNameHolder {
+ private static final Map<String, ObjectChecker.ErrorType> errors;
+
+ static {
+ errors = new HashMap<>();
+ for (ObjectChecker.ErrorType m : ObjectChecker.ErrorType.values()) {
+ errors.put(keyNameFor(m.name()), m);
+ }
+ }
+
+ @Nullable
+ static ObjectChecker.ErrorType parse(String key) {
+ return errors.get(toLowerCase(key));
+ }
+
+ private static String keyNameFor(String name) {
+ StringBuilder r = new StringBuilder(name.length());
+ for (int i = 0; i < name.length(); i++) {
+ char c = name.charAt(i);
+ if (c != '_') {
+ r.append(c);
+ }
+ }
+ return toLowerCase(r.toString());
+ }
+
+ private FsckKeyNameHolder() {
+ }
+ }
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java
index 6af153cbc9..9e6d1f68f5 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java
@@ -98,7 +98,7 @@ import org.eclipse.jgit.storage.pack.PackConfig;
* Transport instances and the connections they create are not thread-safe.
* Callers must ensure a transport is accessed by only one thread at a time.
*/
-public abstract class Transport {
+public abstract class Transport implements AutoCloseable {
/** Type of operation a Transport is being opened for. */
public enum Operation {
/** Transport is to fetch objects locally. */
@@ -1353,6 +1353,10 @@ public abstract class Transport {
* must close that network socket, disconnecting the two peers. If the
* remote repository is actually local (same system) this method must close
* any open file handles used to read the "remote" repository.
+ * <p>
+ * {@code AutoClosable.close()} declares that it throws {@link Exception}.
+ * Implementers shouldn't throw checked exceptions. This override narrows
+ * the signature to prevent them from doing so.
*/
public abstract void close();
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java
index 2f9dfa1d6b..3ee2feb140 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java
@@ -83,7 +83,7 @@ public class URIish implements Serializable {
* capturing groups: the first containing the user and the second containing
* the password
*/
- private static final String OPT_USER_PWD_P = "(?:([^/:@]+)(?::([^\\\\/]+))?@)?"; //$NON-NLS-1$
+ private static final String OPT_USER_PWD_P = "(?:([^/:]+)(?::([^\\\\/]+))?@)?"; //$NON-NLS-1$
/**
* Part of a pattern which matches the host part of URIs. Defines one
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkFetchConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkFetchConnection.java
index 1c6b8b7363..17edfdc4fb 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkFetchConnection.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkFetchConnection.java
@@ -267,6 +267,10 @@ class WalkFetchConnection extends BaseFetchConnection {
final HashSet<ObjectId> inWorkQueue = new HashSet<ObjectId>();
for (final Ref r : want) {
final ObjectId id = r.getObjectId();
+ if (id == null) {
+ throw new NullPointerException(MessageFormat.format(
+ JGitText.get().transportProvidedRefWithNoObjectId, r.getName()));
+ }
try {
final RevObject obj = revWalk.parseAny(id);
if (obj.has(COMPLETE))
@@ -633,10 +637,11 @@ class WalkFetchConnection extends BaseFetchConnection {
final byte[] raw = uol.getCachedBytes();
if (objCheck != null) {
try {
- objCheck.check(type, raw);
+ objCheck.check(id, type, raw);
} catch (CorruptObjectException e) {
- throw new TransportException(MessageFormat.format(JGitText.get().transportExceptionInvalid
- , Constants.typeString(type), id.name(), e.getMessage()));
+ throw new TransportException(MessageFormat.format(
+ JGitText.get().transportExceptionInvalid,
+ Constants.typeString(type), id.name(), e.getMessage()));
}
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java
index 5e71889574..58136355eb 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java
@@ -59,6 +59,7 @@ import org.eclipse.jgit.lib.MutableObjectId;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.treewalk.filter.TreeFilter;
+import org.eclipse.jgit.util.Paths;
/**
* Walks a Git tree (directory) in Git sort order.
@@ -382,20 +383,9 @@ public abstract class AbstractTreeIterator {
}
private int pathCompare(byte[] b, int bPos, int bEnd, int bMode, int aPos) {
- final byte[] a = path;
- final int aEnd = pathLen;
-
- for (; aPos < aEnd && bPos < bEnd; aPos++, bPos++) {
- final int cmp = (a[aPos] & 0xff) - (b[bPos] & 0xff);
- if (cmp != 0)
- return cmp;
- }
-
- if (aPos < aEnd)
- return (a[aPos] & 0xff) - lastPathChar(bMode);
- if (bPos < bEnd)
- return lastPathChar(mode) - (b[bPos] & 0xff);
- return lastPathChar(mode) - lastPathChar(bMode);
+ return Paths.compare(
+ path, aPos, pathLen, mode,
+ b, bPos, bEnd, bMode);
}
private static int alreadyMatch(AbstractTreeIterator a,
@@ -412,10 +402,6 @@ public abstract class AbstractTreeIterator {
}
}
- private static int lastPathChar(final int mode) {
- return FileMode.TREE.equals(mode) ? '/' : '\0';
- }
-
/**
* Check if the current entry of both iterators has the same id.
* <p>
@@ -692,6 +678,14 @@ public abstract class AbstractTreeIterator {
}
/**
+ * @return true if the iterator implements {@link #stopWalk()}.
+ * @since 4.2
+ */
+ protected boolean needsStopWalk() {
+ return false;
+ }
+
+ /**
* @return the length of the name component of the path for the current entry
*/
public int getNameLength() {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/EmptyTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/EmptyTreeIterator.java
index 8dbf80e6a8..ec4a84eff3 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/EmptyTreeIterator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/EmptyTreeIterator.java
@@ -142,4 +142,9 @@ public class EmptyTreeIterator extends AbstractTreeIterator {
if (parent != null)
parent.stopWalk();
}
+
+ @Override
+ protected boolean needsStopWalk() {
+ return parent != null && parent.needsStopWalk();
+ }
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/NameConflictTreeWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/NameConflictTreeWalk.java
index 350f563964..d2195a874c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/NameConflictTreeWalk.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/NameConflictTreeWalk.java
@@ -43,6 +43,8 @@
package org.eclipse.jgit.treewalk;
+import java.io.IOException;
+
import org.eclipse.jgit.dircache.DirCacheBuilder;
import org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.lib.FileMode;
@@ -338,6 +340,41 @@ public class NameConflictTreeWalk extends TreeWalk {
dfConflict = null;
}
+ void stopWalk() throws IOException {
+ if (!needsStopWalk()) {
+ return;
+ }
+
+ // Name conflicts make aborting early difficult. Multiple paths may
+ // exist between the file and directory versions of a name. To ensure
+ // the directory version is skipped over (as it was previously visited
+ // during the file version step) requires popping up the stack and
+ // finishing out each subtree that the walker dove into. Siblings in
+ // parents do not need to be recursed into, bounding the cost.
+ for (;;) {
+ AbstractTreeIterator t = min();
+ if (t.eof()) {
+ if (depth > 0) {
+ exitSubtree();
+ popEntriesEqual();
+ continue;
+ }
+ return;
+ }
+ currentHead = t;
+ skipEntriesEqual();
+ }
+ }
+
+ private boolean needsStopWalk() {
+ for (AbstractTreeIterator t : trees) {
+ if (t.needsStopWalk()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* True if the current entry is covered by a directory/file conflict.
*
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java
index 06dc0bf6d0..5cd713da78 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java
@@ -57,6 +57,7 @@ import org.eclipse.jgit.attributes.Attributes;
import org.eclipse.jgit.attributes.AttributesNode;
import org.eclipse.jgit.attributes.AttributesNodeProvider;
import org.eclipse.jgit.attributes.AttributesProvider;
+import org.eclipse.jgit.dircache.DirCacheBuildIterator;
import org.eclipse.jgit.dircache.DirCacheIterator;
import org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -256,7 +257,7 @@ public class TreeWalk implements AutoCloseable, AttributesProvider {
private boolean postOrderTraversal;
- private int depth;
+ int depth;
private boolean advance;
@@ -573,18 +574,13 @@ public class TreeWalk implements AutoCloseable, AttributesProvider {
* @param p
* an iterator to walk over. The iterator should be new, with no
* parent, and should still be positioned before the first entry.
- * The tree which the iterator operates on must have the same root
- * as other trees in the walk.
- *
+ * The tree which the iterator operates on must have the same
+ * root as other trees in the walk.
* @return position of this tree within the walker.
- * @throws CorruptObjectException
- * the iterator was unable to obtain its first entry, due to
- * possible data corruption within the backing data store.
*/
- public int addTree(final AbstractTreeIterator p)
- throws CorruptObjectException {
- final int n = trees.length;
- final AbstractTreeIterator[] newTrees = new AbstractTreeIterator[n + 1];
+ public int addTree(AbstractTreeIterator p) {
+ int n = trees.length;
+ AbstractTreeIterator[] newTrees = new AbstractTreeIterator[n + 1];
System.arraycopy(trees, 0, newTrees, 0, n);
newTrees[n] = p;
@@ -665,13 +661,30 @@ public class TreeWalk implements AutoCloseable, AttributesProvider {
return true;
}
} catch (StopWalkException stop) {
- for (final AbstractTreeIterator t : trees)
- t.stopWalk();
+ stopWalk();
return false;
}
}
/**
+ * Notify iterators the walk is aborting.
+ * <p>
+ * Primarily to notify {@link DirCacheBuildIterator} the walk is aborting so
+ * that it can copy any remaining entries.
+ *
+ * @throws IOException
+ * if traversal of remaining entries throws an exception during
+ * object access. This should never occur as remaining trees
+ * should already be in memory, however the methods used to
+ * finish traversal are declared to throw IOException.
+ */
+ void stopWalk() throws IOException {
+ for (AbstractTreeIterator t : trees) {
+ t.stopWalk();
+ }
+ }
+
+ /**
* Obtain the tree iterator for the current entry.
* <p>
* Entering into (or exiting out of) a subtree causes the current tree
@@ -861,10 +874,13 @@ public class TreeWalk implements AutoCloseable, AttributesProvider {
* Test if the supplied path matches the current entry's path.
* <p>
* This method tests that the supplied path is exactly equal to the current
- * entry, or is one of its parent directories. It is faster to use this
+ * entry or is one of its parent directories. It is faster to use this
* method then to use {@link #getPathString()} to first create a String
* object, then test <code>startsWith</code> or some other type of string
* match function.
+ * <p>
+ * If the current entry is a subtree, then all paths within the subtree
+ * are considered to match it.
*
* @param p
* path buffer to test. Callers should ensure the path does not
@@ -900,7 +916,7 @@ public class TreeWalk implements AutoCloseable, AttributesProvider {
// If p[ci] == '/' then pattern matches this subtree,
// otherwise we cannot be certain so we return -1.
//
- return p[ci] == '/' ? 0 : -1;
+ return p[ci] == '/' && FileMode.TREE.equals(t.mode) ? 0 : -1;
}
// Both strings are identical.
@@ -1062,7 +1078,7 @@ public class TreeWalk implements AutoCloseable, AttributesProvider {
}
}
- private void exitSubtree() {
+ void exitSubtree() {
depth--;
for (int i = 0; i < trees.length; i++)
trees[i] = trees[i].parent;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java
index 94beeeb56f..0d617ee7f9 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java
@@ -89,6 +89,7 @@ import org.eclipse.jgit.submodule.SubmoduleWalk;
import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.FS.ExecutionResult;
import org.eclipse.jgit.util.IO;
+import org.eclipse.jgit.util.Paths;
import org.eclipse.jgit.util.RawParseUtils;
import org.eclipse.jgit.util.io.EolCanonicalizingInputStream;
@@ -692,31 +693,13 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator {
}
private static final Comparator<Entry> ENTRY_CMP = new Comparator<Entry>() {
- public int compare(final Entry o1, final Entry o2) {
- final byte[] a = o1.encodedName;
- final byte[] b = o2.encodedName;
- final int aLen = o1.encodedNameLen;
- final int bLen = o2.encodedNameLen;
- int cPos;
-
- for (cPos = 0; cPos < aLen && cPos < bLen; cPos++) {
- final int cmp = (a[cPos] & 0xff) - (b[cPos] & 0xff);
- if (cmp != 0)
- return cmp;
- }
-
- if (cPos < aLen)
- return (a[cPos] & 0xff) - lastPathChar(o2);
- if (cPos < bLen)
- return lastPathChar(o1) - (b[cPos] & 0xff);
- return lastPathChar(o1) - lastPathChar(o2);
+ public int compare(Entry a, Entry b) {
+ return Paths.compare(
+ a.encodedName, 0, a.encodedNameLen, a.getMode().getBits(),
+ b.encodedName, 0, b.encodedNameLen, b.getMode().getBits());
}
};
- static int lastPathChar(final Entry e) {
- return e.getMode() == FileMode.TREE ? '/' : '\0';
- }
-
/**
* Constructor helper.
*
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/PathFilterGroup.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/PathFilterGroup.java
index bdfde0bfcd..7601956c43 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/PathFilterGroup.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/PathFilterGroup.java
@@ -245,9 +245,9 @@ public class PathFilterGroup {
int hash = hasher.nextHash();
if (fullpaths.contains(rp, hasher.length(), hash))
return true;
- if (!hasher.hasNext())
- if (prefixes.contains(rp, hasher.length(), hash))
- return true;
+ if (!hasher.hasNext() && walker.isSubtree()
+ && prefixes.contains(rp, hasher.length(), hash))
+ return true;
}
final int cmp = walker.isPathPrefix(max, max.length);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/ChangeIdUtil.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/ChangeIdUtil.java
index 35fc99e54e..e14096e598 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/ChangeIdUtil.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/ChangeIdUtil.java
@@ -42,7 +42,6 @@
*/
package org.eclipse.jgit.util;
-import java.io.IOException;
import java.util.regex.Pattern;
import org.eclipse.jgit.lib.Constants;
@@ -90,12 +89,10 @@ public class ChangeIdUtil {
* The commit message
* @return the change id SHA1 string (without the 'I') or null if the
* message is not complete enough
- * @throws IOException
*/
public static ObjectId computeChangeId(final ObjectId treeId,
final ObjectId firstParentId, final PersonIdent author,
- final PersonIdent committer, final String message)
- throws IOException {
+ final PersonIdent committer, final String message) {
String cleanMessage = clean(message);
if (cleanMessage.length() == 0)
return null;
@@ -116,8 +113,7 @@ public class ChangeIdUtil {
b.append("\n\n"); //$NON-NLS-1$
b.append(cleanMessage);
try (ObjectInserter f = new ObjectInserter.Formatter()) {
- return f.idFor(Constants.OBJ_COMMIT, //
- b.toString().getBytes(Constants.CHARACTER_ENCODING));
+ return f.idFor(Constants.OBJ_COMMIT, Constants.encode(b.toString()));
}
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java
index 727ea79cc9..aa101f73f9 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java
@@ -409,7 +409,9 @@ public class FileUtils {
throws IOException {
Path nioPath = path.toPath();
if (Files.exists(nioPath, LinkOption.NOFOLLOW_LINKS)) {
- if (Files.isRegularFile(nioPath)) {
+ BasicFileAttributes attrs = Files.readAttributes(nioPath,
+ BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
+ if (attrs.isRegularFile() || attrs.isSymbolicLink()) {
delete(path);
} else {
delete(path, EMPTY_DIRECTORIES_ONLY | RECURSIVE);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/Paths.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/Paths.java
new file mode 100644
index 0000000000..6be7ddbe12
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/Paths.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.util;
+
+import static org.eclipse.jgit.lib.FileMode.TYPE_MASK;
+import static org.eclipse.jgit.lib.FileMode.TYPE_TREE;
+
+/**
+ * Utility functions for paths inside of a Git repository.
+ *
+ * @since 4.2
+ */
+public class Paths {
+ /**
+ * Remove trailing {@code '/'} if present.
+ *
+ * @param path
+ * input path to potentially remove trailing {@code '/'} from.
+ * @return null if {@code path == null}; {@code path} after removing a
+ * trailing {@code '/'}.
+ */
+ public static String stripTrailingSeparator(String path) {
+ if (path == null || path.isEmpty()) {
+ return path;
+ }
+
+ int i = path.length();
+ if (path.charAt(path.length() - 1) != '/') {
+ return path;
+ }
+ do {
+ i--;
+ } while (path.charAt(i - 1) == '/');
+ return path.substring(0, i);
+ }
+
+ /**
+ * Compare two paths according to Git path sort ordering rules.
+ *
+ * @param aPath
+ * first path buffer. The range {@code [aPos, aEnd)} is used.
+ * @param aPos
+ * index into {@code aPath} where the first path starts.
+ * @param aEnd
+ * 1 past last index of {@code aPath}.
+ * @param aMode
+ * mode of the first file. Trees are sorted as though
+ * {@code aPath[aEnd] == '/'}, even if aEnd does not exist.
+ * @param bPath
+ * second path buffer. The range {@code [bPos, bEnd)} is used.
+ * @param bPos
+ * index into {@code bPath} where the second path starts.
+ * @param bEnd
+ * 1 past last index of {@code bPath}.
+ * @param bMode
+ * mode of the second file. Trees are sorted as though
+ * {@code bPath[bEnd] == '/'}, even if bEnd does not exist.
+ * @return &lt;0 if {@code aPath} sorts before {@code bPath};
+ * 0 if the paths are the same;
+ * &gt;0 if {@code aPath} sorts after {@code bPath}.
+ */
+ public static int compare(byte[] aPath, int aPos, int aEnd, int aMode,
+ byte[] bPath, int bPos, int bEnd, int bMode) {
+ int cmp = coreCompare(
+ aPath, aPos, aEnd, aMode,
+ bPath, bPos, bEnd, bMode);
+ if (cmp == 0) {
+ cmp = lastPathChar(aMode) - lastPathChar(bMode);
+ }
+ return cmp;
+ }
+
+ /**
+ * Compare two paths, checking for identical name.
+ * <p>
+ * Unlike {@code compare} this method returns {@code 0} when the paths have
+ * the same characters in their names, even if the mode differs. It is
+ * intended for use in validation routines detecting duplicate entries.
+ * <p>
+ * Returns {@code 0} if the names are identical and a conflict exists
+ * between {@code aPath} and {@code bPath}, as they share the same name.
+ * <p>
+ * Returns {@code <0} if all possibles occurrences of {@code aPath} sort
+ * before {@code bPath} and no conflict can happen. In a properly sorted
+ * tree there are no other occurrences of {@code aPath} and therefore there
+ * are no duplicate names.
+ * <p>
+ * Returns {@code >0} when it is possible for a duplicate occurrence of
+ * {@code aPath} to appear later, after {@code bPath}. Callers should
+ * continue to examine candidates for {@code bPath} until the method returns
+ * one of the other return values.
+ *
+ * @param aPath
+ * first path buffer. The range {@code [aPos, aEnd)} is used.
+ * @param aPos
+ * index into {@code aPath} where the first path starts.
+ * @param aEnd
+ * 1 past last index of {@code aPath}.
+ * @param bPath
+ * second path buffer. The range {@code [bPos, bEnd)} is used.
+ * @param bPos
+ * index into {@code bPath} where the second path starts.
+ * @param bEnd
+ * 1 past last index of {@code bPath}.
+ * @param bMode
+ * mode of the second file. Trees are sorted as though
+ * {@code bPath[bEnd] == '/'}, even if bEnd does not exist.
+ * @return &lt;0 if no duplicate name could exist;
+ * 0 if the paths have the same name;
+ * &gt;0 other {@code bPath} should still be checked by caller.
+ */
+ public static int compareSameName(
+ byte[] aPath, int aPos, int aEnd,
+ byte[] bPath, int bPos, int bEnd, int bMode) {
+ return coreCompare(
+ aPath, aPos, aEnd, TYPE_TREE,
+ bPath, bPos, bEnd, bMode);
+ }
+
+ private static int coreCompare(
+ byte[] aPath, int aPos, int aEnd, int aMode,
+ byte[] bPath, int bPos, int bEnd, int bMode) {
+ while (aPos < aEnd && bPos < bEnd) {
+ int cmp = (aPath[aPos++] & 0xff) - (bPath[bPos++] & 0xff);
+ if (cmp != 0) {
+ return cmp;
+ }
+ }
+ if (aPos < aEnd) {
+ return (aPath[aPos] & 0xff) - lastPathChar(bMode);
+ }
+ if (bPos < bEnd) {
+ return lastPathChar(aMode) - (bPath[bPos] & 0xff);
+ }
+ return 0;
+ }
+
+ private static int lastPathChar(int mode) {
+ if ((mode & TYPE_MASK) == TYPE_TREE) {
+ return '/';
+ }
+ return 0;
+ }
+
+ private Paths() {
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/RawParseUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/RawParseUtils.java
index 45c339fb48..f2955f7e6b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/RawParseUtils.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/RawParseUtils.java
@@ -44,6 +44,8 @@
package org.eclipse.jgit.util;
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+import static java.nio.charset.StandardCharsets.UTF_8;
import static org.eclipse.jgit.lib.ObjectChecker.author;
import static org.eclipse.jgit.lib.ObjectChecker.committer;
import static org.eclipse.jgit.lib.ObjectChecker.encoding;
@@ -60,6 +62,7 @@ import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
+import org.eclipse.jgit.annotations.Nullable;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.PersonIdent;
@@ -70,7 +73,7 @@ public final class RawParseUtils {
*
* @since 2.2
*/
- public static final Charset UTF8_CHARSET = Charset.forName("UTF-8"); //$NON-NLS-1$
+ public static final Charset UTF8_CHARSET = UTF_8;
private static final byte[] digits10;
@@ -81,8 +84,9 @@ public final class RawParseUtils {
private static final Map<String, Charset> encodingAliases;
static {
- encodingAliases = new HashMap<String, Charset>();
- encodingAliases.put("latin-1", Charset.forName("ISO-8859-1")); //$NON-NLS-1$ //$NON-NLS-2$
+ encodingAliases = new HashMap<>();
+ encodingAliases.put("latin-1", ISO_8859_1); //$NON-NLS-1$
+ encodingAliases.put("iso-latin-1", ISO_8859_1); //$NON-NLS-1$
digits10 = new byte['9' + 1];
Arrays.fill(digits10, (byte) -1);
@@ -671,35 +675,60 @@ public final class RawParseUtils {
}
/**
+ * Parse the "encoding " header as a string.
+ * <p>
+ * Locates the "encoding " header (if present) and returns its value.
+ *
+ * @param b
+ * buffer to scan.
+ * @return the encoding header as specified in the commit; null if the
+ * header was not present and should be assumed.
+ * @since 4.2
+ */
+ @Nullable
+ public static String parseEncodingName(final byte[] b) {
+ int enc = encoding(b, 0);
+ if (enc < 0) {
+ return null;
+ }
+ int lf = nextLF(b, enc);
+ return decode(UTF_8, b, enc, lf - 1);
+ }
+
+ /**
* Parse the "encoding " header into a character set reference.
* <p>
* Locates the "encoding " header (if present) by first calling
* {@link #encoding(byte[], int)} and then returns the proper character set
* to apply to this buffer to evaluate its contents as character data.
* <p>
- * If no encoding header is present, {@link Constants#CHARSET} is assumed.
+ * If no encoding header is present {@code UTF-8} is assumed.
*
* @param b
* buffer to scan.
* @return the Java character set representation. Never null.
+ * @throws IllegalCharsetNameException
+ * if the character set requested by the encoding header is
+ * malformed and unsupportable.
+ * @throws UnsupportedCharsetException
+ * if the JRE does not support the character set requested by
+ * the encoding header.
*/
public static Charset parseEncoding(final byte[] b) {
- final int enc = encoding(b, 0);
- if (enc < 0)
- return Constants.CHARSET;
- final int lf = nextLF(b, enc);
- String decoded = decode(Constants.CHARSET, b, enc, lf - 1);
+ String enc = parseEncodingName(b);
+ if (enc == null) {
+ return UTF_8;
+ }
+
+ String name = enc.trim();
try {
- return Charset.forName(decoded);
- } catch (IllegalCharsetNameException badName) {
- Charset aliased = charsetForAlias(decoded);
- if (aliased != null)
- return aliased;
- throw badName;
- } catch (UnsupportedCharsetException badName) {
- Charset aliased = charsetForAlias(decoded);
- if (aliased != null)
+ return Charset.forName(name);
+ } catch (IllegalCharsetNameException
+ | UnsupportedCharsetException badName) {
+ Charset aliased = charsetForAlias(name);
+ if (aliased != null) {
return aliased;
+ }
throw badName;
}
}
@@ -738,7 +767,15 @@ public final class RawParseUtils {
* parsed.
*/
public static PersonIdent parsePersonIdent(final byte[] raw, final int nameB) {
- final Charset cs = parseEncoding(raw);
+ Charset cs;
+ try {
+ cs = parseEncoding(raw);
+ } catch (IllegalCharsetNameException | UnsupportedCharsetException e) {
+ // Assume UTF-8 for person identities, usually this is correct.
+ // If not decode() will fall back to the ISO-8859-1 encoding.
+ cs = UTF_8;
+ }
+
final int emailB = nextLF(raw, nameB, '<');
final int emailE = nextLF(raw, emailB, '>');
if (emailB >= raw.length || raw[emailB] == '\n' ||
@@ -886,7 +923,7 @@ public final class RawParseUtils {
*/
public static String decode(final byte[] buffer, final int start,
final int end) {
- return decode(Constants.CHARSET, buffer, start, end);
+ return decode(UTF_8, buffer, start, end);
}
/**
@@ -960,23 +997,21 @@ public final class RawParseUtils {
public static String decodeNoFallback(final Charset cs,
final byte[] buffer, final int start, final int end)
throws CharacterCodingException {
- final ByteBuffer b = ByteBuffer.wrap(buffer, start, end - start);
+ ByteBuffer b = ByteBuffer.wrap(buffer, start, end - start);
b.mark();
// Try our built-in favorite. The assumption here is that
// decoding will fail if the data is not actually encoded
// using that encoder.
- //
try {
- return decode(b, Constants.CHARSET);
+ return decode(b, UTF_8);
} catch (CharacterCodingException e) {
b.reset();
}
- if (!cs.equals(Constants.CHARSET)) {
+ if (!cs.equals(UTF_8)) {
// Try the suggested encoding, it might be right since it was
// provided by the caller.
- //
try {
return decode(b, cs);
} catch (CharacterCodingException e) {
@@ -986,9 +1021,8 @@ public final class RawParseUtils {
// Try the default character set. A small group of people
// might actually use the same (or very similar) locale.
- //
- final Charset defcs = Charset.defaultCharset();
- if (!defcs.equals(cs) && !defcs.equals(Constants.CHARSET)) {
+ Charset defcs = Charset.defaultCharset();
+ if (!defcs.equals(cs) && !defcs.equals(UTF_8)) {
try {
return decode(b, defcs);
} catch (CharacterCodingException e) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/StreamCopyThread.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/StreamCopyThread.java
index 24b8b53330..8d39a22ac2 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/StreamCopyThread.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/StreamCopyThread.java
@@ -47,6 +47,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
+import java.util.concurrent.atomic.AtomicInteger;
/** Thread to copy from an input stream to an output stream. */
public class StreamCopyThread extends Thread {
@@ -58,6 +59,8 @@ public class StreamCopyThread extends Thread {
private volatile boolean done;
+ private final AtomicInteger flushCount = new AtomicInteger(0);
+
/**
* Create a thread to copy data from an input stream to an output stream.
*
@@ -82,6 +85,7 @@ public class StreamCopyThread extends Thread {
* the request.
*/
public void flush() {
+ flushCount.incrementAndGet();
interrupt();
}
@@ -109,22 +113,30 @@ public class StreamCopyThread extends Thread {
public void run() {
try {
final byte[] buf = new byte[BUFFER_SIZE];
- int interruptCounter = 0;
+ int flushCountBeforeRead = 0;
+ boolean readInterrupted = false;
for (;;) {
try {
- if (interruptCounter > 0) {
+ if (readInterrupted) {
dst.flush();
- interruptCounter--;
+ readInterrupted = false;
+ if (!flushCount.compareAndSet(flushCountBeforeRead, 0)) {
+ // There was a flush() call since last blocked read.
+ // Set interrupt status, so next blocked read will throw
+ // an InterruptedIOException and we will flush again.
+ interrupt();
+ }
}
if (done)
break;
+ flushCountBeforeRead = flushCount.get();
final int n;
try {
n = src.read(buf);
} catch (InterruptedIOException wakey) {
- interruptCounter++;
+ readInterrupted = true;
continue;
}
if (n < 0)
@@ -141,7 +153,7 @@ public class StreamCopyThread extends Thread {
// set interrupt status, which will be checked
// when we block in src.read
- if (writeInterrupted)
+ if (writeInterrupted || flushCount.get() > 0)
interrupt();
break;
}
diff --git a/tools/default.defs b/tools/default.defs
new file mode 100644
index 0000000000..3481fa1f8a
--- /dev/null
+++ b/tools/default.defs
@@ -0,0 +1,42 @@
+def java_sources(
+ name,
+ srcs,
+ visibility = ['PUBLIC']
+ ):
+ java_library(
+ name = name,
+ resources = srcs,
+ visibility = visibility,
+ )
+
+def maven_jar(
+ name,
+ group,
+ artifact,
+ version,
+ bin_sha1,
+ src_sha1,
+ visibility = ['PUBLIC']):
+ jar_name = '%s__jar' % name
+ src_name = '%s__src' % name
+
+ remote_file(
+ name = jar_name,
+ sha1 = bin_sha1,
+ url = 'mvn:%s:%s:jar:%s' % (group, artifact, version),
+ out = '%s.jar' % jar_name,
+ )
+
+ remote_file(
+ name = src_name,
+ sha1 = src_sha1,
+ url = 'mvn:%s:%s:src:%s' % (group, artifact, version),
+ out = '%s.jar' % src_name,
+ )
+
+ prebuilt_jar(
+ name = name,
+ binary_jar = ':' + jar_name,
+ source_jar = ':' + src_name,
+ visibility = visibility)
+
diff --git a/tools/git.defs b/tools/git.defs
new file mode 100644
index 0000000000..557dff2319
--- /dev/null
+++ b/tools/git.defs
@@ -0,0 +1,9 @@
+def git_version():
+ import subprocess
+ cmd = ['git', 'describe', '--always', '--match', 'v[0-9].*', '--dirty']
+ p = subprocess.Popen(cmd, stdout = subprocess.PIPE)
+ v = p.communicate()[0].strip()
+ r = p.returncode
+ if r != 0:
+ raise subprocess.CalledProcessError(r, ' '.join(cmd))
+ return v