aboutsummaryrefslogtreecommitdiffstats
path: root/org.eclipse.jgit.test/tst/org/eclipse/jgit
diff options
context:
space:
mode:
Diffstat (limited to 'org.eclipse.jgit.test/tst/org/eclipse/jgit')
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java43
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ArchiveCommandTest.java15
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/BranchCommandTest.java15
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java191
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CloneCommandTest.java18
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitAndLogCommandTest.java17
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java107
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DescribeCommandTest.java22
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/EolRepositoryTest.java2
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/FetchCommandTest.java4
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/GarbageCollectCommandTest.java17
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/GitConstructionTest.java3
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/LinkedWorktreeTest.java192
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java161
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PathCheckoutCommandTest.java8
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java9
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java7
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java298
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RenameBranchCommandTest.java24
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java53
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RevertCommandTest.java75
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/SecurityManagerMissingPermissionsTest.java126
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/SecurityManagerTest.java173
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashCreateCommandTest.java4
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashDropCommandTest.java26
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/api/blame/BlameGeneratorTest.java36
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesHandlerTest.java2
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeDirCacheIteratorTest.java9
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeTest.java6
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeWorkingTreeIteratorTest.java8
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/CGitAttributesTest.java3
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/TreeWalkAttributeTest.java2
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/merge/MergeGitAttributeTest.java79
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/blame/BlameGeneratorCacheTest.java552
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/blame/BlameRegionMergerTest.java320
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/DiffFormatterBuiltInDriverTest.java173
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/EditListTest.java6
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/EditTest.java2
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheBuilderTest.java2
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java12
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/InvalidPathCheckoutTest.java61
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/BareSuperprojectWriterTest.java45
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/ManifestParserTest.java102
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java88
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/BasicRuleTest.java2
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreNodeTest.java108
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/indexdiff/IndexDiffWithSymlinkTest.java3
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/revwalk/ObjectReachabilityTestCase.java1
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/revwalk/ReachabilityCheckerTestCase.java1
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphTest.java68
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphWriterTest.java393
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/commitgraph/GraphObjectIndexTest.java28
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/AggregatedBlockCacheStatsTest.java210
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/ClockBlockCacheTableTest.java67
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfigTest.java312
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTest.java53
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollectorTest.java319
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsInserterTest.java83
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackCompacterTest.java44
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackFileTest.java208
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackParserTest.java71
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsReaderTest.java323
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/PackExtBlockCacheTableTest.java679
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/AbbreviationTest.java1
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BasePackBitmapIndexTest.java137
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BasePackWriterTest.java (renamed from org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java)107
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BatchRefUpdateTest.java4
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileReftableStackTest.java10
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileReftableTest.java197
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileRepositoryBuilderAfterOpenConfigTest.java1
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcBasicPackingTest.java4
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcConcurrentTest.java16
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcKeepFilesTest.java160
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcNumberOfPackFilesSinceBitmapStatisticsTest.java105
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcObjectSizeIndexTest.java279
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPackRefsTest.java33
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPruneNonReferencedTest.java6
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcReflogTest.java2
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcReverseIndexTest.java116
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcSinceBitmapStatisticsTest.java190
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcTestCase.java2
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/LockFileTest.java3
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ObjectDirectoryTest.java51
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackIndexTestCase.java85
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackInserterTest.java38
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackReverseIndexComputedTest.java108
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackReverseIndexTest.java136
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackReverseIndexV1Test.java289
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackReverseIndexV1WriteReadTest.java119
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackTest.java29
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryAfterOpenConfigTest.java1
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java44
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefUpdateTest.java172
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ReflogReaderTest.java32
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ReflogWriterTest.java6
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/SnapshottingRefDirectoryTest.java1
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/StoredBitmapTest.java29
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java71
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/MultiPackIndexBuilderTest.java107
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/MultiPackIndexLoaderTest.java90
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/MultiPackIndexTest.java345
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/MultiPackIndexWriterTest.java179
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/PackIndexMergerTest.java239
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/PackIndexPeekIteratorTest.java71
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/pack/GcCommitSelectionTest.java4
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/ReftableCompactorTest.java96
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/ReftableTest.java38
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/junit/TestRepositoryTest.java14
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/BitmapIndexTest.java85
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitTemplateConfigTest.java10
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java116
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/GpgConfigTest.java10
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexDiffTest.java16
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectCheckerTest.java3
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectIdTest.java13
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/PersonIdentTest.java6
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RacyGitTests.java16
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RefDatabaseConflictingNamesTest.java5
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ReflogConfigTest.java38
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ThreadSafeProgressMonitorTest.java2
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/CherryPickTest.java6
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/GitlinkMergeTest.java5
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergeAlgorithmTest.java157
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergeAlgorithmUnionTest.java328
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java266
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/SimpleMergeTest.java5
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/patch/PatchApplierTest.java96
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/patch/PatchCcErrorTest.java5
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/revplot/AbstractPlotRendererTest.java4
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/BitmapWalkerTest.java161
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/DateRevPriorityQueueTest.java109
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/FooterLineTest.java297
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevCommitParseTest.java36
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkCarryFlagsTest.java2
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkCommitGraphTest.java467
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkFilterTest.java123
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkFollowFilterTest.java24
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkTestCase.java9
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkUtilsReachableTest.java3
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RewriteGeneratorTest.java63
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/TreeRevFilterTest.java132
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/storage/file/UserConfigFileTest.java301
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleAddTest.java7
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleUpdateTest.java218
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/symlinks/DirectoryTest.java259
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/test/resources/SampleDataRepositoryTestCase.java1
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/AtomicPushTest.java16
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/HttpAuthTest.java2
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/InMemoryPack.java88
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ObjectDirectoryPackParserTest.java227
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PackParserTest.java195
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateIdentTest.java49
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateStoreTest.java6
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ReceivePackAdvertiseRefsHookTest.java6
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RequestValidatorTestCase.java18
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/SideBandOutputStreamTest.java4
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportHttpTest.java9
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/URIishTest.java37
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackHandleDeletedPackFileTest.java146
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackReachabilityTest.java16
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackTest.java301
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/http/JDKHttpConnectionTest.java4
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorTest.java11
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/ChangedPathTreeFilterTest.java122
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/PathFilterGroupTest.java27
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/PathSuffixFilterTest.java4
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/util/BlockListTest.java2
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/util/ChangeIdUtilTest.java46
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FSTest.java44
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FileUtilsTest.java8
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitDateFormatterTest.java6
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitTimeParserBadlyFormattedTest.java62
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitTimeParserTest.java247
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/util/HookTest.java2
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/util/HttpSupportTest.java12
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RawParseUtilsTest.java56
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RawParseUtils_ParsePersonIdentTest.java50
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RefMapTest.java4
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RelativeDateFormatterTest.java8
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/util/StringUtilsTest.java38
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/util/http/HttpCookiesMatcher.java6
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/util/io/ByteBufferInputStreamTest.java152
182 files changed, 13803 insertions, 1490 deletions
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 db2d5d1404..226677229c 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
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2010, Stefan Lay <stefan.lay@sap.com>
- * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com> and others
+ * Copyright (C) 2010, 2025 Christian Halstrick <christian.halstrick@sap.com> and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -13,6 +13,8 @@ package org.eclipse.jgit.api;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.eclipse.jgit.util.FileUtils.RECURSIVE;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
@@ -607,14 +609,14 @@ public class AddCommandTest extends RepositoryTestCase {
try (Git git = new Git(db)) {
DirCache dc = git.add().addFilepattern("a.txt").call();
- dc.getEntry(0).getObjectId();
+ ObjectId oid = dc.getEntry(0).getObjectId();
try (PrintWriter writer = new PrintWriter(file, UTF_8.name())) {
writer.print("other content");
}
dc = git.add().addFilepattern("a.txt").call();
-
+ assertNotEquals(oid, dc.getEntry(0).getObjectId());
assertEquals(
"[a.txt, mode:100644, content:other content]",
indexState(CONTENT));
@@ -632,7 +634,7 @@ public class AddCommandTest extends RepositoryTestCase {
try (Git git = new Git(db)) {
DirCache dc = git.add().addFilepattern("a.txt").call();
- dc.getEntry(0).getObjectId();
+ ObjectId oid = dc.getEntry(0).getObjectId();
git.commit().setMessage("commit a.txt").call();
@@ -641,7 +643,7 @@ public class AddCommandTest extends RepositoryTestCase {
}
dc = git.add().addFilepattern("a.txt").call();
-
+ assertNotEquals(oid, dc.getEntry(0).getObjectId());
assertEquals(
"[a.txt, mode:100644, content:other content]",
indexState(CONTENT));
@@ -659,15 +661,17 @@ public class AddCommandTest extends RepositoryTestCase {
try (Git git = new Git(db)) {
DirCache dc = git.add().addFilepattern("a.txt").call();
- dc.getEntry(0).getObjectId();
+ ObjectId oid = dc.getEntry(0).getObjectId();
FileUtils.delete(file);
// is supposed to do nothing
- dc = git.add().addFilepattern("a.txt").call();
-
+ dc = git.add().addFilepattern("a.txt").setAll(false).call();
+ assertEquals(oid, dc.getEntry(0).getObjectId());
assertEquals(
"[a.txt, mode:100644, content:content]",
indexState(CONTENT));
+ git.add().addFilepattern("a.txt").call();
+ assertEquals("", indexState(CONTENT));
}
}
@@ -684,15 +688,17 @@ public class AddCommandTest extends RepositoryTestCase {
git.commit().setMessage("commit a.txt").call();
- dc.getEntry(0).getObjectId();
+ ObjectId oid = dc.getEntry(0).getObjectId();
FileUtils.delete(file);
// is supposed to do nothing
- dc = git.add().addFilepattern("a.txt").call();
-
+ dc = git.add().addFilepattern("a.txt").setAll(false).call();
+ assertEquals(oid, dc.getEntry(0).getObjectId());
assertEquals(
"[a.txt, mode:100644, content:content]",
indexState(CONTENT));
+ git.add().addFilepattern("a.txt").call();
+ assertEquals("", indexState(CONTENT));
}
}
@@ -878,7 +884,7 @@ public class AddCommandTest extends RepositoryTestCase {
}
}
// Help null pointer analysis.
- assert lastFile != null;
+ assertNotNull(lastFile);
// Wait a bit. If entries are "racily clean", we'll recompute
// hashes from the disk files, and then the second add is also slow.
// We want to test the normal case.
@@ -962,7 +968,7 @@ public class AddCommandTest extends RepositoryTestCase {
// file sub/b.txt is deleted
FileUtils.delete(file2);
- git.add().addFilepattern("sub").call();
+ git.add().addFilepattern("sub").setAll(false).call();
// change in sub/a.txt is staged
// deletion of sub/b.txt is not staged
// sub/c.txt is staged
@@ -971,6 +977,12 @@ public class AddCommandTest extends RepositoryTestCase {
"[sub/b.txt, mode:100644, content:content b]" +
"[sub/c.txt, mode:100644, content:content c]",
indexState(CONTENT));
+ git.add().addFilepattern("sub").call();
+ // deletion of sub/b.txt is staged
+ assertEquals(
+ "[sub/a.txt, mode:100644, content:modified content]"
+ + "[sub/c.txt, mode:100644, content:content c]",
+ indexState(CONTENT));
}
}
@@ -1259,7 +1271,7 @@ public class AddCommandTest extends RepositoryTestCase {
"[git-link-dir, mode:160000]",
indexState(0));
Set<String> untrackedFiles = git.status().call().getUntracked();
- assert (untrackedFiles.isEmpty());
+ assertTrue(untrackedFiles.isEmpty());
}
}
@@ -1274,7 +1286,8 @@ public class AddCommandTest extends RepositoryTestCase {
ConfigConstants.CONFIG_KEY_DIRNOGITLINKS, true);
config.save();
- assert (db.getConfig().get(WorkingTreeOptions.KEY).isDirNoGitLinks());
+ assertTrue(
+ db.getConfig().get(WorkingTreeOptions.KEY).isDirNoGitLinks());
try (Git git = new Git(db)) {
git.add().addFilepattern("nested-repo").call();
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ArchiveCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ArchiveCommandTest.java
index f2cceac4b3..9c2b16a0ae 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ArchiveCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ArchiveCommandTest.java
@@ -254,24 +254,23 @@ public class ArchiveCommandTest extends RepositoryTestCase {
}
}
- @SuppressWarnings({ "serial", "boxing" })
+ @SuppressWarnings({ "boxing" })
private void archiveHeadAllFilesWithCompression(String fmt) throws Exception {
try (Git git = new Git(db)) {
createLargeTestContent(git);
File archive = new File(getTemporaryDirectory(),
"archive." + format);
- archive(git, archive, fmt, new HashMap<String, Object>() {{
- put("compression-level", 1);
- }});
+ archive(git, archive, fmt, Map.of("compression-level", 1));
int sizeCompression1 = getNumBytes(archive);
- archive(git, archive, fmt, new HashMap<String, Object>() {{
- put("compression-level", 9);
- }});
+ archive(git, archive, fmt, Map.of("compression-level", 9));
int sizeCompression9 = getNumBytes(archive);
- assertTrue(sizeCompression1 > sizeCompression9);
+ assertTrue(
+ "Expected sizeCompression1 = " + sizeCompression1
+ + " > sizeCompression9 = " + sizeCompression9,
+ sizeCompression1 > sizeCompression9);
}
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/BranchCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/BranchCommandTest.java
index 87be813c85..7c1cbc37d6 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/BranchCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/BranchCommandTest.java
@@ -12,6 +12,7 @@ package org.eclipse.jgit.api;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
import java.util.List;
@@ -160,6 +161,20 @@ public class BranchCommandTest extends RepositoryTestCase {
- allBefore);
}
+ @Test
+ public void testExistingNameInBothBranchesAndTags() throws Exception {
+ git.branchCreate().setName("test").call();
+ git.tag().setName("test").call();
+
+ // existing name not allowed w/o force
+ assertThrows("Create branch with existing ref name should fail",
+ RefAlreadyExistsException.class,
+ () -> git.branchCreate().setName("test").call());
+
+ // existing name allowed with force option
+ git.branchCreate().setName("test").setForce(true).call();
+ }
+
@Test(expected = InvalidRefNameException.class)
public void testInvalidBranchHEAD() throws Exception {
git.branchCreate().setName("HEAD").call();
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java
index 301d6be662..3f5c5da55a 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CherryPickCommandTest.java
@@ -9,6 +9,8 @@
*/
package org.eclipse.jgit.api;
+import static org.eclipse.jgit.api.CherryPickCommitMessageProvider.ORIGINAL;
+import static org.eclipse.jgit.api.CherryPickCommitMessageProvider.ORIGINAL_WITH_REFERENCE;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@@ -32,6 +34,7 @@ import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.RefDatabase;
import org.eclipse.jgit.lib.ReflogReader;
import org.eclipse.jgit.lib.RepositoryState;
import org.eclipse.jgit.merge.ContentMergeStrategy;
@@ -527,10 +530,11 @@ public class CherryPickCommandTest extends RepositoryTestCase {
assertEquals(RepositoryState.SAFE, db.getRepositoryState());
if (reason == null) {
- ReflogReader reader = db.getReflogReader(Constants.HEAD);
+ RefDatabase refDb = db.getRefDatabase();
+ ReflogReader reader = refDb.getReflogReader(Constants.HEAD);
assertTrue(reader.getLastEntry().getComment()
.startsWith("cherry-pick: "));
- reader = db.getReflogReader(db.getBranch());
+ reader = refDb.getReflogReader(db.getFullBranch());
assertTrue(reader.getLastEntry().getComment()
.startsWith("cherry-pick: "));
}
@@ -590,4 +594,187 @@ public class CherryPickCommandTest extends RepositoryTestCase {
checkFile(new File(db.getWorkTree(), "file"), "a\n2\n3\n");
}
}
+
+ private void doCherryPickWithCustomProviderBaseTest(Git git,
+ CherryPickCommitMessageProvider commitMessageProvider)
+ throws Exception {
+ writeTrashFile("fileA", "line 1\nline 2\nline 3\n");
+ git.add().addFilepattern("fileA").call();
+ RevCommit commitFirst = git.commit().setMessage("create fileA").call();
+
+ writeTrashFile("fileB", "content from file B\n");
+ git.add().addFilepattern("fileB").call();
+ RevCommit commitCreateFileB = git.commit()
+ .setMessage("create fileB\n\nsome commit details").call();
+
+ writeTrashFile("fileA", "line 1\nline 2\nline 3\nline 4\n");
+ git.add().addFilepattern("fileA").call();
+ RevCommit commitEditFileA1 = git.commit().setMessage("patch fileA 1")
+ .call();
+
+ writeTrashFile("fileA", "line 1\nline 2\nline 3\nline 4\nline 5\n");
+ git.add().addFilepattern("fileA").call();
+ RevCommit commitEditFileA2 = git.commit().setMessage("patch fileA 2")
+ .call();
+
+ git.branchCreate().setName("side").setStartPoint(commitFirst).call();
+ checkoutBranch("refs/heads/side");
+
+ CherryPickResult pickResult = git.cherryPick()
+ .setCherryPickCommitMessageProvider(commitMessageProvider)
+ .include(commitCreateFileB).include(commitEditFileA1)
+ .include(commitEditFileA2).call();
+
+ assertEquals(CherryPickStatus.OK, pickResult.getStatus());
+
+ assertTrue(new File(db.getWorkTree(), "fileA").exists());
+ assertTrue(new File(db.getWorkTree(), "fileB").exists());
+
+ checkFile(new File(db.getWorkTree(), "fileA"),
+ "line 1\nline 2\nline 3\nline 4\nline 5\n");
+ checkFile(new File(db.getWorkTree(), "fileB"), "content from file B\n");
+ }
+
+ @Test
+ public void testCherryPickWithCustomCommitMessageProvider()
+ throws Exception {
+ try (Git git = new Git(db)) {
+ @SuppressWarnings("boxing")
+ CherryPickCommitMessageProvider messageProvider = srcCommit -> {
+ String message = srcCommit.getFullMessage();
+ return String.format("%s (message length: %d)", message,
+ message.length());
+ };
+ doCherryPickWithCustomProviderBaseTest(git, messageProvider);
+
+ Iterator<RevCommit> history = git.log().call().iterator();
+ assertEquals("patch fileA 2 (message length: 13)",
+ history.next().getFullMessage());
+ assertEquals("patch fileA 1 (message length: 13)",
+ history.next().getFullMessage());
+ assertEquals(
+ "create fileB\n\nsome commit details (message length: 33)",
+ history.next().getFullMessage());
+ assertEquals("create fileA", history.next().getFullMessage());
+ assertFalse(history.hasNext());
+ }
+ }
+
+ @Test
+ public void testCherryPickWithCustomCommitMessageProvider_ORIGINAL()
+ throws Exception {
+ try (Git git = new Git(db)) {
+ doCherryPickWithCustomProviderBaseTest(git, ORIGINAL);
+
+ Iterator<RevCommit> history = git.log().call().iterator();
+ assertEquals("patch fileA 2", history.next().getFullMessage());
+ assertEquals("patch fileA 1", history.next().getFullMessage());
+ assertEquals("create fileB\n\nsome commit details",
+ history.next().getFullMessage());
+ assertEquals("create fileA", history.next().getFullMessage());
+ assertFalse(history.hasNext());
+ }
+ }
+
+ @Test
+ public void testCherryPickWithCustomCommitMessageProvider_ORIGINAL_WITH_REFERENCE()
+ throws Exception {
+ try (Git git = new Git(db)) {
+ doCherryPickWithCustomProviderBaseTest(git,
+ ORIGINAL_WITH_REFERENCE);
+
+ Iterator<RevCommit> history = git.log().call().iterator();
+ assertEquals("patch fileA 2\n\n(cherry picked from commit 1ac121e90b0fb6fb18bbb4307e3e9731ceeba9e1)", history.next().getFullMessage());
+ assertEquals("patch fileA 1\n\n(cherry picked from commit 71475239df59076e18564fa360e3a74280926c2a)", history.next().getFullMessage());
+ assertEquals("create fileB\n\nsome commit details\n\n(cherry picked from commit 29b4501297ccf8de9de9f451e7beb384b51f5378)",
+ history.next().getFullMessage());
+ assertEquals("create fileA", history.next().getFullMessage());
+ assertFalse(history.hasNext());
+ }
+ }
+
+ @Test
+ public void testCherryPickWithCustomCommitMessageProvider_ORIGINAL_WITH_REFERENCE_DonNotAddNewLineAfterFooter()
+ throws Exception {
+ try (Git git = new Git(db)) {
+ CherryPickCommitMessageProvider commitMessageProvider = CherryPickCommitMessageProvider.ORIGINAL_WITH_REFERENCE;
+
+ RevCommit commit1 = addFileAndCommit(git, "file1", "content 1",
+ "commit1: no footer line");
+ RevCommit commit2 = addFileAndCommit(git, "file2", "content 2",
+ "commit2: simple single footer line"
+ + "\n\nSigned-off-by: Alice <alice@example.com>");
+ RevCommit commit3 = addFileAndCommit(git, "file3", "content 3",
+ "commit3: multiple footer lines\n\n"
+ + "Signed-off-by: Alice <alice@example.com>\n"
+ + "Signed-off-by: Bob <bob@example.com>");
+ RevCommit commit4 = addFileAndCommit(git, "file4", "content 4",
+ "commit4: extra commit text before footer line\n\n"
+ + "Commit message details\n\n"
+ + "Signed-off-by: Alice <alice@example.com>\n"
+ + "Signed-off-by: Bob <bob@example.com>");
+ RevCommit commit5 = addFileAndCommit(git, "file5", "content 5",
+ "commit5: extra commit text after footer line\n\n"
+ + "Signed-off-by: Alice <alice@example.com>\n"
+ + "Signed-off-by: Bob <bob@example.com>\n\n"
+ + "some extra description after footer");
+
+ git.branchCreate().setName("side").setStartPoint(commit1).call();
+ checkoutBranch("refs/heads/side");
+
+ CherryPickResult pickResult = git.cherryPick()
+ .setCherryPickCommitMessageProvider(commitMessageProvider)
+ .include(commit2).include(commit3).include(commit4)
+ .include(commit5).call();
+
+ assertEquals(CherryPickStatus.OK, pickResult.getStatus());
+
+ assertTrue(new File(db.getWorkTree(), "file1").exists());
+ assertTrue(new File(db.getWorkTree(), "file2").exists());
+ assertTrue(new File(db.getWorkTree(), "file3").exists());
+ assertTrue(new File(db.getWorkTree(), "file4").exists());
+ assertTrue(new File(db.getWorkTree(), "file5").exists());
+
+ Iterator<RevCommit> history = git.log().call().iterator();
+ RevCommit cpCommit1 = history.next();
+ RevCommit cpCommit2 = history.next();
+ RevCommit cpCommit3 = history.next();
+ RevCommit cpCommit4 = history.next();
+ RevCommit cpCommitInit = history.next();
+ assertFalse(history.hasNext());
+
+ assertEquals("commit5: extra commit text after footer line\n\n"
+ + "Signed-off-by: Alice <alice@example.com>\n"
+ + "Signed-off-by: Bob <bob@example.com>\n\n"
+ + "some extra description after footer\n\n"
+ + "(cherry picked from commit c3c9959207dc7ae7c83da5d36dc14ef2ca42d572)",
+ cpCommit1.getFullMessage());
+ assertEquals("commit4: extra commit text before footer line\n\n"
+ + "Commit message details\n\n"
+ + "Signed-off-by: Alice <alice@example.com>\n"
+ + "Signed-off-by: Bob <bob@example.com>\n"
+ + "(cherry picked from commit af3e8106c12cb946a37b403ddb2dd6c11a883698)",
+ cpCommit2.getFullMessage());
+ assertEquals("commit3: multiple footer lines\n\n"
+ + "Signed-off-by: Alice <alice@example.com>\n"
+ + "Signed-off-by: Bob <bob@example.com>\n"
+ + "(cherry picked from commit 6d60f1a70a11a32dff4402c157c4ac328c32ce6c)",
+ cpCommit3.getFullMessage());
+ assertEquals("commit2: simple single footer line\n\n"
+ + "Signed-off-by: Alice <alice@example.com>\n"
+ + "(cherry picked from commit 92bf0ec458814ecc73da8e050e60547d2ea6cce5)",
+ cpCommit4.getFullMessage());
+
+ assertEquals("commit1: no footer line",
+ cpCommitInit.getFullMessage());
+ }
+ }
+
+ private RevCommit addFileAndCommit(Git git, String fileName,
+ String fileText, String commitMessage)
+ throws IOException, GitAPIException {
+ writeTrashFile(fileName, fileText);
+ git.add().addFilepattern(fileName).call();
+ return git.commit().setMessage(commitMessage).call();
+ }
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CloneCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CloneCommandTest.java
index 63ab8094ae..661878fa07 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CloneCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CloneCommandTest.java
@@ -182,7 +182,8 @@ public class CloneCommandTest extends RepositoryTestCase {
private static boolean hasRefLog(Repository repo, Ref ref) {
try {
- return repo.getReflogReader(ref.getName()).getLastEntry() != null;
+ return repo.getRefDatabase().getReflogReader(ref)
+ .getLastEntry() != null;
} catch (IOException ioe) {
throw new IllegalStateException(ioe);
}
@@ -647,7 +648,8 @@ public class CloneCommandTest extends RepositoryTestCase {
new File(git.getRepository().getWorkTree(), walk.getPath()),
subRepo.getWorkTree());
assertEquals(new File(new File(git.getRepository().getDirectory(),
- "modules"), walk.getPath()), subRepo.getDirectory());
+ "modules"), walk.getPath()).getCanonicalPath(),
+ subRepo.getDirectory().getCanonicalPath());
}
File directory = createTempDirectory("testCloneRepositoryWithSubmodules");
@@ -681,8 +683,8 @@ public class CloneCommandTest extends RepositoryTestCase {
walk.getPath()), clonedSub1.getWorkTree());
assertEquals(
new File(new File(git2.getRepository().getDirectory(),
- "modules"), walk.getPath()),
- clonedSub1.getDirectory());
+ "modules"), walk.getPath()).getCanonicalPath(),
+ clonedSub1.getDirectory().getCanonicalPath());
}
}
@@ -770,8 +772,8 @@ public class CloneCommandTest extends RepositoryTestCase {
walk.getPath()), clonedSub1.getWorkTree());
assertEquals(
new File(new File(git2.getRepository().getDirectory(),
- "modules"), walk.getPath()),
- clonedSub1.getDirectory());
+ "modules"), walk.getPath()).getCanonicalPath(),
+ clonedSub1.getDirectory().getCanonicalPath());
status = new SubmoduleStatusCommand(clonedSub1);
statuses = status.call();
}
@@ -795,7 +797,7 @@ public class CloneCommandTest extends RepositoryTestCase {
assertNull(git2.getRepository().getConfig().getEnum(
BranchRebaseMode.values(),
ConfigConstants.CONFIG_BRANCH_SECTION, "test",
- ConfigConstants.CONFIG_KEY_REBASE, null));
+ ConfigConstants.CONFIG_KEY_REBASE));
StoredConfig userConfig = SystemReader.getInstance()
.getUserConfig();
@@ -811,7 +813,6 @@ public class CloneCommandTest extends RepositoryTestCase {
addRepoToClose(git2.getRepository());
assertEquals(BranchRebaseMode.REBASE,
git2.getRepository().getConfig().getEnum(
- BranchRebaseMode.values(),
ConfigConstants.CONFIG_BRANCH_SECTION, "test",
ConfigConstants.CONFIG_KEY_REBASE,
BranchRebaseMode.NONE));
@@ -828,7 +829,6 @@ public class CloneCommandTest extends RepositoryTestCase {
addRepoToClose(git2.getRepository());
assertEquals(BranchRebaseMode.REBASE,
git2.getRepository().getConfig().getEnum(
- BranchRebaseMode.values(),
ConfigConstants.CONFIG_BRANCH_SECTION, "test",
ConfigConstants.CONFIG_KEY_REBASE,
BranchRebaseMode.NONE));
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitAndLogCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitAndLogCommandTest.java
index b7abba4209..4e5f44e5a6 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitAndLogCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitAndLogCommandTest.java
@@ -26,6 +26,7 @@ import org.eclipse.jgit.junit.RepositoryTestCase;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.RefDatabase;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.ReflogReader;
import org.eclipse.jgit.revwalk.RevCommit;
@@ -69,10 +70,11 @@ public class CommitAndLogCommandTest extends RepositoryTestCase {
l--;
}
assertEquals(l, -1);
- ReflogReader reader = db.getReflogReader(Constants.HEAD);
+ RefDatabase refDb = db.getRefDatabase();
+ ReflogReader reader = refDb.getReflogReader(Constants.HEAD);
assertTrue(
reader.getLastEntry().getComment().startsWith("commit:"));
- reader = db.getReflogReader(db.getBranch());
+ reader = refDb.getReflogReader(db.getFullBranch());
assertTrue(
reader.getLastEntry().getComment().startsWith("commit:"));
}
@@ -248,10 +250,11 @@ public class CommitAndLogCommandTest extends RepositoryTestCase {
c++;
}
assertEquals(1, c);
- ReflogReader reader = db.getReflogReader(Constants.HEAD);
+ RefDatabase refDb = db.getRefDatabase();
+ ReflogReader reader = refDb.getReflogReader(Constants.HEAD);
assertTrue(reader.getLastEntry().getComment()
.startsWith("commit (amend):"));
- reader = db.getReflogReader(db.getBranch());
+ reader = refDb.getReflogReader(db.getFullBranch());
assertTrue(reader.getLastEntry().getComment()
.startsWith("commit (amend):"));
}
@@ -284,11 +287,10 @@ public class CommitAndLogCommandTest extends RepositoryTestCase {
// template)
chars = commit.getFullMessage().getBytes(UTF_8);
int lineStart = 0;
- int lineEnd = 0;
for (int i = 0; i < 4; i++) {
lineStart = RawParseUtils.nextLF(chars, lineStart);
}
- lineEnd = RawParseUtils.nextLF(chars, lineStart);
+ int lineEnd = RawParseUtils.nextLF(chars, lineStart);
String line = RawParseUtils.decode(chars, lineStart, lineEnd);
@@ -303,13 +305,12 @@ public class CommitAndLogCommandTest extends RepositoryTestCase {
// we should find the untouched template
chars = commit.getFullMessage().getBytes(UTF_8);
lineStart = 0;
- lineEnd = 0;
for (int i = 0; i < 4; i++) {
lineStart = RawParseUtils.nextLF(chars, lineStart);
}
lineEnd = RawParseUtils.nextLF(chars, lineStart);
- line = RawParseUtils.decode(chars, lineStart, lineEnd);
+ RawParseUtils.decode(chars, lineStart, lineEnd);
assertTrue(commit.getFullMessage()
.contains("Change-Id: I" + ObjectId.zeroId().getName()));
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 35de73e204..21cfcc4e34 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
@@ -19,14 +19,15 @@ import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
import java.io.File;
-import java.util.Date;
+import java.time.Instant;
+import java.time.ZoneOffset;
import java.util.List;
-import java.util.TimeZone;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jgit.api.CherryPickResult.CherryPickStatus;
import org.eclipse.jgit.api.errors.CanceledException;
import org.eclipse.jgit.api.errors.EmptyCommitException;
+import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException;
import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.dircache.DirCache;
@@ -34,19 +35,23 @@ import org.eclipse.jgit.dircache.DirCacheBuilder;
import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.junit.RepositoryTestCase;
import org.eclipse.jgit.junit.time.TimeUtil;
-import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.CommitConfig.CleanupMode;
import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
-import org.eclipse.jgit.lib.GpgSigner;
+import org.eclipse.jgit.lib.GpgConfig;
+import org.eclipse.jgit.lib.GpgConfig.GpgFormat;
+import org.eclipse.jgit.lib.GpgSignature;
+import org.eclipse.jgit.lib.ObjectBuilder;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.RefUpdate.Result;
import org.eclipse.jgit.lib.ReflogEntry;
import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.Signer;
+import org.eclipse.jgit.lib.Signers;
import org.eclipse.jgit.lib.StoredConfig;
-import org.eclipse.jgit.lib.CommitConfig.CleanupMode;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.eclipse.jgit.submodule.SubmoduleWalk;
@@ -430,10 +435,12 @@ public class CommitCommandTest extends RepositoryTestCase {
assertEquals(1, squashedCommit.getParentCount());
assertNull(db.readSquashCommitMsg());
- assertEquals("commit: Squashed commit of the following:", db
- .getReflogReader(Constants.HEAD).getLastEntry().getComment());
- assertEquals("commit: Squashed commit of the following:", db
- .getReflogReader(db.getBranch()).getLastEntry().getComment());
+ assertEquals("commit: Squashed commit of the following:",
+ db.getRefDatabase().getReflogReader(Constants.HEAD)
+ .getLastEntry().getComment());
+ assertEquals("commit: Squashed commit of the following:",
+ db.getRefDatabase().getReflogReader(db.getFullBranch())
+ .getLastEntry().getComment());
}
}
@@ -450,12 +457,15 @@ public class CommitCommandTest extends RepositoryTestCase {
git.commit().setMessage("c3").setAll(true)
.setReflogComment("testRl").call();
- db.getReflogReader(Constants.HEAD).getReverseEntries();
+ db.getRefDatabase().getReflogReader(Constants.HEAD)
+ .getReverseEntries();
assertEquals("testRl;commit (initial): c1;", reflogComments(
- db.getReflogReader(Constants.HEAD).getReverseEntries()));
+ db.getRefDatabase().getReflogReader(Constants.HEAD)
+ .getReverseEntries()));
assertEquals("testRl;commit (initial): c1;", reflogComments(
- db.getReflogReader(db.getBranch()).getReverseEntries()));
+ db.getRefDatabase().getReflogReader(db.getFullBranch())
+ .getReverseEntries()));
}
}
@@ -481,11 +491,11 @@ public class CommitCommandTest extends RepositoryTestCase {
writeTrashFile("file1", "file1");
git.add().addFilepattern("file1").call();
- final String authorName = "First Author";
- final String authorEmail = "author@example.org";
- final Date authorDate = new Date(1349621117000L);
+ String authorName = "First Author";
+ String authorEmail = "author@example.org";
+ Instant authorDate = Instant.ofEpochSecond(1349621117L);
PersonIdent firstAuthor = new PersonIdent(authorName, authorEmail,
- authorDate, TimeZone.getTimeZone("UTC"));
+ authorDate, ZoneOffset.UTC);
git.commit().setMessage("initial commit").setAuthor(firstAuthor).call();
RevCommit amended = git.commit().setAmend(true)
@@ -494,7 +504,8 @@ public class CommitCommandTest extends RepositoryTestCase {
PersonIdent amendedAuthor = amended.getAuthorIdent();
assertEquals(authorName, amendedAuthor.getName());
assertEquals(authorEmail, amendedAuthor.getEmailAddress());
- assertEquals(authorDate.getTime(), amendedAuthor.getWhen().getTime());
+ assertEquals(authorDate.getEpochSecond(),
+ amendedAuthor.getWhenAsInstant().getEpochSecond());
}
}
@@ -839,21 +850,39 @@ public class CommitCommandTest extends RepositoryTestCase {
String[] signingKey = new String[1];
PersonIdent[] signingCommitters = new PersonIdent[1];
AtomicInteger callCount = new AtomicInteger();
- GpgSigner.setDefault(new GpgSigner() {
+ // Since GpgFormat defaults to OpenPGP just set a new signer for
+ // that.
+ Signers.set(GpgFormat.OPENPGP, new Signer() {
+
@Override
- public void sign(CommitBuilder commit, String gpgSigningKey,
- PersonIdent signingCommitter, CredentialsProvider credentialsProvider) {
- signingKey[0] = gpgSigningKey;
+ public void signObject(Repository repo, GpgConfig config,
+ ObjectBuilder builder, PersonIdent signingCommitter,
+ String signingKeySpec,
+ CredentialsProvider credentialsProvider)
+ throws CanceledException,
+ UnsupportedSigningFormatException {
+ signingKey[0] = signingKeySpec;
signingCommitters[0] = signingCommitter;
callCount.incrementAndGet();
}
@Override
- public boolean canLocateSigningKey(String gpgSigningKey,
- PersonIdent signingCommitter,
+ public GpgSignature sign(Repository repo, GpgConfig config,
+ byte[] data, PersonIdent signingCommitter,
+ String signingKeySpec,
+ CredentialsProvider credentialsProvider)
+ throws CanceledException,
+ UnsupportedSigningFormatException {
+ throw new CanceledException("Unexpected call");
+ }
+
+ @Override
+ public boolean canLocateSigningKey(Repository repo,
+ GpgConfig config, PersonIdent signingCommitter,
+ String signingKeySpec,
CredentialsProvider credentialsProvider)
throws CanceledException {
- return false;
+ throw new CanceledException("Unexpected call");
}
});
@@ -904,19 +933,37 @@ public class CommitCommandTest extends RepositoryTestCase {
git.add().addFilepattern("file1").call();
AtomicInteger callCount = new AtomicInteger();
- GpgSigner.setDefault(new GpgSigner() {
+ // Since GpgFormat defaults to OpenPGP just set a new signer for
+ // that.
+ Signers.set(GpgFormat.OPENPGP, new Signer() {
+
@Override
- public void sign(CommitBuilder commit, String gpgSigningKey,
- PersonIdent signingCommitter, CredentialsProvider credentialsProvider) {
+ public void signObject(Repository repo, GpgConfig config,
+ ObjectBuilder builder, PersonIdent signingCommitter,
+ String signingKeySpec,
+ CredentialsProvider credentialsProvider)
+ throws CanceledException,
+ UnsupportedSigningFormatException {
callCount.incrementAndGet();
}
@Override
- public boolean canLocateSigningKey(String gpgSigningKey,
- PersonIdent signingCommitter,
+ public GpgSignature sign(Repository repo, GpgConfig config,
+ byte[] data, PersonIdent signingCommitter,
+ String signingKeySpec,
+ CredentialsProvider credentialsProvider)
+ throws CanceledException,
+ UnsupportedSigningFormatException {
+ throw new CanceledException("Unexpected call");
+ }
+
+ @Override
+ public boolean canLocateSigningKey(Repository repo,
+ GpgConfig config, PersonIdent signingCommitter,
+ String signingKeySpec,
CredentialsProvider credentialsProvider)
throws CanceledException {
- return false;
+ throw new CanceledException("Unexpected call");
}
});
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DescribeCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DescribeCommandTest.java
index ab87fa9662..060e6d3e84 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DescribeCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DescribeCommandTest.java
@@ -12,6 +12,7 @@ package org.eclipse.jgit.api;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.eclipse.jgit.lib.Constants.OBJECT_ID_ABBREV_STRING_LENGTH;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@@ -87,6 +88,9 @@ public class DescribeCommandTest extends RepositoryTestCase {
assertEquals("alice-t1", describe(c2, "alice*"));
assertEquals("alice-t1", describe(c2, "a*", "b*", "c*"));
+ assertNotEquals("alice-t1", describeExcluding(c2, "alice*"));
+ assertNotEquals("alice-t1", describeCommand(c2).setMatch("*").setExclude("alice*").call());
+
assertEquals("bob-t2", describe(c3));
assertEquals("bob-t2-0-g44579eb", describe(c3, true, false));
assertEquals("alice-t1-1-g44579eb", describe(c3, "alice*"));
@@ -95,6 +99,15 @@ public class DescribeCommandTest extends RepositoryTestCase {
assertEquals("bob-t2", describe(c3, "?ob*"));
assertEquals("bob-t2", describe(c3, "a*", "b*", "c*"));
+ assertNotEquals("alice-t1-1-g44579eb", describeExcluding(c3, "alice*"));
+ assertNotEquals("alice-t1-1-g44579eb", describeCommand(c3).setMatch("*").setExclude("alice*").call());
+ assertNotEquals("alice-t1-1-g44579eb", describeExcluding(c3, "a??c?-t*"));
+ assertNotEquals("alice-t1-1-g44579eb", describeCommand(c3).setMatch("bob*").setExclude("a??c?-t*").call());
+ assertNotEquals("bob-t2", describeExcluding(c3, "bob*"));
+ assertNotEquals("bob-t2", describeCommand(c3).setMatch("alice*").setExclude("bob*"));
+ assertNotEquals("bob-t2", describeExcluding(c3, "?ob*"));
+ assertNotEquals("bob-t2", describeCommand(c3).setMatch("a??c?-t*").setExclude("?ob*"));
+
// the value verified with git-describe(1)
assertEquals("bob-t2-1-g3e563c5", describe(c4));
assertEquals("bob-t2-1-g3e563c5", describe(c4, true, false));
@@ -518,6 +531,15 @@ public class DescribeCommandTest extends RepositoryTestCase {
.setMatch(patterns).call();
}
+ private String describeExcluding(ObjectId c1, String... patterns) throws Exception {
+ return git.describe().setTarget(c1).setTags(describeUseAllTags)
+ .setExclude(patterns).call();
+ }
+
+ private DescribeCommand describeCommand(ObjectId c1) throws Exception {
+ return git.describe().setTarget(c1).setTags(describeUseAllTags);
+ }
+
private static void assertNameStartsWith(ObjectId c4, String prefix) {
assertTrue(c4.name(), c4.name().startsWith(prefix));
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/EolRepositoryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/EolRepositoryTest.java
index b937b1f6a9..4c971ffb6b 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/EolRepositoryTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/EolRepositoryTest.java
@@ -559,7 +559,7 @@ public class EolRepositoryTest extends RepositoryTestCase {
}
if (infoAttributesContent != null) {
- File f = new File(db.getDirectory(), Constants.INFO_ATTRIBUTES);
+ File f = new File(db.getCommonDirectory(), Constants.INFO_ATTRIBUTES);
write(f, infoAttributesContent);
}
config.save();
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/FetchCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/FetchCommandTest.java
index 3ec454cfc3..3731347f11 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/FetchCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/FetchCommandTest.java
@@ -92,8 +92,8 @@ public class FetchCommandTest extends RepositoryTestCase {
assertTrue(remoteRef.getName().startsWith(Constants.R_REMOTES));
assertEquals(defaultBranchSha1, remoteRef.getObjectId());
- assertNotNull(git.getRepository().getReflogReader(remoteRef.getName())
- .getLastEntry());
+ assertNotNull(git.getRepository().getRefDatabase()
+ .getReflogReader(remoteRef.getName()).getLastEntry());
}
@Test
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/GarbageCollectCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/GarbageCollectCommandTest.java
index f98db3497b..6090d5efbe 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/GarbageCollectCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/GarbageCollectCommandTest.java
@@ -11,12 +11,11 @@ package org.eclipse.jgit.api;
import static org.junit.Assert.assertTrue;
-import java.util.Date;
+import java.time.Instant;
import java.util.Properties;
import org.eclipse.jgit.junit.RepositoryTestCase;
-import org.eclipse.jgit.util.GitDateParser;
-import org.eclipse.jgit.util.SystemReader;
+import org.eclipse.jgit.util.GitTimeParser;
import org.junit.Before;
import org.junit.Test;
@@ -36,9 +35,8 @@ public class GarbageCollectCommandTest extends RepositoryTestCase {
@Test
public void testGConeCommit() throws Exception {
- Date expire = GitDateParser.parse("now", null, SystemReader
- .getInstance().getLocale());
- Properties res = git.gc().setExpire(expire).call();
+ Instant expireNow = GitTimeParser.parseInstant("now");
+ Properties res = git.gc().setExpire(expireNow).call();
assertTrue(res.size() == 8);
}
@@ -52,11 +50,8 @@ public class GarbageCollectCommandTest extends RepositoryTestCase {
writeTrashFile("b.txt", "a couple of words for gc to pack more 2");
writeTrashFile("c.txt", "a couple of words for gc to pack more 3");
git.commit().setAll(true).setMessage("commit3").call();
- Properties res = git
- .gc()
- .setExpire(
- GitDateParser.parse("now", null, SystemReader
- .getInstance().getLocale())).call();
+ Instant expireNow = GitTimeParser.parseInstant("now");
+ Properties res = git.gc().setExpire(expireNow).call();
assertTrue(res.size() == 8);
}
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/GitConstructionTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/GitConstructionTest.java
index 76934343da..e847e72415 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/GitConstructionTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/GitConstructionTest.java
@@ -14,6 +14,7 @@ import static org.junit.Assert.fail;
import java.io.File;
import java.io.IOException;
+import java.time.Instant;
import org.eclipse.jgit.api.ListBranchCommand.ListMode;
import org.eclipse.jgit.api.errors.GitAPIException;
@@ -100,7 +101,7 @@ public class GitConstructionTest extends RepositoryTestCase {
GitAPIException {
File workTree = db.getWorkTree();
Git git = Git.open(workTree);
- git.gc().setExpire(null).call();
+ git.gc().setExpire((Instant) null).call();
git.checkout().setName(git.getRepository().resolve("HEAD^").getName())
.call();
try {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/LinkedWorktreeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/LinkedWorktreeTest.java
new file mode 100644
index 0000000000..3b60e1b5c0
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/LinkedWorktreeTest.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2024, Broadcom and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.api;
+
+import static org.eclipse.jgit.lib.Constants.HEAD;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import java.util.Iterator;
+
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.junit.JGitTestUtil;
+import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ReflogEntry;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.FS.ExecutionResult;
+import org.eclipse.jgit.util.RawParseUtils;
+import org.eclipse.jgit.util.TemporaryBuffer;
+import org.junit.Test;
+
+public class LinkedWorktreeTest extends RepositoryTestCase {
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ try (Git git = new Git(db)) {
+ git.commit().setMessage("Initial commit").call();
+ }
+ }
+
+ @Test
+ public void testWeCanReadFromLinkedWorktreeFromBare() throws Exception {
+ FS fs = db.getFS();
+ File directory = trash.getParentFile();
+ String dbDirName = db.getWorkTree().getName();
+ cloneBare(fs, directory, dbDirName, "bare");
+ File bareDirectory = new File(directory, "bare");
+ worktreeAddExisting(fs, bareDirectory, "master");
+
+ File worktreesDir = new File(bareDirectory, "worktrees");
+ File masterWorktreesDir = new File(worktreesDir, "master");
+
+ FileRepository repository = new FileRepository(masterWorktreesDir);
+ try (Git git = new Git(repository)) {
+ ObjectId objectId = repository.resolve(HEAD);
+ assertNotNull(objectId);
+
+ Iterator<RevCommit> log = git.log().all().call().iterator();
+ assertTrue(log.hasNext());
+ assertTrue("Initial commit".equals(log.next().getShortMessage()));
+
+ // we have reflog entry
+ // depending on git version we either have one or
+ // two entries where extra is zeroid entry with
+ // same message or no message
+ Collection<ReflogEntry> reflog = git.reflog().call();
+ assertNotNull(reflog);
+ assertTrue(reflog.size() > 0);
+ ReflogEntry[] reflogs = reflog.toArray(new ReflogEntry[0]);
+ assertEquals(reflogs[reflogs.length - 1].getComment(),
+ "reset: moving to HEAD");
+
+ // index works with file changes
+ File masterDir = new File(directory, "master");
+ File testFile = new File(masterDir, "test");
+
+ Status status = git.status().call();
+ assertTrue(status.getUncommittedChanges().size() == 0);
+ assertTrue(status.getUntracked().size() == 0);
+
+ JGitTestUtil.write(testFile, "test");
+ status = git.status().call();
+ assertTrue(status.getUncommittedChanges().size() == 0);
+ assertTrue(status.getUntracked().size() == 1);
+
+ git.add().addFilepattern("test").call();
+ status = git.status().call();
+ assertTrue(status.getUncommittedChanges().size() == 1);
+ assertTrue(status.getUntracked().size() == 0);
+ }
+ }
+
+ @Test
+ public void testWeCanReadFromLinkedWorktreeFromNonBare() throws Exception {
+ FS fs = db.getFS();
+ worktreeAddNew(fs, db.getWorkTree(), "wt");
+
+ File worktreesDir = new File(db.getDirectory(), "worktrees");
+ File masterWorktreesDir = new File(worktreesDir, "wt");
+
+ FileRepository repository = new FileRepository(masterWorktreesDir);
+ try (Git git = new Git(repository)) {
+ ObjectId objectId = repository.resolve(HEAD);
+ assertNotNull(objectId);
+
+ Iterator<RevCommit> log = git.log().all().call().iterator();
+ assertTrue(log.hasNext());
+ assertTrue("Initial commit".equals(log.next().getShortMessage()));
+
+ // we have reflog entry
+ Collection<ReflogEntry> reflog = git.reflog().call();
+ assertNotNull(reflog);
+ assertTrue(reflog.size() > 0);
+ ReflogEntry[] reflogs = reflog.toArray(new ReflogEntry[0]);
+ assertEquals(reflogs[reflogs.length - 1].getComment(),
+ "reset: moving to HEAD");
+
+ // index works with file changes
+ File directory = trash.getParentFile();
+ File wtDir = new File(directory, "wt");
+ File testFile = new File(wtDir, "test");
+
+ Status status = git.status().call();
+ assertTrue(status.getUncommittedChanges().size() == 0);
+ assertTrue(status.getUntracked().size() == 0);
+
+ JGitTestUtil.write(testFile, "test");
+ status = git.status().call();
+ assertTrue(status.getUncommittedChanges().size() == 0);
+ assertTrue(status.getUntracked().size() == 1);
+
+ git.add().addFilepattern("test").call();
+ status = git.status().call();
+ assertTrue(status.getUncommittedChanges().size() == 1);
+ assertTrue(status.getUntracked().size() == 0);
+ }
+
+ }
+
+ private static void cloneBare(FS fs, File directory, String from, String to) throws IOException, InterruptedException {
+ ProcessBuilder builder = fs.runInShell("git",
+ new String[] { "clone", "--bare", from, to });
+ builder.directory(directory);
+ builder.environment().put("HOME", fs.userHome().getAbsolutePath());
+ StringBuilder input = new StringBuilder();
+ ExecutionResult result = fs.execute(builder, new ByteArrayInputStream(
+ input.toString().getBytes(StandardCharsets.UTF_8)));
+ String stdOut = toString(result.getStdout());
+ String errorOut = toString(result.getStderr());
+ assertNotNull(stdOut);
+ assertNotNull(errorOut);
+ }
+
+ private static void worktreeAddExisting(FS fs, File directory, String name) throws IOException, InterruptedException {
+ ProcessBuilder builder = fs.runInShell("git",
+ new String[] { "worktree", "add", "../" + name, name });
+ builder.directory(directory);
+ builder.environment().put("HOME", fs.userHome().getAbsolutePath());
+ StringBuilder input = new StringBuilder();
+ ExecutionResult result = fs.execute(builder, new ByteArrayInputStream(
+ input.toString().getBytes(StandardCharsets.UTF_8)));
+ String stdOut = toString(result.getStdout());
+ String errorOut = toString(result.getStderr());
+ assertNotNull(stdOut);
+ assertNotNull(errorOut);
+ }
+
+ private static void worktreeAddNew(FS fs, File directory, String name) throws IOException, InterruptedException {
+ ProcessBuilder builder = fs.runInShell("git",
+ new String[] { "worktree", "add", "-b", name, "../" + name, "master"});
+ builder.directory(directory);
+ builder.environment().put("HOME", fs.userHome().getAbsolutePath());
+ StringBuilder input = new StringBuilder();
+ ExecutionResult result = fs.execute(builder, new ByteArrayInputStream(
+ input.toString().getBytes(StandardCharsets.UTF_8)));
+ String stdOut = toString(result.getStdout());
+ String errorOut = toString(result.getStderr());
+ assertNotNull(stdOut);
+ assertNotNull(errorOut);
+ }
+
+ private static String toString(TemporaryBuffer b) throws IOException {
+ return RawParseUtils.decode(b.toByteArray());
+ }
+
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java
index 917b6c3297..1ec506798c 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/MergeCommandTest.java
@@ -21,6 +21,9 @@ import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
import java.io.File;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.Iterator;
import java.util.regex.Pattern;
@@ -33,6 +36,7 @@ import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.junit.TestRepository.BranchBuilder;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.RepositoryState;
import org.eclipse.jgit.lib.Sets;
@@ -45,6 +49,7 @@ import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.FileUtils;
import org.eclipse.jgit.util.GitDateFormatter;
import org.eclipse.jgit.util.GitDateFormatter.Format;
+import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.theories.DataPoints;
@@ -76,12 +81,12 @@ public class MergeCommandTest extends RepositoryTestCase {
assertEquals(MergeResult.MergeStatus.ALREADY_UP_TO_DATE, result.getMergeStatus());
}
// no reflog entry written by merge
- assertEquals("commit (initial): initial commit",
- db
+ RefDatabase refDb = db.getRefDatabase();
+ assertEquals("commit (initial): initial commit", refDb
.getReflogReader(Constants.HEAD).getLastEntry().getComment());
- assertEquals("commit (initial): initial commit",
- db
- .getReflogReader(db.getBranch()).getLastEntry().getComment());
+ assertEquals("commit (initial): initial commit", refDb
+ .getReflogReader(db.getFullBranch()).getLastEntry()
+ .getComment());
}
@Test
@@ -96,10 +101,11 @@ public class MergeCommandTest extends RepositoryTestCase {
assertEquals(second, result.getNewHead());
}
// no reflog entry written by merge
- assertEquals("commit: second commit", db
+ assertEquals("commit: second commit", db.getRefDatabase()
.getReflogReader(Constants.HEAD).getLastEntry().getComment());
- assertEquals("commit: second commit", db
- .getReflogReader(db.getBranch()).getLastEntry().getComment());
+ assertEquals("commit: second commit", db.getRefDatabase()
+ .getReflogReader(db.getFullBranch()).getLastEntry()
+ .getComment());
}
@Test
@@ -117,10 +123,13 @@ public class MergeCommandTest extends RepositoryTestCase {
assertEquals(MergeResult.MergeStatus.FAST_FORWARD, result.getMergeStatus());
assertEquals(second, result.getNewHead());
}
+ RefDatabase refDb = db.getRefDatabase();
assertEquals("merge refs/heads/master: Fast-forward",
- db.getReflogReader(Constants.HEAD).getLastEntry().getComment());
+ refDb.getReflogReader(Constants.HEAD)
+ .getLastEntry().getComment());
assertEquals("merge refs/heads/master: Fast-forward",
- db.getReflogReader(db.getBranch()).getLastEntry().getComment());
+ refDb.getReflogReader(db.getFullBranch())
+ .getLastEntry().getComment());
}
@Test
@@ -140,10 +149,12 @@ public class MergeCommandTest extends RepositoryTestCase {
result.getMergeStatus());
assertEquals(second, result.getNewHead());
}
- assertEquals("merge refs/heads/master: Fast-forward", db
+ RefDatabase refDb = db.getRefDatabase();
+ assertEquals("merge refs/heads/master: Fast-forward", refDb
.getReflogReader(Constants.HEAD).getLastEntry().getComment());
- assertEquals("merge refs/heads/master: Fast-forward", db
- .getReflogReader(db.getBranch()).getLastEntry().getComment());
+ assertEquals("merge refs/heads/master: Fast-forward", refDb
+ .getReflogReader(db.getFullBranch()).getLastEntry()
+ .getComment());
}
@Test
@@ -171,10 +182,12 @@ public class MergeCommandTest extends RepositoryTestCase {
assertEquals(MergeResult.MergeStatus.FAST_FORWARD, result.getMergeStatus());
assertEquals(second, result.getNewHead());
}
- assertEquals("merge refs/heads/master: Fast-forward",
- db.getReflogReader(Constants.HEAD).getLastEntry().getComment());
- assertEquals("merge refs/heads/master: Fast-forward",
- db.getReflogReader(db.getBranch()).getLastEntry().getComment());
+ RefDatabase refDb = db.getRefDatabase();
+ assertEquals("merge refs/heads/master: Fast-forward", refDb
+ .getReflogReader(Constants.HEAD).getLastEntry().getComment());
+ assertEquals("merge refs/heads/master: Fast-forward", refDb
+ .getReflogReader(db.getFullBranch()).getLastEntry()
+ .getComment());
}
@Test
@@ -229,14 +242,17 @@ public class MergeCommandTest extends RepositoryTestCase {
.include(db.exactRef(R_HEADS + MASTER)).call();
assertEquals(MergeStatus.MERGED, result.getMergeStatus());
}
+ RefDatabase refDb = db.getRefDatabase();
assertEquals(
"merge refs/heads/master: Merge made by "
+ mergeStrategy.getName() + ".",
- db.getReflogReader(Constants.HEAD).getLastEntry().getComment());
+ refDb.getReflogReader(Constants.HEAD).getLastEntry()
+ .getComment());
assertEquals(
"merge refs/heads/master: Merge made by "
+ mergeStrategy.getName() + ".",
- db.getReflogReader(db.getBranch()).getLastEntry().getComment());
+ refDb.getReflogReader(db.getFullBranch()).getLastEntry()
+ .getComment());
}
@Theory
@@ -662,14 +678,17 @@ public class MergeCommandTest extends RepositoryTestCase {
.setStrategy(MergeStrategy.RESOLVE).call();
assertEquals(MergeStatus.MERGED, result.getMergeStatus());
assertEquals("1\nb(1)\n3\n", read(new File(db.getWorkTree(), "b")));
- assertEquals("merge " + secondCommit.getId().getName()
- + ": Merge made by resolve.", db
- .getReflogReader(Constants.HEAD)
- .getLastEntry().getComment());
- assertEquals("merge " + secondCommit.getId().getName()
- + ": Merge made by resolve.", db
- .getReflogReader(db.getBranch())
- .getLastEntry().getComment());
+ RefDatabase refDb = db.getRefDatabase();
+ assertEquals(
+ "merge " + secondCommit.getId().getName()
+ + ": Merge made by resolve.",
+ refDb.getReflogReader(Constants.HEAD).getLastEntry()
+ .getComment());
+ assertEquals(
+ "merge " + secondCommit.getId().getName()
+ + ": Merge made by resolve.",
+ refDb.getReflogReader(db.getFullBranch()).getLastEntry()
+ .getComment());
}
}
@@ -2086,6 +2105,94 @@ public class MergeCommandTest extends RepositoryTestCase {
}
}
+ @Test
+ public void testMergeCaseInsensitiveRename() throws Exception {
+ Assume.assumeTrue(
+ "Test makes only sense on a case-insensitive file system",
+ db.isWorkTreeCaseInsensitive());
+ try (Git git = new Git(db)) {
+ writeTrashFile("a", "aaa");
+ git.add().addFilepattern("a").call();
+ RevCommit initialCommit = git.commit().setMessage("initial").call();
+ // "Rename" "a" to "A"
+ git.rm().addFilepattern("a").call();
+ writeTrashFile("A", "aaa");
+ git.add().addFilepattern("A").call();
+ RevCommit master = git.commit().setMessage("rename to A").call();
+
+ createBranch(initialCommit, "refs/heads/side");
+ checkoutBranch("refs/heads/side");
+
+ writeTrashFile("b", "bbb");
+ git.add().addFilepattern("b").call();
+ git.commit().setMessage("side").call();
+
+ // Merge master into side
+ MergeResult result = git.merge().include(master)
+ .setStrategy(MergeStrategy.RECURSIVE).call();
+ assertEquals(MergeStatus.MERGED, result.getMergeStatus());
+ assertTrue(new File(db.getWorkTree(), "A").isFile());
+ // Double check
+ boolean found = true;
+ try (DirectoryStream<Path> dir = Files
+ .newDirectoryStream(db.getWorkTree().toPath())) {
+ for (Path p : dir) {
+ found = "A".equals(p.getFileName().toString());
+ if (found) {
+ break;
+ }
+ }
+ }
+ assertTrue(found);
+ }
+ }
+
+ @Test
+ public void testMergeCaseInsensitiveRenameConflict() throws Exception {
+ Assume.assumeTrue(
+ "Test makes only sense on a case-insensitive file system",
+ db.isWorkTreeCaseInsensitive());
+ try (Git git = new Git(db)) {
+ writeTrashFile("a", "aaa");
+ git.add().addFilepattern("a").call();
+ RevCommit initialCommit = git.commit().setMessage("initial").call();
+ // "Rename" "a" to "A" and change it
+ git.rm().addFilepattern("a").call();
+ writeTrashFile("A", "yyy");
+ git.add().addFilepattern("A").call();
+ RevCommit master = git.commit().setMessage("rename to A").call();
+
+ createBranch(initialCommit, "refs/heads/side");
+ checkoutBranch("refs/heads/side");
+
+ writeTrashFile("a", "xxx");
+ git.add().addFilepattern("a").call();
+ git.commit().setMessage("side").call();
+
+ // Merge master into side
+ MergeResult result = git.merge().include(master)
+ .setStrategy(MergeStrategy.RECURSIVE).call();
+ assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
+ File a = new File(db.getWorkTree(), "A");
+ assertTrue(a.isFile());
+ // Double check
+ boolean found = true;
+ try (DirectoryStream<Path> dir = Files
+ .newDirectoryStream(db.getWorkTree().toPath())) {
+ for (Path p : dir) {
+ found = "A".equals(p.getFileName().toString());
+ if (found) {
+ break;
+ }
+ }
+ }
+ assertTrue(found);
+ assertEquals(1, result.getConflicts().size());
+ assertTrue(result.getConflicts().containsKey("a"));
+ checkFile(a, "yyy");
+ }
+ }
+
private static void setExecutable(Git git, String path, boolean executable) {
FS.DETECTED.setExecute(
new File(git.getRepository().getWorkTree(), path), executable);
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 f52b715d39..cf952d2b77 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
@@ -172,9 +172,9 @@ public class PathCheckoutCommandTest extends RepositoryTestCase {
@Test
public void testUpdateWorkingDirectoryFromIndex() throws Exception {
CheckoutCommand co = git.checkout();
- File written = writeTrashFile(FILE1, "3a");
+ writeTrashFile(FILE1, "3a");
git.add().addFilepattern(FILE1).call();
- written = writeTrashFile(FILE1, "");
+ File written = writeTrashFile(FILE1, "");
assertEquals("", read(written));
co.addPath(FILE1).call();
assertEquals("3a", read(written));
@@ -185,9 +185,9 @@ public class PathCheckoutCommandTest extends RepositoryTestCase {
public void testUpdateWorkingDirectoryFromHeadWithIndexChange()
throws Exception {
CheckoutCommand co = git.checkout();
- File written = writeTrashFile(FILE1, "3a");
+ writeTrashFile(FILE1, "3a");
git.add().addFilepattern(FILE1).call();
- written = writeTrashFile(FILE1, "");
+ File written = writeTrashFile(FILE1, "");
assertEquals("", read(written));
co.addPath(FILE1).setStartPoint("HEAD").call();
assertEquals("3", read(written));
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java
index 12300b3390..695681de8d 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java
@@ -21,6 +21,7 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
+import java.util.Map;
import java.util.concurrent.Callable;
import org.eclipse.jgit.api.CreateBranchCommand.SetupUpstreamMode;
@@ -29,6 +30,7 @@ import org.eclipse.jgit.api.errors.NoHeadException;
import org.eclipse.jgit.junit.JGitTestUtil;
import org.eclipse.jgit.junit.RepositoryTestCase;
import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.IndexDiff.StageState;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
@@ -117,6 +119,7 @@ public class PullCommandTest extends RepositoryTestCase {
+ db.getWorkTree().getAbsolutePath();
assertEquals(message, mergeCommit.getShortMessage());
}
+ assertTrue(target.status().call().isClean());
}
@Test
@@ -153,6 +156,10 @@ public class PullCommandTest extends RepositoryTestCase {
assertFileContentsEqual(targetFile, result);
assertEquals(RepositoryState.MERGING, target.getRepository()
.getRepositoryState());
+ Status status = target.status().call();
+ Map<String, StageState> conflicting = status.getConflictingStageState();
+ assertEquals(1, conflicting.size());
+ assertEquals(StageState.BOTH_MODIFIED, conflicting.get("SomeFile.txt"));
}
@Test
@@ -473,7 +480,7 @@ public class PullCommandTest extends RepositoryTestCase {
@Test
/** without config it should merge */
public void testPullWithoutConfig() throws Exception {
- Callable<PullResult> setup = target.pull()::call;
+ Callable<PullResult> setup = target.pull();
doTestPullWithRebase(setup, TestPullMode.MERGE);
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java
index ff5f8b76cc..d1696d62a8 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java
@@ -22,6 +22,7 @@ import java.io.IOException;
import java.io.PrintStream;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
+import java.time.Instant;
import java.util.Properties;
import org.eclipse.jgit.api.errors.DetachedHeadException;
@@ -941,7 +942,7 @@ public class PushCommandTest extends RepositoryTestCase {
}
/**
- * Check that branch.<name>.pushRemote overrides anything else.
+ * Check that branch.&lt;name&gt;.pushRemote overrides anything else.
*
* @throws Exception
*/
@@ -980,7 +981,7 @@ public class PushCommandTest extends RepositoryTestCase {
}
/**
- * Check that remote.pushDefault overrides branch.<name>.remote
+ * Check that remote.pushDefault overrides branch.&lt;name&gt;.remote
*
* @throws Exception
*/
@@ -1146,7 +1147,7 @@ public class PushCommandTest extends RepositoryTestCase {
RevCommit commit2 = git2.commit().setMessage("adding a").call();
// run a gc to ensure we have a bitmap index
- Properties res = git1.gc().setExpire(null).call();
+ Properties res = git1.gc().setExpire((Instant) null).call();
assertEquals(8, res.size());
// create another commit so we have something else to push
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java
index d574e45f6f..4c8cf06a67 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RebaseCommandTest.java
@@ -24,6 +24,8 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
+import java.time.Instant;
+import java.time.ZoneOffset;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
@@ -55,6 +57,7 @@ import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RebaseTodoLine;
import org.eclipse.jgit.lib.RebaseTodoLine.Action;
+import org.eclipse.jgit.lib.RefDatabase;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.ReflogEntry;
import org.eclipse.jgit.lib.RepositoryState;
@@ -76,6 +79,10 @@ public class RebaseCommandTest extends RepositoryTestCase {
private static final String FILE1 = "file1";
+ private static final String FILE2 = "file2";
+
+ private static final String FILE3 = "file3";
+
protected Git git;
@Override
@@ -127,11 +134,12 @@ public class RebaseCommandTest extends RepositoryTestCase {
checkFile(file2, "file2");
assertEquals(Status.FAST_FORWARD, res.getStatus());
- List<ReflogEntry> headLog = db.getReflogReader(Constants.HEAD)
+ RefDatabase refDb = db.getRefDatabase();
+ List<ReflogEntry> headLog = refDb.getReflogReader(Constants.HEAD)
.getReverseEntries();
- List<ReflogEntry> topicLog = db.getReflogReader("refs/heads/topic")
+ List<ReflogEntry> topicLog = refDb.getReflogReader("refs/heads/topic")
.getReverseEntries();
- List<ReflogEntry> masterLog = db.getReflogReader("refs/heads/master")
+ List<ReflogEntry> masterLog = refDb.getReflogReader("refs/heads/master")
.getReverseEntries();
assertEquals("rebase finished: returning to refs/heads/topic", headLog
.get(0).getComment());
@@ -173,11 +181,12 @@ public class RebaseCommandTest extends RepositoryTestCase {
checkFile(file2, "file2 new content");
assertEquals(Status.FAST_FORWARD, res.getStatus());
- List<ReflogEntry> headLog = db.getReflogReader(Constants.HEAD)
+ RefDatabase refDb = db.getRefDatabase();
+ List<ReflogEntry> headLog = refDb.getReflogReader(Constants.HEAD)
.getReverseEntries();
- List<ReflogEntry> topicLog = db.getReflogReader("refs/heads/topic")
+ List<ReflogEntry> topicLog = refDb.getReflogReader("refs/heads/topic")
.getReverseEntries();
- List<ReflogEntry> masterLog = db.getReflogReader("refs/heads/master")
+ List<ReflogEntry> masterLog = refDb.getReflogReader("refs/heads/master")
.getReverseEntries();
assertEquals("rebase finished: returning to refs/heads/topic", headLog
.get(0).getComment());
@@ -191,6 +200,177 @@ public class RebaseCommandTest extends RepositoryTestCase {
}
/**
+ * Rebase a single root commit onto an independent branch.
+ *
+ * <pre>
+ * A (master)
+ *
+ * B - C (orphan)
+ * </pre>
+ *
+ * to
+ *
+ * <pre>
+ * A
+ *
+ * B - C (orphan) - A' (master)
+ * </pre>
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testRebaseRootCommit() throws Exception {
+ writeTrashFile(FILE1, FILE1);
+ writeTrashFile(FILE2, FILE2);
+ git.add().addFilepattern(FILE1).addFilepattern(FILE2).call();
+ RevCommit first = git.commit().setMessage("Add files").call();
+ File file1 = new File(db.getWorkTree(), FILE1);
+ File file2 = new File(db.getWorkTree(), FILE2);
+ assertTrue(file1.exists());
+ assertTrue(file2.exists());
+ // Create an independent branch
+ git.checkout().setOrphan(true).setName("orphan").call();
+ git.rm().addFilepattern(FILE1).addFilepattern(FILE2).call();
+ assertFalse(file1.exists());
+ assertFalse(file2.exists());
+ writeTrashFile(FILE1, "something else");
+ git.add().addFilepattern(FILE1).call();
+ RevCommit orphanBase = git.commit().setMessage("Orphan base").call();
+ writeTrashFile(FILE1, FILE1);
+ git.add().addFilepattern(FILE1).call();
+ RevCommit orphanTop = git.commit().setMessage("Same file1").call();
+ checkoutBranch("refs/heads/master");
+ assertEquals(first.getId(), db.resolve("HEAD"));
+ RebaseResult res = git.rebase().setUpstream("refs/heads/orphan").call();
+ assertEquals(Status.OK, res.getStatus());
+ Iterable<RevCommit> log = git.log().add(db.resolve("HEAD")).call();
+ ObjectId[] ids = { orphanTop.getId(), orphanBase.getId() };
+ int nOfCommits = 0;
+ for (RevCommit c : log) {
+ nOfCommits++;
+ if (nOfCommits == 1) {
+ assertEquals("Add files", c.getFullMessage());
+ } else {
+ assertEquals(ids[nOfCommits - 2], c.getId());
+ }
+ }
+ assertEquals(3, nOfCommits);
+ assertTrue(file1.exists());
+ checkFile(file1, FILE1);
+ assertTrue(file2.exists());
+ checkFile(file2, FILE2);
+ }
+
+ /**
+ * Rebase a branch onto an independent branch.
+ *
+ * <pre>
+ * A - B (master)
+ *
+ * C - D (orphan)
+ * </pre>
+ *
+ * to
+ *
+ * <pre>
+ * A - B
+ *
+ * C - D (orphan) - A' - B' (master)
+ * </pre>
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testRebaseNoMergeBase() throws Exception {
+ writeTrashFile(FILE1, FILE1);
+ writeTrashFile(FILE2, FILE2);
+ git.add().addFilepattern(FILE1).addFilepattern(FILE2).call();
+ git.commit().setMessage("Add files").call();
+ writeTrashFile(FILE3, FILE3);
+ git.add().addFilepattern(FILE3).call();
+ RevCommit first = git.commit().setMessage("File3").call();
+ File file1 = new File(db.getWorkTree(), FILE1);
+ File file2 = new File(db.getWorkTree(), FILE2);
+ File file3 = new File(db.getWorkTree(), FILE3);
+ assertTrue(file1.exists());
+ assertTrue(file2.exists());
+ assertTrue(file3.exists());
+ // Create an independent branch
+ git.checkout().setOrphan(true).setName("orphan").call();
+ git.rm()
+ .addFilepattern(FILE1)
+ .addFilepattern(FILE2)
+ .addFilepattern(FILE3)
+ .call();
+ assertFalse(file1.exists());
+ assertFalse(file2.exists());
+ assertFalse(file3.exists());
+ writeTrashFile(FILE1, "something else");
+ git.add().addFilepattern(FILE1).call();
+ RevCommit orphanBase = git.commit().setMessage("Orphan base").call();
+ writeTrashFile(FILE1, FILE1);
+ git.add().addFilepattern(FILE1).call();
+ RevCommit orphanTop = git.commit().setMessage("Same file1").call();
+ checkoutBranch("refs/heads/master");
+ assertEquals(first.getId(), db.resolve("HEAD"));
+ RebaseResult res = git.rebase().setUpstream("refs/heads/orphan").call();
+ assertEquals(Status.OK, res.getStatus());
+ Iterable<RevCommit> log = git.log().add(db.resolve("HEAD")).call();
+ String[] msgs = { "File3", "Add files" };
+ ObjectId[] ids = { orphanTop.getId(), orphanBase.getId() };
+ int nOfCommits = 0;
+ for (RevCommit c : log) {
+ nOfCommits++;
+ if (nOfCommits <= msgs.length) {
+ assertEquals(msgs[nOfCommits - 1], c.getFullMessage());
+ } else {
+ assertEquals(ids[nOfCommits - msgs.length - 1], c.getId());
+ }
+ }
+ assertEquals(4, nOfCommits);
+ assertTrue(file1.exists());
+ checkFile(file1, FILE1);
+ assertTrue(file2.exists());
+ checkFile(file2, FILE2);
+ assertTrue(file3.exists());
+ checkFile(file3, FILE3);
+ }
+
+ /**
+ * Create a commit A and an unrelated commit B creating the same file with
+ * different content. Then rebase A onto B. The rebase should stop with a
+ * conflict.
+ *
+ * @throws Exception
+ * on errors
+ */
+ @Test
+ public void testRebaseNoMergeBaseConflict() throws Exception {
+ writeTrashFile(FILE1, FILE1);
+ git.add().addFilepattern(FILE1).call();
+ RevCommit first = git.commit().setMessage("Add file").call();
+ File file1 = new File(db.getWorkTree(), FILE1);
+ assertTrue(file1.exists());
+ // Create an independent branch
+ git.checkout().setOrphan(true).setName("orphan").call();
+ git.rm().addFilepattern(FILE1).call();
+ assertFalse(file1.exists());
+ writeTrashFile(FILE1, "something else");
+ git.add().addFilepattern(FILE1).call();
+ git.commit().setMessage("Orphan").call();
+ checkoutBranch("refs/heads/master");
+ assertEquals(first.getId(), db.resolve("HEAD"));
+ RebaseResult res = git.rebase().setUpstream("refs/heads/orphan").call();
+ assertEquals(Status.STOPPED, res.getStatus());
+ assertEquals(first, res.getCurrentCommit());
+ checkFile(file1, "<<<<<<< Upstream, based on orphan\n"
+ + "something else\n"
+ + "=======\n"
+ + "file1\n"
+ + ">>>>>>> " + first.abbreviate(7).name() + " Add file\n");
+ }
+
+ /**
* Create the following commits and then attempt to rebase topic onto
* master. This will serialize the branches.
*
@@ -270,13 +450,14 @@ public class RebaseCommandTest extends RepositoryTestCase {
assertEquals(a, rw.next());
}
- List<ReflogEntry> headLog = db.getReflogReader(Constants.HEAD)
+ RefDatabase refDb = db.getRefDatabase();
+ List<ReflogEntry> headLog = refDb.getReflogReader(Constants.HEAD)
.getReverseEntries();
- List<ReflogEntry> sideLog = db.getReflogReader("refs/heads/side")
+ List<ReflogEntry> sideLog = refDb.getReflogReader("refs/heads/side")
.getReverseEntries();
- List<ReflogEntry> topicLog = db.getReflogReader("refs/heads/topic")
+ List<ReflogEntry> topicLog = refDb.getReflogReader("refs/heads/topic")
.getReverseEntries();
- List<ReflogEntry> masterLog = db.getReflogReader("refs/heads/master")
+ List<ReflogEntry> masterLog = refDb.getReflogReader("refs/heads/master")
.getReverseEntries();
assertEquals("rebase finished: returning to refs/heads/topic", headLog
.get(0).getComment());
@@ -591,9 +772,10 @@ public class RebaseCommandTest extends RepositoryTestCase {
RebaseResult result = git.rebase().setUpstream(parent).call();
assertEquals(Status.UP_TO_DATE, result.getStatus());
- assertEquals(2, db.getReflogReader(Constants.HEAD).getReverseEntries()
- .size());
- assertEquals(2, db.getReflogReader("refs/heads/master")
+ RefDatabase refDb = db.getRefDatabase();
+ assertEquals(2, refDb.getReflogReader(Constants.HEAD)
+ .getReverseEntries().size());
+ assertEquals(2, refDb.getReflogReader("refs/heads/master")
.getReverseEntries().size());
}
@@ -609,9 +791,10 @@ public class RebaseCommandTest extends RepositoryTestCase {
RebaseResult res = git.rebase().setUpstream(first).call();
assertEquals(Status.UP_TO_DATE, res.getStatus());
- assertEquals(1, db.getReflogReader(Constants.HEAD).getReverseEntries()
- .size());
- assertEquals(1, db.getReflogReader("refs/heads/master")
+ RefDatabase refDb = db.getRefDatabase();
+ assertEquals(1, refDb.getReflogReader(Constants.HEAD)
+ .getReverseEntries().size());
+ assertEquals(1, refDb.getReflogReader("refs/heads/master")
.getReverseEntries().size());
}
@@ -669,11 +852,12 @@ public class RebaseCommandTest extends RepositoryTestCase {
db.resolve(Constants.HEAD)).getParent(0));
}
assertEquals(origHead, db.readOrigHead());
- List<ReflogEntry> headLog = db.getReflogReader(Constants.HEAD)
+ RefDatabase refDb = db.getRefDatabase();
+ List<ReflogEntry> headLog = refDb.getReflogReader(Constants.HEAD)
.getReverseEntries();
- List<ReflogEntry> topicLog = db.getReflogReader("refs/heads/topic")
+ List<ReflogEntry> topicLog = refDb.getReflogReader("refs/heads/topic")
.getReverseEntries();
- List<ReflogEntry> masterLog = db.getReflogReader("refs/heads/master")
+ List<ReflogEntry> masterLog = refDb.getReflogReader("refs/heads/master")
.getReverseEntries();
assertEquals(2, masterLog.size());
assertEquals(3, topicLog.size());
@@ -721,8 +905,8 @@ public class RebaseCommandTest extends RepositoryTestCase {
db.resolve(Constants.HEAD)).getParent(0));
}
- List<ReflogEntry> headLog = db.getReflogReader(Constants.HEAD)
- .getReverseEntries();
+ List<ReflogEntry> headLog = db.getRefDatabase()
+ .getReflogReader(Constants.HEAD).getReverseEntries();
assertEquals(8, headLog.size());
assertEquals("rebase: change file1 in topic", headLog.get(0)
.getComment());
@@ -1428,7 +1612,7 @@ public class RebaseCommandTest extends RepositoryTestCase {
public void testAuthorScriptConverter() throws Exception {
// -1 h timezone offset
PersonIdent ident = new PersonIdent("Author name", "a.mail@some.com",
- 123456789123L, -60);
+ Instant.ofEpochMilli(123456789123L), ZoneOffset.ofHours(-1));
String convertedAuthor = git.rebase().toAuthorScript(ident);
String[] lines = convertedAuthor.split("\n");
assertEquals("GIT_AUTHOR_NAME='Author name'", lines[0]);
@@ -1440,12 +1624,14 @@ public class RebaseCommandTest extends RepositoryTestCase {
assertEquals(ident.getName(), parsedIdent.getName());
assertEquals(ident.getEmailAddress(), parsedIdent.getEmailAddress());
// this is rounded to the last second
- assertEquals(123456789000L, parsedIdent.getWhen().getTime());
- assertEquals(ident.getTimeZoneOffset(), parsedIdent.getTimeZoneOffset());
+ assertEquals(123456789000L,
+ parsedIdent.getWhenAsInstant().toEpochMilli());
+ assertEquals(ident.getZoneId(), parsedIdent.getZoneId());
// + 9.5h timezone offset
ident = new PersonIdent("Author name", "a.mail@some.com",
- 123456789123L, +570);
+ Instant.ofEpochMilli(123456789123L),
+ ZoneOffset.ofHoursMinutes(9, 30));
convertedAuthor = git.rebase().toAuthorScript(ident);
lines = convertedAuthor.split("\n");
assertEquals("GIT_AUTHOR_NAME='Author name'", lines[0]);
@@ -1456,8 +1642,9 @@ public class RebaseCommandTest extends RepositoryTestCase {
convertedAuthor.getBytes(UTF_8));
assertEquals(ident.getName(), parsedIdent.getName());
assertEquals(ident.getEmailAddress(), parsedIdent.getEmailAddress());
- assertEquals(123456789000L, parsedIdent.getWhen().getTime());
- assertEquals(ident.getTimeZoneOffset(), parsedIdent.getTimeZoneOffset());
+ assertEquals(123456789000L,
+ parsedIdent.getWhenAsInstant().toEpochMilli());
+ assertEquals(ident.getZoneId(), parsedIdent.getZoneId());
}
@Test
@@ -2080,7 +2267,7 @@ public class RebaseCommandTest extends RepositoryTestCase {
checkoutBranch("refs/heads/master");
writeTrashFile(FILE1, "modified file1");
git.add().addFilepattern(FILE1).call();
- git.commit().setMessage("commit3").call();
+ git.commit().setMessage("commit2").call();
// checkout topic branch / modify file0
checkoutBranch("refs/heads/topic");
@@ -2099,6 +2286,57 @@ public class RebaseCommandTest extends RepositoryTestCase {
assertEquals(RepositoryState.SAFE, db.getRepositoryState());
}
+ @Test
+ public void testFastForwardRebaseWithAutoStashConflict() throws Exception {
+ // create file0, add and commit
+ db.getConfig().setBoolean(ConfigConstants.CONFIG_REBASE_SECTION, null,
+ ConfigConstants.CONFIG_KEY_AUTOSTASH, true);
+ writeTrashFile("file0", "file0");
+ git.add().addFilepattern("file0").call();
+ git.commit().setMessage("commit0").call();
+ // create file1, add and commit
+ writeTrashFile(FILE1, "file1");
+ git.add().addFilepattern(FILE1).call();
+ RevCommit commit = git.commit().setMessage("commit1").call();
+
+ // create topic branch
+ createBranch(commit, "refs/heads/topic");
+
+ // checkout master branch / modify file1, add and commit
+ checkoutBranch("refs/heads/master");
+ writeTrashFile(FILE1, "modified file1");
+ git.add().addFilepattern(FILE1).call();
+ RevCommit master = git.commit().setMessage("commit2").call();
+
+ // checkout topic branch / modify file0 and file1
+ checkoutBranch("refs/heads/topic");
+ writeTrashFile("file0", "unstaged modified file0");
+ writeTrashFile(FILE1, "unstaged modified file1");
+
+ // rebase
+ assertEquals(Status.STASH_APPLY_CONFLICTS,
+ git.rebase().setUpstream("refs/heads/master").call()
+ .getStatus());
+ checkFile(new File(db.getWorkTree(), "file0"),
+ "unstaged modified file0");
+ checkFile(new File(db.getWorkTree(), FILE1),
+ "<<<<<<< HEAD\n"
+ + "modified file1\n"
+ + "=======\n"
+ + "unstaged modified file1\n"
+ + ">>>>>>> stash\n");
+ // If there is a merge conflict, the index is not reset, and thus file0
+ // is staged here. This is the same behavior as in C git.
+ String expected = "[file0, mode:100644, content:unstaged modified file0]"
+ + "[file1, mode:100644, stage:1, content:file1]"
+ + "[file1, mode:100644, stage:2, content:modified file1]"
+ + "[file1, mode:100644, stage:3, content:unstaged modified file1]";
+ assertEquals(expected, indexState(CONTENT));
+ assertEquals(RepositoryState.SAFE, db.getRepositoryState());
+ assertEquals(master, db.resolve(Constants.HEAD));
+ assertEquals(master, db.resolve("refs/heads/topic"));
+ }
+
private List<DiffEntry> getStashedDiff() throws AmbiguousObjectException,
IncorrectObjectTypeException, IOException, MissingObjectException {
ObjectId stashId = db.resolve("stash@{0}");
@@ -2424,7 +2662,9 @@ public class RebaseCommandTest extends RepositoryTestCase {
assertEquals("1111111", firstLine.getCommit().name());
assertEquals("pick", firstLine.getAction().toToken());
} catch (Exception e) {
- fail("Valid parsable RebaseTodoLine that has been commented out should allow to change the action, but failed");
+ throw new AssertionError(
+ "Valid parsable RebaseTodoLine that has been commented out should allow to change the action, but failed",
+ e);
}
assertEquals("2222222", steps.get(1).getCommit().name());
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RenameBranchCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RenameBranchCommandTest.java
index 534ebd9c61..add5886c2d 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RenameBranchCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RenameBranchCommandTest.java
@@ -118,23 +118,21 @@ public class RenameBranchCommandTest extends RepositoryTestCase {
String branch = "b1";
assertEquals(BranchRebaseMode.REBASE,
- config.getEnum(BranchRebaseMode.values(),
- ConfigConstants.CONFIG_BRANCH_SECTION, Constants.MASTER,
- ConfigConstants.CONFIG_KEY_REBASE,
+ config.getEnum(ConfigConstants.CONFIG_BRANCH_SECTION,
+ Constants.MASTER, ConfigConstants.CONFIG_KEY_REBASE,
BranchRebaseMode.NONE));
assertNull(config.getEnum(BranchRebaseMode.values(),
ConfigConstants.CONFIG_BRANCH_SECTION, branch,
- ConfigConstants.CONFIG_KEY_REBASE, null));
+ ConfigConstants.CONFIG_KEY_REBASE));
assertNotNull(git.branchRename().setNewName(branch).call());
config = git.getRepository().getConfig();
assertNull(config.getEnum(BranchRebaseMode.values(),
ConfigConstants.CONFIG_BRANCH_SECTION, Constants.MASTER,
- ConfigConstants.CONFIG_KEY_REBASE, null));
+ ConfigConstants.CONFIG_KEY_REBASE));
assertEquals(BranchRebaseMode.REBASE,
- config.getEnum(BranchRebaseMode.values(),
- ConfigConstants.CONFIG_BRANCH_SECTION, branch,
+ config.getEnum(ConfigConstants.CONFIG_BRANCH_SECTION, branch,
ConfigConstants.CONFIG_KEY_REBASE,
BranchRebaseMode.NONE));
}
@@ -170,13 +168,12 @@ public class RenameBranchCommandTest extends RepositoryTestCase {
String branch = "b1";
assertEquals(BranchRebaseMode.REBASE,
- config.getEnum(BranchRebaseMode.values(),
- ConfigConstants.CONFIG_BRANCH_SECTION, Constants.MASTER,
- ConfigConstants.CONFIG_KEY_REBASE,
+ config.getEnum(ConfigConstants.CONFIG_BRANCH_SECTION,
+ Constants.MASTER, ConfigConstants.CONFIG_KEY_REBASE,
BranchRebaseMode.NONE));
assertNull(config.getEnum(BranchRebaseMode.values(),
ConfigConstants.CONFIG_BRANCH_SECTION, branch,
- ConfigConstants.CONFIG_KEY_REBASE, null));
+ ConfigConstants.CONFIG_KEY_REBASE));
assertTrue(config.getBoolean(ConfigConstants.CONFIG_BRANCH_SECTION,
Constants.MASTER, ConfigConstants.CONFIG_KEY_MERGE, true));
assertFalse(config.getBoolean(ConfigConstants.CONFIG_BRANCH_SECTION,
@@ -187,10 +184,9 @@ public class RenameBranchCommandTest extends RepositoryTestCase {
config = git.getRepository().getConfig();
assertNull(config.getEnum(BranchRebaseMode.values(),
ConfigConstants.CONFIG_BRANCH_SECTION, Constants.MASTER,
- ConfigConstants.CONFIG_KEY_REBASE, null));
+ ConfigConstants.CONFIG_KEY_REBASE));
assertEquals(BranchRebaseMode.REBASE,
- config.getEnum(BranchRebaseMode.values(),
- ConfigConstants.CONFIG_BRANCH_SECTION, branch,
+ config.getEnum(ConfigConstants.CONFIG_BRANCH_SECTION, branch,
ConfigConstants.CONFIG_KEY_REBASE,
BranchRebaseMode.NONE));
assertFalse(config.getBoolean(ConfigConstants.CONFIG_BRANCH_SECTION,
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 8a479a0ca0..99873e1be1 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
@@ -36,11 +36,13 @@ 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.lib.RefDatabase;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.util.FileUtils;
import org.junit.Assert;
+import org.junit.Assume;
import org.junit.Test;
public class ResetCommandTest extends RepositoryTestCase {
@@ -554,46 +556,73 @@ public class ResetCommandTest extends RepositoryTestCase {
assertNull(db.resolve(Constants.HEAD));
}
+ @Test
+ public void testHardResetFileMode() throws Exception {
+ Assume.assumeTrue("Test must be able to set executable bit",
+ db.getFS().supportsExecute());
+ git = new Git(db);
+ File a = writeTrashFile("a.txt", "aaa");
+ File b = writeTrashFile("b.txt", "bbb");
+ db.getFS().setExecute(b, true);
+ assertFalse(db.getFS().canExecute(a));
+ assertTrue(db.getFS().canExecute(b));
+ git.add().addFilepattern("a.txt").addFilepattern("b.txt").call();
+ RevCommit commit = git.commit().setMessage("files created").call();
+ db.getFS().setExecute(a, true);
+ db.getFS().setExecute(b, false);
+ assertTrue(db.getFS().canExecute(a));
+ assertFalse(db.getFS().canExecute(b));
+ git.add().addFilepattern("a.txt").addFilepattern("b.txt").call();
+ git.commit().setMessage("change exe bits").call();
+ Ref ref = git.reset().setRef(commit.getName()).setMode(HARD).call();
+ assertSameAsHead(ref);
+ assertEquals(commit.getId(), ref.getObjectId());
+ assertFalse(db.getFS().canExecute(a));
+ assertTrue(db.getFS().canExecute(b));
+ }
+
private void assertReflog(ObjectId prevHead, ObjectId head)
throws IOException {
// Check the reflog for HEAD
- String actualHeadMessage = db.getReflogReader(Constants.HEAD)
+ RefDatabase refDb = db.getRefDatabase();
+ String actualHeadMessage = refDb.getReflogReader(Constants.HEAD)
.getLastEntry().getComment();
String expectedHeadMessage = head.getName() + ": updating HEAD";
assertEquals(expectedHeadMessage, actualHeadMessage);
- assertEquals(head.getName(), db.getReflogReader(Constants.HEAD)
+ assertEquals(head.getName(), refDb.getReflogReader(Constants.HEAD)
.getLastEntry().getNewId().getName());
- assertEquals(prevHead.getName(), db.getReflogReader(Constants.HEAD)
+ assertEquals(prevHead.getName(), refDb.getReflogReader(Constants.HEAD)
.getLastEntry().getOldId().getName());
// The reflog for master contains the same as the one for HEAD
- String actualMasterMessage = db.getReflogReader("refs/heads/master")
+ String actualMasterMessage = refDb.getReflogReader("refs/heads/master")
.getLastEntry().getComment();
String expectedMasterMessage = head.getName() + ": updating HEAD"; // yes!
assertEquals(expectedMasterMessage, actualMasterMessage);
- assertEquals(head.getName(), db.getReflogReader(Constants.HEAD)
+ assertEquals(head.getName(), refDb.getReflogReader(Constants.HEAD)
.getLastEntry().getNewId().getName());
- assertEquals(prevHead.getName(), db
- .getReflogReader("refs/heads/master").getLastEntry().getOldId()
- .getName());
+ assertEquals(prevHead.getName(),
+ refDb.getReflogReader("refs/heads/master").getLastEntry()
+ .getOldId().getName());
}
private void assertReflogDisabled(ObjectId head)
throws IOException {
+ RefDatabase refDb = db.getRefDatabase();
// Check the reflog for HEAD
- String actualHeadMessage = db.getReflogReader(Constants.HEAD)
+ String actualHeadMessage = refDb.getReflogReader(Constants.HEAD)
.getLastEntry().getComment();
String expectedHeadMessage = "commit: adding a.txt and dir/b.txt";
assertEquals(expectedHeadMessage, actualHeadMessage);
- assertEquals(head.getName(), db.getReflogReader(Constants.HEAD)
+ assertEquals(head.getName(), refDb.getReflogReader(Constants.HEAD)
.getLastEntry().getOldId().getName());
// The reflog for master contains the same as the one for HEAD
- String actualMasterMessage = db.getReflogReader("refs/heads/master")
+ String actualMasterMessage = refDb.getReflogReader("refs/heads/master")
.getLastEntry().getComment();
String expectedMasterMessage = "commit: adding a.txt and dir/b.txt";
assertEquals(expectedMasterMessage, actualMasterMessage);
- assertEquals(head.getName(), db.getReflogReader(Constants.HEAD)
+ assertEquals(head.getName(), refDb.getReflogReader(Constants.HEAD)
.getLastEntry().getOldId().getName());
}
/**
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RevertCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RevertCommandTest.java
index 1c7b8d13a8..89fdb32220 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RevertCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/RevertCommandTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2011, Robin Rosenberg and others
+ * Copyright (C) 2011, 2024 Robin Rosenberg and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -29,6 +29,7 @@ import org.eclipse.jgit.junit.RepositoryTestCase;
import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.RefDatabase;
import org.eclipse.jgit.lib.ReflogReader;
import org.eclipse.jgit.lib.RepositoryState;
import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason;
@@ -39,6 +40,7 @@ import org.junit.Test;
* Test revert command
*/
public class RevertCommandTest extends RepositoryTestCase {
+
@Test
public void testRevert() throws IOException, JGitInternalException,
GitAPIException {
@@ -58,7 +60,9 @@ public class RevertCommandTest extends RepositoryTestCase {
writeTrashFile("a",
"first line\nsecond line\nthird line\nfourth line\n");
git.add().addFilepattern("a").call();
- RevCommit fixingA = git.commit().setMessage("fixed a").call();
+ // Commit message with a non-empty second line on purpose
+ RevCommit fixingA = git.commit().setMessage("fixed a\nsecond line")
+ .call();
writeTrashFile("b", "first line\n");
git.add().addFilepattern("b").call();
@@ -77,20 +81,58 @@ public class RevertCommandTest extends RepositoryTestCase {
+ "This reverts commit " + fixingA.getId().getName() + ".\n";
assertEquals(expectedMessage, revertCommit.getFullMessage());
assertEquals("fixed b", history.next().getFullMessage());
- assertEquals("fixed a", history.next().getFullMessage());
+ assertEquals("fixed a\nsecond line",
+ history.next().getFullMessage());
assertEquals("enlarged a", history.next().getFullMessage());
assertEquals("create b", history.next().getFullMessage());
assertEquals("create a", history.next().getFullMessage());
assertFalse(history.hasNext());
- ReflogReader reader = db.getReflogReader(Constants.HEAD);
+ RefDatabase refDb = db.getRefDatabase();
+ ReflogReader reader = refDb.getReflogReader(Constants.HEAD);
assertTrue(reader.getLastEntry().getComment()
.startsWith("revert: Revert \""));
- reader = db.getReflogReader(db.getBranch());
+ reader = refDb.getReflogReader(db.getFullBranch());
assertTrue(reader.getLastEntry().getComment()
.startsWith("revert: Revert \""));
}
+ }
+ @Test
+ public void testRevertWithChangeId()
+ throws IOException, JGitInternalException, GitAPIException {
+ try (Git git = new Git(db)) {
+ writeTrashFile("a", "first line\nthird line\n");
+ git.add().addFilepattern("a").call();
+ git.commit().setMessage("create a").call();
+
+ writeTrashFile("a", "first line\nsecond line\nthird line\n");
+ git.add().addFilepattern("a").call();
+ RevCommit second = git.commit().setMessage("changed a").call();
+
+ writeTrashFile("a",
+ "first line\nsecond line\nthird line\nfourth line\n");
+ git.add().addFilepattern("a").call();
+ git.commit().setMessage("changed a again").call();
+
+ git.revert().include(second).setInsertChangeId(true).call();
+
+ assertEquals(RepositoryState.SAFE, db.getRepositoryState());
+
+ checkFile(new File(db.getWorkTree(), "a"),
+ "first line\nthird line\nfourth line\n");
+ Iterator<RevCommit> history = git.log().call().iterator();
+ RevCommit revertCommit = history.next();
+ String expectedMessage = "Revert \"changed a\"\n\n"
+ + "This reverts commit " + second.getId().getName() + ".\n";
+ String commitMessage = revertCommit.getFullMessage();
+ assertTrue(commitMessage.matches("^\\Q" + expectedMessage
+ + "\\E\nChange-Id: I[a-fA-F0-9]{40}\n$"));
+ assertEquals("changed a again", history.next().getFullMessage());
+ assertEquals("changed a", history.next().getFullMessage());
+ assertEquals("create a", history.next().getFullMessage());
+ assertFalse(history.hasNext());
+ }
}
@Test
@@ -130,10 +172,11 @@ public class RevertCommandTest extends RepositoryTestCase {
assertEquals("add first", history.next().getFullMessage());
assertFalse(history.hasNext());
- ReflogReader reader = db.getReflogReader(Constants.HEAD);
+ RefDatabase refDb = db.getRefDatabase();
+ ReflogReader reader = refDb.getReflogReader(Constants.HEAD);
assertTrue(reader.getLastEntry().getComment()
.startsWith("revert: Revert \""));
- reader = db.getReflogReader(db.getBranch());
+ reader = refDb.getReflogReader(db.getFullBranch());
assertTrue(reader.getLastEntry().getComment()
.startsWith("revert: Revert \""));
}
@@ -183,10 +226,11 @@ public class RevertCommandTest extends RepositoryTestCase {
assertEquals("add first", history.next().getFullMessage());
assertFalse(history.hasNext());
- ReflogReader reader = db.getReflogReader(Constants.HEAD);
+ RefDatabase refDb = db.getRefDatabase();
+ ReflogReader reader = refDb.getReflogReader(Constants.HEAD);
assertTrue(reader.getLastEntry().getComment()
.startsWith("revert: Revert \""));
- reader = db.getReflogReader(db.getBranch());
+ reader = refDb.getReflogReader(db.getFullBranch());
assertTrue(reader.getLastEntry().getComment()
.startsWith("revert: Revert \""));
}
@@ -391,12 +435,13 @@ public class RevertCommandTest extends RepositoryTestCase {
assertEquals(RepositoryState.SAFE, db.getRepositoryState());
if (reason == null) {
- ReflogReader reader = db.getReflogReader(Constants.HEAD);
- assertTrue(reader.getLastEntry().getComment()
- .startsWith("revert: "));
- reader = db.getReflogReader(db.getBranch());
- assertTrue(reader.getLastEntry().getComment()
- .startsWith("revert: "));
+ RefDatabase refDb = db.getRefDatabase();
+ ReflogReader reader = refDb.getReflogReader(Constants.HEAD);
+ assertTrue(
+ reader.getLastEntry().getComment().startsWith("revert: "));
+ reader = refDb.getReflogReader(db.getFullBranch());
+ assertTrue(
+ reader.getLastEntry().getComment().startsWith("revert: "));
}
}
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/SecurityManagerMissingPermissionsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/SecurityManagerMissingPermissionsTest.java
deleted file mode 100644
index d0fbdbd090..0000000000
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/SecurityManagerMissingPermissionsTest.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * Copyright (c) 2019 Alex Jitianu <alex_jitianu@sync.ro> and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-package org.eclipse.jgit.api;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.IOException;
-import java.io.PrintStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.security.Policy;
-import java.util.Collections;
-
-import org.eclipse.jgit.junit.RepositoryTestCase;
-import org.eclipse.jgit.util.FileUtils;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * Tests that using a SecurityManager does not result in errors logged.
- */
-public class SecurityManagerMissingPermissionsTest extends RepositoryTestCase {
-
- /**
- * Collects all logging sent to the logging system.
- */
- private final ByteArrayOutputStream errorOutput = new ByteArrayOutputStream();
-
- private SecurityManager originalSecurityManager;
-
- private PrintStream defaultErrorOutput;
-
- @Override
- @Before
- public void setUp() throws Exception {
- originalSecurityManager = System.getSecurityManager();
-
- // slf4j-simple logs to System.err, redirect it to enable asserting
- // logged errors
- defaultErrorOutput = System.err;
- System.setErr(new PrintStream(errorOutput));
-
- refreshPolicyAllPermission(Policy.getPolicy());
- System.setSecurityManager(new SecurityManager());
- super.setUp();
- }
-
- /**
- * If a SecurityManager is active a lot of {@link java.io.FilePermission}
- * errors are thrown and logged while initializing a repository.
- *
- * @throws Exception
- */
- @Test
- public void testCreateNewRepos_MissingPermissions() throws Exception {
- File wcTree = new File(getTemporaryDirectory(),
- "CreateNewRepositoryTest_testCreateNewRepos");
-
- File marker = new File(getTemporaryDirectory(), "marker");
- Files.write(marker.toPath(), Collections.singletonList("Can write"));
- assertTrue("Can write in test directory", marker.isFile());
- FileUtils.delete(marker);
- assertFalse("Can delete in test direcory", marker.exists());
-
- Git git = Git.init().setBare(false)
- .setDirectory(new File(wcTree.getAbsolutePath())).call();
-
- addRepoToClose(git.getRepository());
-
- assertEquals("", errorOutput.toString());
- }
-
- @Override
- @After
- public void tearDown() throws Exception {
- System.setSecurityManager(originalSecurityManager);
- System.setErr(defaultErrorOutput);
- super.tearDown();
- }
-
- /**
- * Refresh the Java Security Policy.
- *
- * @param policy
- * the policy object
- *
- * @throws IOException
- * if the temporary file that contains the policy could not be
- * created
- */
- private static void refreshPolicyAllPermission(Policy policy)
- throws IOException {
- // Starting with an all permissions policy.
- String policyString = "grant { permission java.security.AllPermission; };";
-
- // Do not use TemporaryFilesFactory, it will create a dependency cycle
- Path policyFile = Files.createTempFile("testpolicy", ".txt");
-
- try {
- Files.write(policyFile, Collections.singletonList(policyString));
- System.setProperty("java.security.policy",
- policyFile.toUri().toURL().toString());
- policy.refresh();
- } finally {
- try {
- Files.delete(policyFile);
- } catch (IOException e) {
- // Do not log; the test tests for no logging having occurred
- e.printStackTrace();
- }
- }
- }
-
-}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/SecurityManagerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/SecurityManagerTest.java
deleted file mode 100644
index 2b930a1133..0000000000
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/SecurityManagerTest.java
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- * Copyright (C) 2019 Nail Samatov <sanail@yandex.ru> and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
-package org.eclipse.jgit.api;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-
-import java.io.File;
-import java.io.FilePermission;
-import java.io.IOException;
-import java.lang.reflect.ReflectPermission;
-import java.nio.file.Files;
-import java.security.Permission;
-import java.security.SecurityPermission;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.PropertyPermission;
-import java.util.logging.LoggingPermission;
-
-import javax.security.auth.AuthPermission;
-
-import org.eclipse.jgit.api.errors.GitAPIException;
-import org.eclipse.jgit.junit.JGitTestUtil;
-import org.eclipse.jgit.junit.MockSystemReader;
-import org.eclipse.jgit.junit.SeparateClassloaderTestRunner;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.treewalk.TreeWalk;
-import org.eclipse.jgit.util.FileUtils;
-import org.eclipse.jgit.util.SystemReader;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * <p>
- * Tests if jgit works if SecurityManager is enabled.
- * </p>
- *
- * <p>
- * Note: JGit's classes shouldn't be used before SecurityManager is configured.
- * If you use some JGit's class before SecurityManager is replaced then part of
- * the code can be invoked outside of our custom SecurityManager and this test
- * becomes useless.
- * </p>
- *
- * <p>
- * For example the class {@link org.eclipse.jgit.util.FS} is used widely in jgit
- * sources. It contains DETECTED static field. At the first usage of the class
- * FS the field DETECTED is initialized and during initialization many system
- * operations that SecurityManager can forbid are invoked.
- * </p>
- *
- * <p>
- * For this reason this test doesn't extend LocalDiskRepositoryTestCase (it uses
- * JGit's classes in setUp() method) and other JGit's utility classes. It's done
- * to affect SecurityManager as less as possible.
- * </p>
- *
- * <p>
- * We use SeparateClassloaderTestRunner to isolate FS.DETECTED field
- * initialization between different tests run.
- * </p>
- */
-@RunWith(SeparateClassloaderTestRunner.class)
-public class SecurityManagerTest {
- private File root;
-
- private SecurityManager originalSecurityManager;
-
- private List<Permission> permissions = new ArrayList<>();
-
- @Before
- public void setUp() throws Exception {
- // Create working directory
- SystemReader.setInstance(new MockSystemReader());
- root = Files.createTempDirectory("jgit-security").toFile();
-
- // Add system permissions
- permissions.add(new RuntimePermission("*"));
- permissions.add(new SecurityPermission("*"));
- permissions.add(new AuthPermission("*"));
- permissions.add(new ReflectPermission("*"));
- permissions.add(new PropertyPermission("*", "read,write"));
- permissions.add(new LoggingPermission("control", null));
-
- permissions.add(new FilePermission(
- System.getProperty("java.home") + "/-", "read"));
-
- String tempDir = System.getProperty("java.io.tmpdir");
- permissions.add(new FilePermission(tempDir, "read,write,delete"));
- permissions
- .add(new FilePermission(tempDir + "/-", "read,write,delete"));
-
- // Add permissions to dependent jar files.
- String classPath = System.getProperty("java.class.path");
- if (classPath != null) {
- for (String path : classPath.split(File.pathSeparator)) {
- permissions.add(new FilePermission(path, "read"));
- }
- }
- // Add permissions to jgit class files.
- String jgitSourcesRoot = new File(System.getProperty("user.dir"))
- .getParent();
- permissions.add(new FilePermission(jgitSourcesRoot + "/-", "read"));
-
- // Add permissions to working dir for jgit. Our git repositories will be
- // initialized and cloned here.
- permissions.add(new FilePermission(root.getPath() + "/-",
- "read,write,delete,execute"));
-
- // Replace Security Manager
- originalSecurityManager = System.getSecurityManager();
- System.setSecurityManager(new SecurityManager() {
-
- @Override
- public void checkPermission(Permission requested) {
- for (Permission permission : permissions) {
- if (permission.implies(requested)) {
- return;
- }
- }
-
- super.checkPermission(requested);
- }
- });
- }
-
- @After
- public void tearDown() throws Exception {
- System.setSecurityManager(originalSecurityManager);
-
- // Note: don't use this method before security manager is replaced in
- // setUp() method. The method uses FS.DETECTED internally and can affect
- // the test.
- FileUtils.delete(root, FileUtils.RECURSIVE | FileUtils.RETRY);
- }
-
- @Test
- public void testInitAndClone() throws IOException, GitAPIException {
- File remote = new File(root, "remote");
- File local = new File(root, "local");
-
- try (Git git = Git.init().setDirectory(remote).call()) {
- JGitTestUtil.write(new File(remote, "hello.txt"), "Hello world!");
- git.add().addFilepattern(".").call();
- git.commit().setMessage("Initial commit").call();
- }
-
- try (Git git = Git.cloneRepository().setURI(remote.toURI().toString())
- .setDirectory(local).call()) {
- assertTrue(new File(local, ".git").exists());
-
- JGitTestUtil.write(new File(local, "hi.txt"), "Hi!");
- git.add().addFilepattern(".").call();
- RevCommit commit1 = git.commit().setMessage("Commit on local repo")
- .call();
- assertEquals("Commit on local repo", commit1.getFullMessage());
- assertNotNull(TreeWalk.forPath(git.getRepository(), "hello.txt",
- commit1.getTree()));
- }
-
- }
-
-}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashCreateCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashCreateCommandTest.java
index 5d0ab05174..18cd21a5d7 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashCreateCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashCreateCommandTest.java
@@ -409,8 +409,8 @@ public class StashCreateCommandTest extends RepositoryTestCase {
assertEquals("content", read(committedFile));
validateStashedCommit(stashed);
- ReflogReader reader = git.getRepository().getReflogReader(
- Constants.R_STASH);
+ ReflogReader reader = git.getRepository().getRefDatabase()
+ .getReflogReader(Constants.R_STASH);
ReflogEntry entry = reader.getLastEntry();
assertNotNull(entry);
assertEquals(ObjectId.zeroId(), entry.getOldId());
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashDropCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashDropCommandTest.java
index f9af968a7e..d937579283 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashDropCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashDropCommandTest.java
@@ -69,8 +69,7 @@ public class StashDropCommandTest extends RepositoryTestCase {
RevCommit stashed = git.stashCreate().call();
assertNotNull(stashed);
stashRef = git.getRepository().exactRef(Constants.R_STASH);
- assertEquals(stashed,
- git.getRepository().exactRef(Constants.R_STASH).getObjectId());
+ assertEquals(stashed, stashRef.getObjectId());
try {
assertNull(git.stashDrop().setStashRef(100).call());
fail("Exception not thrown");
@@ -88,14 +87,13 @@ public class StashDropCommandTest extends RepositoryTestCase {
RevCommit stashed = git.stashCreate().call();
assertNotNull(stashed);
stashRef = git.getRepository().exactRef(Constants.R_STASH);
- assertEquals(stashed,
- git.getRepository().exactRef(Constants.R_STASH).getObjectId());
+ assertEquals(stashed, stashRef.getObjectId());
assertNull(git.stashDrop().call());
stashRef = git.getRepository().exactRef(Constants.R_STASH);
assertNull(stashRef);
- ReflogReader reader = git.getRepository().getReflogReader(
- Constants.R_STASH);
+ ReflogReader reader = git.getRepository().getRefDatabase()
+ .getReflogReader(Constants.R_STASH);
assertNull(reader);
}
@@ -122,8 +120,8 @@ public class StashDropCommandTest extends RepositoryTestCase {
assertNull(git.stashDrop().setAll(true).call());
assertNull(git.getRepository().exactRef(Constants.R_STASH));
- ReflogReader reader = git.getRepository().getReflogReader(
- Constants.R_STASH);
+ ReflogReader reader = git.getRepository().getRefDatabase()
+ .getReflogReader(Constants.R_STASH);
assertNull(reader);
}
@@ -152,8 +150,8 @@ public class StashDropCommandTest extends RepositoryTestCase {
assertNotNull(stashRef);
assertEquals(firstStash, stashRef.getObjectId());
- ReflogReader reader = git.getRepository().getReflogReader(
- Constants.R_STASH);
+ ReflogReader reader = git.getRepository().getRefDatabase()
+ .getReflogReader(Constants.R_STASH);
List<ReflogEntry> entries = reader.getReverseEntries();
assertEquals(1, entries.size());
assertEquals(ObjectId.zeroId(), entries.get(0).getOldId());
@@ -194,8 +192,8 @@ public class StashDropCommandTest extends RepositoryTestCase {
assertNotNull(stashRef);
assertEquals(thirdStash, stashRef.getObjectId());
- ReflogReader reader = git.getRepository().getReflogReader(
- Constants.R_STASH);
+ ReflogReader reader = git.getRepository().getRefDatabase()
+ .getReflogReader(Constants.R_STASH);
List<ReflogEntry> entries = reader.getReverseEntries();
assertEquals(2, entries.size());
assertEquals(ObjectId.zeroId(), entries.get(1).getOldId());
@@ -252,8 +250,8 @@ public class StashDropCommandTest extends RepositoryTestCase {
assertNotNull(stashRef);
assertEquals(thirdStash, stashRef.getObjectId());
- ReflogReader reader = git.getRepository().getReflogReader(
- Constants.R_STASH);
+ ReflogReader reader = git.getRepository().getRefDatabase()
+ .getReflogReader(Constants.R_STASH);
List<ReflogEntry> entries = reader.getReverseEntries();
assertEquals(2, entries.size());
assertEquals(ObjectId.zeroId(), entries.get(1).getOldId());
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/blame/BlameGeneratorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/blame/BlameGeneratorTest.java
index f47f447375..c2c06b2477 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/blame/BlameGeneratorTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/blame/BlameGeneratorTest.java
@@ -23,20 +23,22 @@ import org.junit.Test;
/** Unit tests of {@link BlameGenerator}. */
public class BlameGeneratorTest extends RepositoryTestCase {
+ private static final String FILE = "file.txt";
+
@Test
public void testBoundLineDelete() throws Exception {
try (Git git = new Git(db)) {
String[] content1 = new String[] { "first", "second" };
- writeTrashFile("file.txt", join(content1));
- git.add().addFilepattern("file.txt").call();
+ writeTrashFile(FILE, join(content1));
+ git.add().addFilepattern(FILE).call();
RevCommit c1 = git.commit().setMessage("create file").call();
String[] content2 = new String[] { "third", "first", "second" };
- writeTrashFile("file.txt", join(content2));
- git.add().addFilepattern("file.txt").call();
+ writeTrashFile(FILE, join(content2));
+ git.add().addFilepattern(FILE).call();
RevCommit c2 = git.commit().setMessage("create file").call();
- try (BlameGenerator generator = new BlameGenerator(db, "file.txt")) {
+ try (BlameGenerator generator = new BlameGenerator(db, FILE)) {
generator.push(null, db.resolve(Constants.HEAD));
assertEquals(3, generator.getResultContents().size());
@@ -47,7 +49,7 @@ public class BlameGeneratorTest extends RepositoryTestCase {
assertEquals(1, generator.getResultEnd());
assertEquals(0, generator.getSourceStart());
assertEquals(1, generator.getSourceEnd());
- assertEquals("file.txt", generator.getSourcePath());
+ assertEquals(FILE, generator.getSourcePath());
assertTrue(generator.next());
assertEquals(c1, generator.getSourceCommit());
@@ -56,7 +58,7 @@ public class BlameGeneratorTest extends RepositoryTestCase {
assertEquals(3, generator.getResultEnd());
assertEquals(0, generator.getSourceStart());
assertEquals(2, generator.getSourceEnd());
- assertEquals("file.txt", generator.getSourcePath());
+ assertEquals(FILE, generator.getSourcePath());
assertFalse(generator.next());
}
@@ -87,7 +89,8 @@ public class BlameGeneratorTest extends RepositoryTestCase {
git.add().addFilepattern(FILENAME_2).call();
RevCommit c2 = git.commit().setMessage("change file2").call();
- try (BlameGenerator generator = new BlameGenerator(db, FILENAME_2)) {
+ try (BlameGenerator generator = new BlameGenerator(db,
+ FILENAME_2)) {
generator.push(null, db.resolve(Constants.HEAD));
assertEquals(3, generator.getResultContents().size());
@@ -113,7 +116,8 @@ public class BlameGeneratorTest extends RepositoryTestCase {
}
// and test again with other BlameGenerator API:
- try (BlameGenerator generator = new BlameGenerator(db, FILENAME_2)) {
+ try (BlameGenerator generator = new BlameGenerator(db,
+ FILENAME_2)) {
generator.push(null, db.resolve(Constants.HEAD));
BlameResult result = generator.computeBlameResult();
@@ -136,21 +140,21 @@ public class BlameGeneratorTest extends RepositoryTestCase {
try (Git git = new Git(db)) {
String[] content1 = new String[] { "first", "second", "third" };
- writeTrashFile("file.txt", join(content1));
- git.add().addFilepattern("file.txt").call();
+ writeTrashFile(FILE, join(content1));
+ git.add().addFilepattern(FILE).call();
git.commit().setMessage("create file").call();
String[] content2 = new String[] { "" };
- writeTrashFile("file.txt", join(content2));
- git.add().addFilepattern("file.txt").call();
+ writeTrashFile(FILE, join(content2));
+ git.add().addFilepattern(FILE).call();
git.commit().setMessage("create file").call();
- writeTrashFile("file.txt", join(content1));
- git.add().addFilepattern("file.txt").call();
+ writeTrashFile(FILE, join(content1));
+ git.add().addFilepattern(FILE).call();
RevCommit c3 = git.commit().setMessage("create file").call();
- try (BlameGenerator generator = new BlameGenerator(db, "file.txt")) {
+ try (BlameGenerator generator = new BlameGenerator(db, FILE)) {
generator.push(null, db.resolve(Constants.HEAD));
assertEquals(3, generator.getResultContents().size());
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesHandlerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesHandlerTest.java
index 7fb98ec53b..c41dd81add 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesHandlerTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesHandlerTest.java
@@ -584,7 +584,7 @@ public class AttributesHandlerTest extends RepositoryTestCase {
}
if (infoAttributesContent != null) {
- File f = new File(db.getDirectory(), Constants.INFO_ATTRIBUTES);
+ File f = new File(db.getCommonDirectory(), Constants.INFO_ATTRIBUTES);
write(f, infoAttributesContent);
}
config.save();
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeDirCacheIteratorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeDirCacheIteratorTest.java
index f23469eda0..35b953320e 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeDirCacheIteratorTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeDirCacheIteratorTest.java
@@ -26,6 +26,7 @@ import org.eclipse.jgit.attributes.Attribute.State;
import org.eclipse.jgit.dircache.DirCacheIterator;
import org.eclipse.jgit.junit.RepositoryTestCase;
import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.junit.Before;
import org.junit.Test;
@@ -230,10 +231,10 @@ public class AttributesNodeDirCacheIteratorTest extends RepositoryTestCase {
else {
Attributes entryAttributes = new Attributes();
- new AttributesHandler(walk).mergeAttributes(attributesNode,
- pathName,
- false,
- entryAttributes);
+ new AttributesHandler(walk,
+ () -> walk.getTree(CanonicalTreeParser.class))
+ .mergeAttributes(attributesNode, pathName, false,
+ entryAttributes);
if (nodeAttrs != null && !nodeAttrs.isEmpty()) {
for (Attribute attribute : nodeAttrs) {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeTest.java
index 1fcfbaf0fa..dbbcb75da9 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeTest.java
@@ -20,6 +20,7 @@ import java.io.InputStream;
import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.junit.After;
import org.junit.Test;
@@ -156,8 +157,9 @@ public class AttributesNodeTest {
private void assertAttribute(String path, AttributesNode node,
Attributes attrs) throws IOException {
Attributes attributes = new Attributes();
- new AttributesHandler(DUMMY_WALK).mergeAttributes(node, path, false,
- attributes);
+ new AttributesHandler(DUMMY_WALK,
+ () -> DUMMY_WALK.getTree(CanonicalTreeParser.class))
+ .mergeAttributes(node, path, false, attributes);
assertEquals(attrs, attributes);
}
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 7b573e122e..c6c91386a2 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
@@ -26,6 +26,7 @@ import org.eclipse.jgit.attributes.Attribute.State;
import org.eclipse.jgit.junit.JGitTestUtil;
import org.eclipse.jgit.junit.RepositoryTestCase;
import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.treewalk.FileTreeIterator;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.WorkingTreeIterator;
@@ -194,9 +195,10 @@ public class AttributesNodeWorkingTreeIteratorTest extends RepositoryTestCase {
else {
Attributes entryAttributes = new Attributes();
- new AttributesHandler(walk).mergeAttributes(attributesNode,
- pathName, false,
- entryAttributes);
+ new AttributesHandler(walk,
+ () -> walk.getTree(CanonicalTreeParser.class))
+ .mergeAttributes(attributesNode, pathName, false,
+ entryAttributes);
if (nodeAttrs != null && !nodeAttrs.isEmpty()) {
for (Attribute attribute : nodeAttrs) {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/CGitAttributesTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/CGitAttributesTest.java
index 5638c1f7d9..562a515721 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/CGitAttributesTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/CGitAttributesTest.java
@@ -104,10 +104,9 @@ public class CGitAttributesTest extends RepositoryTestCase {
UTF_8))) {
r.lines().forEach(line -> {
// Parse the line and add to result map
- int start = 0;
int i = line.indexOf(':');
String path = line.substring(0, i).trim();
- start = i + 1;
+ int start = i + 1;
i = line.indexOf(':', start);
String key = line.substring(start, i).trim();
String value = line.substring(i + 1).trim();
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/TreeWalkAttributeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/TreeWalkAttributeTest.java
index 73abd2d37e..698fdb31a8 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/TreeWalkAttributeTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/TreeWalkAttributeTest.java
@@ -791,7 +791,7 @@ public class TreeWalkAttributeTest extends RepositoryTestCase {
for (Attribute a : attributes) {
ret.add(a);
}
- return (ret);
+ return ret;
}
private File writeAttributesFile(String name, String... rules)
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/merge/MergeGitAttributeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/merge/MergeGitAttributeTest.java
index 795029188d..ac30c6c526 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/merge/MergeGitAttributeTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/merge/MergeGitAttributeTest.java
@@ -10,7 +10,6 @@
package org.eclipse.jgit.attributes.merge;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@@ -20,6 +19,7 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.util.function.Consumer;
@@ -42,7 +42,6 @@ import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.treewalk.FileTreeIterator;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.filter.PathFilter;
-import org.junit.Ignore;
import org.junit.Test;
public class MergeGitAttributeTest extends RepositoryTestCase {
@@ -99,19 +98,19 @@ public class MergeGitAttributeTest extends RepositoryTestCase {
try {
writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "D\n");
} catch (IOException e) {
- e.printStackTrace();
+ throw new UncheckedIOException(e);
}
}, g -> {
try {
writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "F\n");
} catch (IOException e) {
- e.printStackTrace();
+ throw new UncheckedIOException(e);
}
}, g -> {
try {
writeTrashFile("main.cat", "A\n" + "E\n" + "C\n" + "D\n");
} catch (IOException e) {
- e.printStackTrace();
+ throw new UncheckedIOException(e);
}
})) {
checkoutBranch(REFS_HEADS_LEFT);
@@ -141,19 +140,19 @@ public class MergeGitAttributeTest extends RepositoryTestCase {
writeTrashFile(".gitattributes", "*.cat -merge");
writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "D\n");
} catch (IOException e) {
- e.printStackTrace();
+ throw new UncheckedIOException(e);
}
}, g -> {
try {
writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "F\n");
} catch (IOException e) {
- e.printStackTrace();
+ throw new UncheckedIOException(e);
}
}, g -> {
try {
writeTrashFile("main.cat", "A\n" + "E\n" + "C\n" + "D\n");
} catch (IOException e) {
- e.printStackTrace();
+ throw new UncheckedIOException(e);
}
})) {
// Check that the merge attribute is unset
@@ -186,19 +185,19 @@ public class MergeGitAttributeTest extends RepositoryTestCase {
writeTrashFile(".gitattributes", "*.txt -merge");
writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "D\n");
} catch (IOException e) {
- e.printStackTrace();
+ throw new UncheckedIOException(e);
}
}, g -> {
try {
writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "F\n");
} catch (IOException e) {
- e.printStackTrace();
+ throw new UncheckedIOException(e);
}
}, g -> {
try {
writeTrashFile("main.cat", "A\n" + "E\n" + "C\n" + "D\n");
} catch (IOException e) {
- e.printStackTrace();
+ throw new UncheckedIOException(e);
}
})) {
// Check that the merge attribute is unset
@@ -230,19 +229,19 @@ public class MergeGitAttributeTest extends RepositoryTestCase {
writeTrashFile(".gitattributes", "*.cat merge=binary");
writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "D\n");
} catch (IOException e) {
- e.printStackTrace();
+ throw new UncheckedIOException(e);
}
}, g -> {
try {
writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "F\n");
} catch (IOException e) {
- e.printStackTrace();
+ throw new UncheckedIOException(e);
}
}, g -> {
try {
writeTrashFile("main.cat", "A\n" + "E\n" + "C\n" + "D\n");
} catch (IOException e) {
- e.printStackTrace();
+ throw new UncheckedIOException(e);
}
})) {
// Check that the merge attribute is set to binary
@@ -268,12 +267,52 @@ public class MergeGitAttributeTest extends RepositoryTestCase {
}
}
- /*
- * This test is commented because JGit add conflict markers in binary files.
- * cf. https://www.eclipse.org/forums/index.php/t/1086511/
- */
@Test
- @Ignore
+ public void mergeTextualFile_SetUnionMerge() throws NoWorkTreeException,
+ NoFilepatternException, GitAPIException, IOException {
+ try (Git git = createRepositoryBinaryConflict(g -> {
+ try {
+ writeTrashFile(".gitattributes", "*.cat merge=union");
+ writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "D\n");
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }, g -> {
+ try {
+ writeTrashFile("main.cat", "A\n" + "G\n" + "C\n" + "F\n");
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }, g -> {
+ try {
+ writeTrashFile("main.cat", "A\n" + "E\n" + "C\n" + "D\n");
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ })) {
+ // Check that the merge attribute is set to union
+ assertAddMergeAttributeCustom(REFS_HEADS_LEFT, "main.cat", "union");
+ assertAddMergeAttributeCustom(REFS_HEADS_RIGHT, "main.cat",
+ "union");
+
+ checkoutBranch(REFS_HEADS_LEFT);
+ // Merge refs/heads/left -> refs/heads/right
+
+ MergeResult mergeResult = git.merge()
+ .include(git.getRepository().resolve(REFS_HEADS_RIGHT))
+ .call();
+ assertEquals(MergeStatus.MERGED, mergeResult.getMergeStatus());
+
+ // Check that the file is the union of both branches (no conflict
+ // marker added)
+ String result = read(writeTrashFile("res.cat",
+ "A\n" + "G\n" + "E\n" + "C\n" + "F\n"));
+ assertEquals(result, read(git.getRepository().getWorkTree().toPath()
+ .resolve("main.cat").toFile()));
+ }
+ }
+
+ @Test
public void mergeBinaryFile_NoAttr_Conflict() throws IllegalStateException,
IOException, NoHeadException, ConcurrentRefUpdateException,
CheckoutConflictException, InvalidMergeHeadsException,
@@ -433,7 +472,7 @@ public class MergeGitAttributeTest extends RepositoryTestCase {
try (FileInputStream mergeResultFile = new FileInputStream(
db.getWorkTree().toPath().resolve(ENABLED_CHECKED_GIF)
.toFile())) {
- assertFalse(contentEquals(
+ assertTrue(contentEquals(
getClass().getResourceAsStream(ENABLED_CHECKED_GIF),
mergeResultFile));
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/blame/BlameGeneratorCacheTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/blame/BlameGeneratorCacheTest.java
new file mode 100644
index 0000000000..3e4ac1f993
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/blame/BlameGeneratorCacheTest.java
@@ -0,0 +1,552 @@
+/*
+ * Copyright (C) 2025, Google LLC.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.blame;
+
+import static java.lang.String.join;
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jgit.blame.cache.BlameCache;
+import org.eclipse.jgit.blame.cache.CacheRegion;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
+public class BlameGeneratorCacheTest extends RepositoryTestCase {
+ private static final String FILE = "file.txt";
+
+ /**
+ * Simple history:
+ *
+ * <pre>
+ * C1 C2 C3 C4 C4 blame
+ * lines ----------------------------------
+ * L1 | C1 C1 C1 C1 C1
+ * L2 | C1 C1 *C3 *C4 C4
+ * L3 | C1 C1 *C3 C3 C3
+ * L4 | *C2 C2 *C4 C4
+ * </pre>
+ *
+ * @throws Exception
+ * any error
+ */
+ @Test
+ public void blame_simple_correctRegions() throws Exception {
+ RevCommit c1, c2, c3, c4;
+ try (TestRepository<FileRepository> r = new TestRepository<>(db)) {
+ c1 = commit(r, lines("L1C1", "L2C1", "L3C1"));
+ c2 = commit(r, lines("L1C1", "L2C1", "L3C1", "L4C2"), c1);
+ c3 = commit(r, lines("L1C1", "L2C3", "L3C3", "L4C2"), c2);
+ c4 = commit(r, lines("L1C1", "L2C4", "L3C3", "L4C4"), c3);
+ }
+
+ List<EmittedRegion> expectedRegions = Arrays.asList(
+ new EmittedRegion(c1, 0, 1),
+ new EmittedRegion(c4, 1, 2),
+ new EmittedRegion(c3, 2, 3),
+ new EmittedRegion(c4, 3, 4));
+
+ assertRegions(c4, null, expectedRegions, 4);
+ assertRegions(c4, emptyCache(), expectedRegions, 4);
+ assertRegions(c4, blameAndCache(c4), expectedRegions, 4);
+ assertRegions(c4, blameAndCache(c3), expectedRegions, 4);
+ assertRegions(c4, blameAndCache(c2), expectedRegions, 4);
+ assertRegions(c4, blameAndCache(c1), expectedRegions, 4);
+ }
+
+ @Test
+ public void blame_simple_cacheUsage() throws Exception {
+ RevCommit c1, c2, c3, c4;
+ try (TestRepository<FileRepository> r = new TestRepository<>(db)) {
+ c1 = commit(r, lines("L1C1", "L2C1", "L3C1"));
+ c2 = commit(r, lines("L1C1", "L2C1", "L3C1", "L4C2"), c1);
+ c3 = commit(r, lines("L1C1", "L2C3", "L3C3", "L4C2"), c2);
+ c4 = commit(r, lines("L1C1", "L2C4", "L3C3", "L4C4"), c3);
+ }
+
+ assertCacheUsage(c4, null, false, 4);
+ assertCacheUsage(c4, emptyCache(), false, 4);
+ assertCacheUsage(c4, blameAndCache(c4), true, 1);
+ assertCacheUsage(c4, blameAndCache(c3), true, 2);
+ assertCacheUsage(c4, blameAndCache(c2), true, 3);
+ // Cache not needed because c1 doesn't have parents
+ assertCacheUsage(c4, blameAndCache(c1), false, 4);
+ }
+
+ @Test
+ public void blame_simple_createdMidHistory_correctRegions() throws Exception {
+ String c1Content = lines("L1C1", "L2C1", "L3C1");
+ String c2Content = lines("L1C1", "L2C1", "L3C1", "L4C2");
+ String c3Content = lines("L1C1", "L2C3", "L3C3", "L4C2");
+ String c4Content = lines("L1C1", "L2C4", "L3C3", "L4C4");
+
+ RevCommit c0, c1, c2, c3, c4;
+ try (TestRepository<FileRepository> r = new TestRepository<>(db)) {
+ c0 = r.commit().add("otherfile", "content").create();
+ c1 = r.commit().parent(c0).add(FILE, c1Content).create();
+ c2 = r.commit().parent(c1).add(FILE, c2Content).create();
+ c3 = r.commit().parent(c2).add(FILE, c3Content).create();
+ c4 = r.commit().parent(c3).add(FILE, c4Content).create();
+ }
+
+ List<EmittedRegion> expectedRegions = Arrays.asList(
+ new EmittedRegion(c1, 0, 1),
+ new EmittedRegion(c4, 1, 2),
+ new EmittedRegion(c3, 2, 3),
+ new EmittedRegion(c4, 3, 4));
+
+ assertRegions(c4, null, expectedRegions, 4);
+ assertRegions(c4, emptyCache(), expectedRegions, 4);
+ assertRegions(c4, blameAndCache(c4, FILE), expectedRegions, 4);
+ assertRegions(c4, blameAndCache(c3, FILE), expectedRegions, 4);
+ assertRegions(c4, blameAndCache(c2, FILE), expectedRegions, 4);
+ assertRegions(c4, blameAndCache(c1, FILE), expectedRegions, 4);
+ assertRegions(c4, blameAndCache(c0, FILE), expectedRegions, 4);
+ }
+
+ @Test
+ public void blame_simple_createdMidHistory_cacheUsage() throws Exception {
+ String c1Content = lines("L1C1", "L2C1", "L3C1");
+ String c2Content = lines("L1C1", "L2C1", "L3C1", "L4C2");
+ String c3Content = lines("L1C1", "L2C3", "L3C3", "L4C2");
+ String c4Content = lines("L1C1", "L2C4", "L3C3", "L4C4");
+
+ RevCommit c0, c1, c2, c3, c4;
+ try (TestRepository<FileRepository> r = new TestRepository<>(db)) {
+ c0 = r.commit().add("otherfile", "content").create();
+ c1 = r.commit().parent(c0).add(FILE, c1Content).create();
+ c2 = r.commit().parent(c1).add(FILE, c2Content).create();
+ c3 = r.commit().parent(c2).add(FILE, c3Content).create();
+ c4 = r.commit().parent(c3).add(FILE, c4Content).create();
+ }
+
+ assertCacheUsage(c4, null, false, 4);
+ assertCacheUsage(c4, emptyCache(), false, 4);
+ assertCacheUsage(c4, blameAndCache(c4, FILE), true, 1);
+ assertCacheUsage(c4, blameAndCache(c3, FILE), true, 2);
+ assertCacheUsage(c4, blameAndCache(c2, FILE), true, 3);
+ // Cache not needed because c1 created the file
+ assertCacheUsage(c4, blameAndCache(c1, FILE), false, 4);
+ }
+
+ /**
+ * Overwrite:
+ *
+ * <pre>
+ * C1 C2 C3 C3 blame
+ * lines ----------------------------------
+ * L1 | C1 C1 *C3 C3
+ * L2 | C1 C1 *C3 C3
+ * L3 | C1 C1 *C3 C3
+ * L4 | *C2
+ * </pre>
+ *
+ * @throws Exception
+ * any error
+ */
+ @Test
+ public void blame_overwrite_correctRegions() throws Exception {
+ RevCommit c1, c2, c3;
+ try (TestRepository<FileRepository> r = new TestRepository<>(db)) {
+ c1 = commit(r, lines("L1C1", "L2C1", "L3C1"));
+ c2 = commit(r, lines("L1C1", "L2C1", "L3C1", "L4C2"), c1);
+ c3 = commit(r, lines("L1C3", "L2C3", "L3C3"), c2);
+ }
+
+ List<EmittedRegion> expectedRegions = Arrays.asList(
+ new EmittedRegion(c3, 0, 3));
+
+ assertRegions(c3, null, expectedRegions, 3);
+ assertRegions(c3, emptyCache(), expectedRegions, 3);
+ assertRegions(c3, blameAndCache(c3), expectedRegions, 3);
+ assertRegions(c3, blameAndCache(c2), expectedRegions, 3);
+ assertRegions(c3, blameAndCache(c1), expectedRegions, 3);
+ }
+
+ @Test
+ public void blame_overwrite_cacheUsage() throws Exception {
+ RevCommit c1, c2, c3;
+ try (TestRepository<FileRepository> r = new TestRepository<>(db)) {
+ c1 = commit(r, lines("L1C1", "L2C1", "L3C1"));
+ c2 = commit(r, lines("L1C1", "L2C1", "L3C1", "L4C2"), c1);
+ c3 = commit(r, lines("L1C3", "L2C3", "L3C3"), c2);
+ }
+
+ assertCacheUsage(c3, null, false, 1);
+ assertCacheUsage(c3, emptyCache(), false, 1);
+ assertCacheUsage(c3, blameAndCache(c3), true, 1);
+ assertCacheUsage(c3, blameAndCache(c2), false, 1);
+ assertCacheUsage(c3, blameAndCache(c1), false, 1);
+ }
+
+ /**
+ * Merge:
+ *
+ * <pre>
+ * root
+ * ----
+ * L1 -
+ * L2 -
+ * L3 -
+ * / \
+ * sideA sideB
+ * ----- -----
+ * *L1 a L1 -
+ * *L2 a L2 -
+ * *L3 a L3 -
+ * *L4 a *L4 b
+ * L5 - *L5 b
+ * L6 - *L6 b
+ * L7 - *L7 b
+ * \ /
+ * merge
+ * -----
+ * L1-L4 a (from sideA)
+ * L5-L7 - (common, from root)
+ * L8-L11 b (from sideB)
+ * </pre>
+ *
+ * @throws Exception
+ * any error
+ */
+ @Test
+ public void blame_merge_correctRegions() throws Exception {
+ RevCommit root, sideA, sideB, mergedTip;
+ try (TestRepository<FileRepository> r = new TestRepository<>(db)) {
+ root = commitAsLines(r, "---");
+ sideA = commitAsLines(r, "aaaa---", root);
+ sideB = commitAsLines(r, "---bbbb", root);
+ mergedTip = commitAsLines(r, "aaaa---bbbb", sideA, sideB);
+ }
+
+ List<EmittedRegion> expectedRegions = Arrays.asList(
+ new EmittedRegion(sideA, 0, 4),
+ new EmittedRegion(root, 4, 7),
+ new EmittedRegion(sideB, 7, 11));
+
+ assertRegions(mergedTip, null, expectedRegions, 11);
+ assertRegions(mergedTip, emptyCache(), expectedRegions, 11);
+ assertRegions(mergedTip, blameAndCache(root), expectedRegions, 11);
+ assertRegions(mergedTip, blameAndCache(sideA), expectedRegions, 11);
+ assertRegions(mergedTip, blameAndCache(sideB), expectedRegions, 11);
+ assertRegions(mergedTip, blameAndCache(mergedTip), expectedRegions, 11);
+ }
+
+ @Test
+ public void blame_merge_cacheUsage() throws Exception {
+ RevCommit root, sideA, sideB, mergedTip;
+ try (TestRepository<FileRepository> r = new TestRepository<>(db)) {
+ root = commitAsLines(r, "---");
+ sideA = commitAsLines(r, "aaaa---", root);
+ sideB = commitAsLines(r, "---bbbb", root);
+ mergedTip = commitAsLines(r, "aaaa---bbbb", sideA, sideB);
+ }
+
+ assertCacheUsage(mergedTip, null, /* cacheUsed */ false,
+ /* candidates */ 4);
+ assertCacheUsage(mergedTip, emptyCache(), false, 4);
+
+ // While splitting unblamed regions to parents, sideA comes first
+ // and gets "aaaa----". Processing is by commit time, so sideB is
+ // explored first
+ assertCacheUsage(mergedTip, blameAndCache(sideA), true, 3);
+ assertCacheUsage(mergedTip, blameAndCache(sideB), true, 4);
+ assertCacheUsage(mergedTip, blameAndCache(root), false, 4);
+ }
+
+ /**
+ * Moving block (insertion)
+ *
+ * <pre>
+ * C1 C2 C3 C3 blame
+ * lines ----------------------------------
+ * L1 | C1 C1 C1 C1
+ * L2 | C1 *C2 C2 C2
+ * L3 | C1 *C3 C3
+ * L4 | C1 C1
+ * </pre>
+ *
+ * @throws Exception
+ * any error
+ */
+ @Test
+ public void blame_movingBlock_correctRegions() throws Exception {
+ RevCommit c1, c2, c3;
+ try (TestRepository<FileRepository> r = new TestRepository<>(db)) {
+ c1 = commit(r, lines("L1C1", "L2C1"));
+ c2 = commit(r, lines("L1C1", "middle", "L2C1"), c1);
+ c3 = commit(r, lines("L1C1", "middle", "extra", "L2C1"), c2);
+ }
+
+ List<EmittedRegion> expectedRegions = Arrays.asList(
+ new EmittedRegion(c1, 0, 1),
+ new EmittedRegion(c2, 1, 2),
+ new EmittedRegion(c3, 2, 3),
+ new EmittedRegion(c1, 3, 4));
+
+ assertRegions(c3, null, expectedRegions, 4);
+ assertRegions(c3, emptyCache(), expectedRegions, 4);
+ assertRegions(c3, blameAndCache(c3), expectedRegions, 4);
+ assertRegions(c3, blameAndCache(c2), expectedRegions, 4);
+ assertRegions(c3, blameAndCache(c1), expectedRegions, 4);
+ }
+
+ @Test
+ public void blame_movingBlock_cacheUsage() throws Exception {
+ RevCommit c1, c2, c3;
+ try (TestRepository<FileRepository> r = new TestRepository<>(db)) {
+ c1 = commitAsLines(r, "root---");
+ c2 = commitAsLines(r, "rootXXX---", c1);
+ c3 = commitAsLines(r, "rootYYYXXX---", c2);
+ }
+
+ assertCacheUsage(c3, null, false, 3);
+ assertCacheUsage(c3, emptyCache(), false, 3);
+ assertCacheUsage(c3, blameAndCache(c3), true, 1);
+ assertCacheUsage(c3, blameAndCache(c2), true, 2);
+ assertCacheUsage(c3, blameAndCache(c1), false, 3);
+ }
+
+ @Test
+ public void blame_cacheOnlyOnChange_unmodifiedInSomeCommits_cacheUsage() throws Exception {
+ String README = "README";
+ String fileC1Content = lines("L1C1", "L2C1", "L3C1");
+ String fileC2Content = lines("L1C1", "L2C1", "L3C1", "L4C2");
+ String fileC3Content = lines("L1C1", "L2C3", "L3C3", "L4C2");
+ String fileC4Content = lines("L1C1", "L2C4", "L3C3", "L4C4");
+
+ RevCommit c1, c2, c3, c4, ni;
+ try (TestRepository<FileRepository> r = new TestRepository<>(db)) {
+ c1 = r.commit().add(FILE, fileC1Content).create();
+ c2 = r.commit().parent(c1).add(FILE, fileC2Content).create();
+ // Keep FILE and edit 100 times README
+ ni = c2;
+ for (int i = 0; i < 100; i++) {
+ ni = r.commit().parent(ni).add(README, "whatever " + i).create();
+ }
+ c3 = r.commit().parent(ni).add(FILE, fileC3Content).create();
+ c4 = r.commit().parent(c3).add(FILE, fileC4Content).create();
+ r.branch("refs/heads/master").update(c4);
+ }
+
+ InMemoryBlameCache empty = emptyCache();
+ assertCacheUsage(c4, empty, false, 104);
+ assertEquals(3, empty.callCount);
+
+ InMemoryBlameCache c4Cached = blameAndCache(c4, FILE);
+ assertCacheUsage(c4, c4Cached, true, 1);
+ assertEquals(1, c4Cached.callCount);
+
+ InMemoryBlameCache c3Cached = blameAndCache(c3, FILE);
+ assertCacheUsage(c4, c3Cached, true, 2);
+ assertEquals(2, c3Cached.callCount);
+
+ // This commit doesn't touch the file, shouldn't check the cache
+ InMemoryBlameCache niCached = blameAndCache(ni, FILE);
+ assertCacheUsage(c4, niCached, false, 104);
+ assertEquals(3, niCached.callCount);
+
+ InMemoryBlameCache c2Cached = blameAndCache(c2, FILE);
+ assertCacheUsage(c4, c2Cached, true, 103);
+ assertEquals(3, c2Cached.callCount);
+
+ // No parents, c1 doesn't need cache.
+ InMemoryBlameCache c1Cached = blameAndCache(c1, FILE);
+ assertCacheUsage(c4, c1Cached, false, 104);
+ assertEquals(3, c1Cached.callCount);
+ }
+
+ @Test
+ public void blame_cacheOnlyOnChange_renameWithoutChange_cacheUsage() throws Exception {
+ String OTHER = "other.txt";
+ String c1Content = lines("L1C1", "L2C1", "L3C1");
+ String c2Content = lines("L1C1", "L2C1", "L3C1", "L4C2");
+
+ RevCommit c1, c2, c3;
+ try (TestRepository<FileRepository> r = new TestRepository<>(db)) {
+ c1 = r.commit().add(OTHER, c1Content).create();
+ c2 = r.commit().parent(c1).add(OTHER, c2Content).create();
+ c3 = r.commit().parent(c2).rm(OTHER).add(FILE, c2Content).create();
+ r.branch("refs/heads/master").update(c3);
+ }
+
+ assertCacheUsage(c3, null, false, 3);
+ assertCacheUsage(c3, emptyCache(), false, 3);
+ assertCacheUsage(c3, blameAndCache(c3, FILE), true, 1);
+ assertCacheUsage(c3, blameAndCache(c2, OTHER), true, 2);
+ assertCacheUsage(c3, blameAndCache(c1, OTHER), false, 3);
+ }
+
+ private void assertRegions(RevCommit commit, InMemoryBlameCache cache,
+ List<EmittedRegion> expectedRegions, int resultLineCount)
+ throws IOException {
+ try (BlameGenerator gen = new BlameGenerator(db, FILE, cache)) {
+ gen.push(null, db.parseCommit(commit));
+ List<EmittedRegion> regions = consume(gen);
+ assertRegionsEquals(expectedRegions, regions);
+ assertAllLinesCovered(/* lines= */ resultLineCount, regions);
+ }
+ }
+
+ private void assertCacheUsage(RevCommit commit, InMemoryBlameCache cache,
+ boolean cacheHit, int candidatesVisited) throws IOException {
+ try (BlameGenerator gen = new BlameGenerator(db, FILE, cache)) {
+ gen.push(null, db.parseCommit(commit));
+ consume(gen);
+ assertEquals(cacheHit, gen.getStats().isCacheHit());
+ assertEquals(candidatesVisited,
+ gen.getStats().getCandidatesVisited());
+ }
+ }
+
+ private static void assertAllLinesCovered(int lines,
+ List<EmittedRegion> regions) {
+ Collections.sort(regions);
+ assertEquals("Starts in first line", 0, regions.get(0).resultStart());
+ for (int i = 1; i < regions.size(); i++) {
+ assertEquals("No gaps", regions.get(i).resultStart(),
+ regions.get(i - 1).resultEnd());
+ }
+ assertEquals("Ends in last line", lines,
+ regions.get(regions.size() - 1).resultEnd());
+ }
+
+ private static void assertRegionsEquals(List<EmittedRegion> expected,
+ List<EmittedRegion> actual) {
+ assertEquals(expected.size(), actual.size());
+ Collections.sort(actual);
+ for (int i = 0; i < expected.size(); i++) {
+ assertEquals(String.format("List differ in element %d", i),
+ expected.get(i), actual.get(i));
+ }
+ }
+
+ private static InMemoryBlameCache emptyCache() {
+ return new InMemoryBlameCache("<empty>");
+ }
+
+ private List<EmittedRegion> consume(BlameGenerator generator)
+ throws IOException {
+ List<EmittedRegion> result = new ArrayList<>();
+ while (generator.next()) {
+ EmittedRegion genRegion = new EmittedRegion(
+ generator.getSourceCommit().toObjectId(),
+ generator.getResultStart(), generator.getResultEnd());
+ result.add(genRegion);
+ }
+ return result;
+ }
+
+ private InMemoryBlameCache blameAndCache(RevCommit commit)
+ throws IOException {
+ return blameAndCache(commit, FILE);
+ }
+
+ private InMemoryBlameCache blameAndCache(RevCommit commit, String path)
+ throws IOException {
+ List<CacheRegion> regions;
+ try (BlameGenerator generator = new BlameGenerator(db, path)) {
+ generator.push(null, commit);
+ regions = consume(generator).stream()
+ .map(EmittedRegion::asCacheRegion)
+ .toList();
+ }
+ InMemoryBlameCache cache = new InMemoryBlameCache("<x>");
+ cache.put(commit, path, regions);
+ return cache;
+ }
+
+ private static RevCommit commitAsLines(TestRepository<?> r,
+ String charPerLine, RevCommit... parents) throws Exception {
+ return commit(r, charPerLine.replaceAll("\\S", "$0\n"), parents);
+ }
+
+ private static RevCommit commit(TestRepository<?> r, String contents,
+ RevCommit... parents) throws Exception {
+ return commit(r, Map.of(FILE, contents), parents);
+ }
+
+ private static RevCommit commit(TestRepository<?> r,
+ Map<String, String> fileContents, RevCommit... parents)
+ throws Exception {
+ TestRepository<?>.CommitBuilder builder = r.commit();
+ for (RevCommit commit : parents) {
+ builder.parent(commit);
+ }
+ fileContents.forEach((path, content) -> {
+ try {
+ builder.add(path, content);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ });
+ return builder.create();
+ }
+
+ private static String lines(String... l) {
+ return join("\n", l);
+ }
+
+ private record EmittedRegion(ObjectId oid, int resultStart, int resultEnd)
+ implements Comparable<EmittedRegion> {
+ @Override
+ public int compareTo(EmittedRegion o) {
+ return resultStart - o.resultStart;
+ }
+
+ CacheRegion asCacheRegion() {
+ return new CacheRegion(FILE, oid, resultStart, resultEnd);
+ }
+ }
+
+ private static class InMemoryBlameCache implements BlameCache {
+
+ private final Map<Key, List<CacheRegion>> cache = new HashMap<>();
+
+ private final String description;
+
+ private int callCount;
+
+ public InMemoryBlameCache(String description) {
+ this.description = description;
+ }
+
+ @Override
+ public List<CacheRegion> get(Repository repo, ObjectId commitId,
+ String path) throws IOException {
+ callCount++;
+ return cache.get(new Key(commitId.name(), path));
+ }
+
+ public void put(ObjectId commitId, String path,
+ List<CacheRegion> cachedRegions) {
+ cache.put(new Key(commitId.name(), path), cachedRegions);
+ }
+
+ @Override
+ public String toString() {
+ return "InMemoryCache: " + description;
+ }
+
+ record Key(String commitId, String path) {
+ }
+ }
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/blame/BlameRegionMergerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/blame/BlameRegionMergerTest.java
new file mode 100644
index 0000000000..1b28676fbf
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/blame/BlameRegionMergerTest.java
@@ -0,0 +1,320 @@
+/*
+ * Copyright (C) 2025, Google LLC.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.blame;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.eclipse.jgit.blame.cache.CacheRegion;
+import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
+public class BlameRegionMergerTest extends RepositoryTestCase {
+
+ private static final ObjectId O1 = ObjectId
+ .fromString("ff6dd8db6edc9aa0ac58fea1d14a55be46c3eb14");
+
+ private static final ObjectId O2 = ObjectId
+ .fromString("c3c7f680c6bee238617f25f6aa85d0b565fc8ecb");
+
+ private static final ObjectId O3 = ObjectId
+ .fromString("29e014aad0399fe8ede7c101d01b6e440ac9966b");
+
+ List<RevCommit> fakeCommits = List.of(new FakeRevCommit(O1),
+ new FakeRevCommit(O2), new FakeRevCommit(O3));
+
+ // In reverse order, so the code doesn't assume a sorted list
+ List<CacheRegion> cachedRegions = List.of(
+ new CacheRegion("README", O3, 20, 30),
+ new CacheRegion("README", O2, 10, 20),
+ new CacheRegion("README", O1, 0, 10));
+
+ BlameRegionMerger blamer = new BlameRegionMergerFakeCommits(fakeCommits,
+ cachedRegions);
+
+ @Test
+ public void intersectRegions_allInside() {
+ Region unblamed = new Region(15, 18, 10);
+ CacheRegion blamed = new CacheRegion("README", O1, 10, 90);
+
+ Region result = BlameRegionMerger.intersectRegions(unblamed, blamed);
+ // Same lines in result and source
+ assertEquals(15, result.resultStart);
+ assertEquals(18, result.sourceStart);
+ assertEquals(10, result.length);
+ assertNull(result.next);
+ }
+
+ @Test
+ public void intersectRegions_startsBefore() {
+ // Intesecting [4, 14) with [10, 90)
+ Region unblamed = new Region(30, 4, 10);
+ CacheRegion blamed = new CacheRegion("README", O1, 10, 90);
+
+ Region result = BlameRegionMerger.intersectRegions(unblamed, blamed);
+
+ // The unblamed region starting at 4 (sourceStart), starts at 30 in the
+ // original file (resultStart). e.g. some commit introduced
+ // lines. If we take the second portion of the region, we need to move
+ // the result start accordingly.
+ assertEquals(36, result.resultStart);
+ assertEquals(10, result.sourceStart);
+ assertEquals(4, result.length);
+ assertNull(result.next);
+ }
+
+ @Test
+ public void intersectRegions_endsAfter() {
+ // Intesecting [85, 95) with [10, 90)
+ Region unblamed = new Region(30, 85, 10);
+ CacheRegion blamed = new CacheRegion("README", O1, 10, 90);
+
+ Region result = BlameRegionMerger.intersectRegions(unblamed, blamed);
+
+ assertEquals(30, result.resultStart);
+ assertEquals(85, result.sourceStart);
+ assertEquals(5, result.length);
+ assertNull(result.next);
+ }
+
+ @Test
+ public void intersectRegions_spillOverBothSides() {
+ // Intesecting [5, 100) with [10, 90)
+ Region unblamed = new Region(30, 5, 95);
+ CacheRegion blamed = new CacheRegion("README", O1, 10, 90);
+
+ Region result = BlameRegionMerger.intersectRegions(unblamed, blamed);
+
+ assertEquals(35, result.resultStart);
+ assertEquals(10, result.sourceStart);
+ assertEquals(80, result.length);
+ assertNull(result.next);
+ }
+
+ @Test
+ public void intersectRegions_exactMatch() {
+ // Intesecting [5, 100) with [10, 90)
+ Region unblamed = new Region(30, 10, 80);
+ CacheRegion blamed = new CacheRegion("README", O1, 10, 90);
+
+ Region result = BlameRegionMerger.intersectRegions(unblamed, blamed);
+
+ assertEquals(30, result.resultStart);
+ assertEquals(10, result.sourceStart);
+ assertEquals(80, result.length);
+ assertNull(result.next);
+ }
+
+ @Test
+ public void findOverlaps_allInside() {
+ Region unblamed = new Region(0, 11, 4);
+ List<CacheRegion> overlaps = blamer.findOverlaps(unblamed);
+ assertEquals(1, overlaps.size());
+ assertEquals(10, overlaps.get(0).getStart());
+ assertEquals(20, overlaps.get(0).getEnd());
+ }
+
+ @Test
+ public void findOverlaps_overTwoRegions() {
+ Region unblamed = new Region(0, 8, 4);
+ List<CacheRegion> overlaps = blamer.findOverlaps(unblamed);
+ assertEquals(2, overlaps.size());
+ assertEquals(0, overlaps.get(0).getStart());
+ assertEquals(10, overlaps.get(0).getEnd());
+ assertEquals(10, overlaps.get(1).getStart());
+ assertEquals(20, overlaps.get(1).getEnd());
+ }
+
+ @Test
+ public void findOverlaps_overThreeRegions() {
+ Region unblamed = new Region(0, 8, 15);
+ List<CacheRegion> overlaps = blamer.findOverlaps(unblamed);
+ assertEquals(3, overlaps.size());
+ assertEquals(0, overlaps.get(0).getStart());
+ assertEquals(10, overlaps.get(0).getEnd());
+ assertEquals(10, overlaps.get(1).getStart());
+ assertEquals(20, overlaps.get(1).getEnd());
+ assertEquals(20, overlaps.get(2).getStart());
+ assertEquals(30, overlaps.get(2).getEnd());
+ }
+
+ @Test
+ public void blame_exactOverlap() throws IOException {
+ Region unblamed = new Region(0, 10, 10);
+ List<Candidate> blamed = blamer.mergeOneRegion(unblamed);
+
+ assertEquals(1, blamed.size());
+ Candidate c = blamed.get(0);
+ assertEquals(c.sourceCommit.name(), O2.name());
+ assertEquals(c.regionList.resultStart, unblamed.resultStart);
+ assertEquals(c.regionList.sourceStart, unblamed.sourceStart);
+ assertEquals(10, c.regionList.length);
+ assertNull(c.regionList.next);
+ }
+
+ @Test
+ public void blame_corruptedIndex() {
+ Region outOfRange = new Region(0, 43, 4);
+ // This region is out of the blamed area
+ assertThrows(IOException.class,
+ () -> blamer.mergeOneRegion(outOfRange));
+ }
+
+ @Test
+ public void blame_allInsideOneBlamedRegion() throws IOException {
+ Region unblamed = new Region(0, 5, 3);
+ // This region if fully blamed to O1
+ List<Candidate> blamed = blamer.mergeOneRegion(unblamed);
+ assertEquals(1, blamed.size());
+ Candidate c = blamed.get(0);
+ assertEquals(c.sourceCommit.name(), O1.name());
+ assertEquals(c.regionList.resultStart, unblamed.resultStart);
+ assertEquals(c.regionList.sourceStart, unblamed.sourceStart);
+ assertEquals(3, c.regionList.length);
+ assertNull(c.regionList.next);
+ }
+
+ @Test
+ public void blame_overTwoBlamedRegions() throws IOException {
+ Region unblamed = new Region(0, 8, 5);
+ // (8, 10) belongs go C1, (10, 13) to C2
+ List<Candidate> blamed = blamer.mergeOneRegion(unblamed);
+ assertEquals(2, blamed.size());
+ Candidate c = blamed.get(0);
+ assertEquals(c.sourceCommit.name(), O1.name());
+ assertEquals(unblamed.resultStart, c.regionList.resultStart);
+ assertEquals(unblamed.sourceStart, c.regionList.sourceStart);
+ assertEquals(2, c.regionList.length);
+ assertNull(c.regionList.next);
+
+ c = blamed.get(1);
+ assertEquals(c.sourceCommit.name(), O2.name());
+ assertEquals(2, c.regionList.resultStart);
+ assertEquals(10, c.regionList.sourceStart);
+ assertEquals(3, c.regionList.length);
+ assertNull(c.regionList.next);
+ }
+
+ @Test
+ public void blame_all() throws IOException {
+ Region unblamed = new Region(0, 0, 30);
+ List<Candidate> blamed = blamer.mergeOneRegion(unblamed);
+ assertEquals(3, blamed.size());
+ Candidate c = blamed.get(0);
+ assertEquals(c.sourceCommit.name(), O1.name());
+ assertEquals(unblamed.resultStart, c.regionList.resultStart);
+ assertEquals(unblamed.sourceStart, c.regionList.sourceStart);
+ assertEquals(10, c.regionList.length);
+ assertNull(c.regionList.next);
+
+ c = blamed.get(1);
+ assertEquals(c.sourceCommit.name(), O2.name());
+ assertEquals(10, c.regionList.resultStart);
+ assertEquals(10, c.regionList.sourceStart);
+ assertEquals(10, c.regionList.length);
+ assertNull(c.regionList.next);
+
+ c = blamed.get(2);
+ assertEquals(c.sourceCommit.name(), O3.name());
+ assertEquals(20, c.regionList.resultStart);
+ assertEquals(20, c.regionList.sourceStart);
+ assertEquals(10, c.regionList.length);
+ assertNull(c.regionList.next);
+ }
+
+ @Test
+ public void blame_fromCandidate() {
+ // We don't use anything from the candidate besides the
+ // regionList
+ Candidate c = new Candidate(null, null, null);
+ c.regionList = new Region(0, 8, 5);
+ c.regionList.next = new Region(22, 22, 4);
+
+ Candidate blamed = blamer.mergeCandidate(c);
+ // Three candidates
+ assertNotNull(blamed);
+ assertNotNull(blamed.queueNext);
+ assertNotNull(blamed.queueNext.queueNext);
+ assertNull(blamed.queueNext.queueNext.queueNext);
+
+ assertEquals(O1.name(), blamed.sourceCommit.name());
+
+ Candidate second = blamed.queueNext;
+ assertEquals(O2.name(), second.sourceCommit.name());
+
+ Candidate third = blamed.queueNext.queueNext;
+ assertEquals(O3.name(), third.sourceCommit.name());
+ }
+
+ @Test
+ public void blame_fromCandidate_twiceCandidateInOutput() {
+ Candidate c = new Candidate(null, null, null);
+ // This produces O1 and O2
+ c.regionList = new Region(0, 8, 5);
+ // This produces O2 and O3
+ c.regionList.next = new Region(20, 15, 7);
+
+ Candidate blamed = blamer.mergeCandidate(c);
+ assertCandidateSingleRegion(O1, 2, blamed);
+ blamed = blamed.queueNext;
+ assertCandidateSingleRegion(O2, 3, blamed);
+ // We do not merge candidates afterwards, so these are
+ // two different candidates to the same source
+ blamed = blamed.queueNext;
+ assertCandidateSingleRegion(O2, 5, blamed);
+ blamed = blamed.queueNext;
+ assertCandidateSingleRegion(O3, 2, blamed);
+ assertNull(blamed.queueNext);
+ }
+
+ private static void assertCandidateSingleRegion(ObjectId expectedOid,
+ int expectedLength, Candidate actual) {
+ assertNotNull("candidate", actual);
+ assertNotNull("region list not empty", actual.regionList);
+ assertNull("region list has only one element", actual.regionList.next);
+ assertEquals(expectedOid, actual.sourceCommit);
+ assertEquals(expectedLength, actual.regionList.length);
+ }
+
+ private static final class BlameRegionMergerFakeCommits
+ extends BlameRegionMerger {
+
+ private final Map<ObjectId, RevCommit> cache;
+
+ BlameRegionMergerFakeCommits(List<RevCommit> commits,
+ List<CacheRegion> blamedRegions) {
+ super(null, null, blamedRegions);
+ cache = commits.stream().collect(Collectors
+ .toMap(RevCommit::toObjectId, Function.identity()));
+ }
+
+ @Override
+ protected RevCommit parse(ObjectId oid) {
+ return cache.get(oid);
+ }
+ }
+
+ private static final class FakeRevCommit extends RevCommit {
+ FakeRevCommit(AnyObjectId id) {
+ super(id);
+ }
+ }
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/DiffFormatterBuiltInDriverTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/DiffFormatterBuiltInDriverTest.java
new file mode 100644
index 0000000000..1352871983
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/DiffFormatterBuiltInDriverTest.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (c) 2024 Qualcomm Innovation Center, 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 v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.diff;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.stream.Collectors;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.junit.JGitTestUtil;
+import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.treewalk.CanonicalTreeParser;
+import org.junit.Test;
+
+public class DiffFormatterBuiltInDriverTest extends RepositoryTestCase {
+ @Test
+ public void testCppDriver() throws Exception {
+ String fileName = "greeting.c";
+ String body = Files.readString(
+ Path.of(JGitTestUtil.getTestResourceFile(fileName)
+ .getAbsolutePath()));
+ RevCommit c1;
+ RevCommit c2;
+ try (Git git = new Git(db)) {
+ createCommit(git, ".gitattributes", "*.c diff=cpp");
+ c1 = createCommit(git, fileName, body);
+ c2 = createCommit(git, fileName,
+ body.replace("Good day", "Greetings")
+ .replace("baz", "qux"));
+ }
+ try (ByteArrayOutputStream os = new ByteArrayOutputStream();
+ DiffFormatter diffFormatter = new DiffFormatter(os)) {
+ String actual = getHunkHeaders(c1, c2, os, diffFormatter);
+ String expected =
+ "@@ -27,7 +27,7 @@ void getPersonalizedGreeting(char *result, const char *name, const char *timeOfD\n"
+ + "@@ -37,7 +37,7 @@ int main() {";
+ assertEquals(expected, actual);
+ }
+ }
+
+ @Test
+ public void testDtsDriver() throws Exception {
+ String fileName = "sample.dtsi";
+ String body = Files.readString(
+ Path.of(JGitTestUtil.getTestResourceFile(fileName)
+ .getAbsolutePath()));
+ RevCommit c1;
+ RevCommit c2;
+ try (Git git = new Git(db)) {
+ createCommit(git, ".gitattributes", "*.dtsi diff=dts");
+ c1 = createCommit(git, fileName, body);
+ c2 = createCommit(git, fileName,
+ body.replace("clock-frequency = <24000000>",
+ "clock-frequency = <48000000>"));
+ }
+ try (ByteArrayOutputStream os = new ByteArrayOutputStream();
+ DiffFormatter diffFormatter = new DiffFormatter(os)) {
+ String actual = getHunkHeaders(c1, c2, os, diffFormatter);
+ String expected = "@@ -20,6 +20,6 @@ uart0: uart@101f1000 {";
+ assertEquals(expected, actual);
+ }
+ }
+
+ @Test
+ public void testJavaDriver() throws Exception {
+ String resourceName = "greeting.javasource";
+ String body = Files.readString(
+ Path.of(JGitTestUtil.getTestResourceFile(resourceName)
+ .getAbsolutePath()));
+ RevCommit c1;
+ RevCommit c2;
+ try (Git git = new Git(db)) {
+ createCommit(git, ".gitattributes", "*.java diff=java");
+ String fileName = "Greeting.java";
+ c1 = createCommit(git, fileName, body);
+ c2 = createCommit(git, fileName,
+ body.replace("Good day", "Greetings")
+ .replace("baz", "qux"));
+ }
+ try (ByteArrayOutputStream os = new ByteArrayOutputStream();
+ DiffFormatter diffFormatter = new DiffFormatter(os)) {
+ String actual = getHunkHeaders(c1, c2, os, diffFormatter);
+ String expected =
+ "@@ -22,7 +22,7 @@ public String getPersonalizedGreeting(String name, String timeOfDay) {\n"
+ + "@@ -32,6 +32,6 @@ public static void main(String[] args) {";
+ assertEquals(expected, actual);
+ }
+ }
+
+ @Test
+ public void testPythonDriver() throws Exception {
+ String fileName = "greeting.py";
+ String body = Files.readString(
+ Path.of(JGitTestUtil.getTestResourceFile(fileName)
+ .getAbsolutePath()));
+ RevCommit c1;
+ RevCommit c2;
+ try (Git git = new Git(db)) {
+ createCommit(git, ".gitattributes", "*.py diff=python");
+ c1 = createCommit(git, fileName, body);
+ c2 = createCommit(git, fileName,
+ body.replace("Good day", "Greetings"));
+ }
+ try (ByteArrayOutputStream os = new ByteArrayOutputStream();
+ DiffFormatter diffFormatter = new DiffFormatter(os)) {
+ String actual = getHunkHeaders(c1, c2, os, diffFormatter);
+ String expected = "@@ -16,7 +16,7 @@ def get_personalized_greeting(self, name, time_of_day):";
+ assertEquals(expected, actual);
+ }
+ }
+
+ @Test
+ public void testRustDriver() throws Exception {
+ String fileName = "greeting.rs";
+ String body = Files.readString(
+ Path.of(JGitTestUtil.getTestResourceFile(fileName)
+ .getAbsolutePath()));
+ RevCommit c1;
+ RevCommit c2;
+ try (Git git = new Git(db)) {
+ createCommit(git, ".gitattributes", "*.rs diff=rust");
+ c1 = createCommit(git, fileName, body);
+ c2 = createCommit(git, fileName,
+ body.replace("Good day", "Greetings")
+ .replace("baz", "qux"));
+ }
+ try (ByteArrayOutputStream os = new ByteArrayOutputStream();
+ DiffFormatter diffFormatter = new DiffFormatter(os)) {
+ String actual = getHunkHeaders(c1, c2, os, diffFormatter);
+ String expected =
+ "@@ -14,7 +14,7 @@ fn get_personalized_greeting(&self, name: &str, time_of_day: &str) -> String {\n"
+ + "@@ -23,5 +23,5 @@ fn main() {";
+ assertEquals(expected, actual);
+ }
+ }
+
+ private String getHunkHeaders(RevCommit c1, RevCommit c2,
+ ByteArrayOutputStream os, DiffFormatter diffFormatter)
+ throws IOException {
+ diffFormatter.setRepository(db);
+ diffFormatter.format(new CanonicalTreeParser(null, db.newObjectReader(),
+ c1.getTree()),
+ new CanonicalTreeParser(null, db.newObjectReader(),
+ c2.getTree()));
+ diffFormatter.flush();
+ return Arrays.stream(os.toString(StandardCharsets.UTF_8).split("\n"))
+ .filter(line -> line.startsWith("@@"))
+ .collect(Collectors.joining("\n"));
+ }
+
+ private RevCommit createCommit(Git git, String fileName, String body)
+ throws IOException, GitAPIException {
+ writeTrashFile(fileName, body);
+ git.add().addFilepattern(".").call();
+ return git.commit().setMessage("message").call();
+ }
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/EditListTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/EditListTest.java
index f657bab771..a2c20aaaba 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/EditListTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/EditListTest.java
@@ -28,7 +28,7 @@ public class EditListTest {
assertTrue(l.isEmpty());
assertEquals("EditList[]", l.toString());
- assertEquals(l, l);
+ assertTrue(l.equals(l));
assertEquals(new EditList(), l);
assertFalse(l.equals(""));
assertEquals(l.hashCode(), new EditList().hashCode());
@@ -44,7 +44,7 @@ public class EditListTest {
assertSame(e, l.get(0));
assertSame(e, l.iterator().next());
- assertEquals(l, l);
+ assertTrue(l.equals(l));
assertFalse(l.equals(new EditList()));
final EditList l2 = new EditList();
@@ -69,7 +69,7 @@ public class EditListTest {
assertSame(e1, i.next());
assertSame(e2, i.next());
- assertEquals(l, l);
+ assertTrue(l.equals(l));
assertFalse(l.equals(new EditList()));
final EditList l2 = new EditList();
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/EditTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/EditTest.java
index 8ab9bb12de..86c6d77cc6 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/EditTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/EditTest.java
@@ -98,7 +98,7 @@ public class EditTest {
final Edit e1 = new Edit(1, 2, 3, 4);
final Edit e2 = new Edit(1, 2, 3, 4);
- assertEquals(e1, e1);
+ assertTrue(e1.equals(e1));
assertEquals(e2, e1);
assertEquals(e1, e2);
assertEquals(e1.hashCode(), e2.hashCode());
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheBuilderTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheBuilderTest.java
index 703d68b37c..61801106af 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheBuilderTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheBuilderTest.java
@@ -218,7 +218,7 @@ public class DirCacheBuilderTest extends RepositoryTestCase {
try {
b.commit();
} catch (ReceivedEventMarkerException e) {
- fail("unexpected IndexChangedEvent");
+ throw new AssertionError("unexpected IndexChangedEvent", e);
}
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java
index 8e84dfa318..01d1e0282d 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCacheEntryTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2009, 2020 Google Inc. and others
+ * Copyright (C) 2009, 2023 Google Inc. and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -46,6 +46,16 @@ public class DirCacheEntryTest {
assertFalse(isValidPath("a/"));
assertFalse(isValidPath("ab/cd/ef/"));
assertFalse(isValidPath("a\u0000b"));
+ assertFalse(isValidPath(".git"));
+ assertFalse(isValidPath(".GIT"));
+ assertFalse(isValidPath(".Git"));
+ assertFalse(isValidPath(".git/b"));
+ assertFalse(isValidPath(".GIT/b"));
+ assertFalse(isValidPath(".Git/b"));
+ assertFalse(isValidPath("x/y/.git/z/b"));
+ assertFalse(isValidPath("x/y/.GIT/z/b"));
+ assertFalse(isValidPath("x/y/.Git/z/b"));
+ assertTrue(isValidPath("git/b"));
}
@SuppressWarnings("unused")
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/InvalidPathCheckoutTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/InvalidPathCheckoutTest.java
new file mode 100644
index 0000000000..e3bc85a512
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/InvalidPathCheckoutTest.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2023 Thomas Wolf <twolf@apache.org> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.dircache;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+
+import java.io.File;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.ResetCommand.ResetType;
+import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevBlob;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
+/**
+ * Tests for checking out with invalid paths.
+ */
+public class InvalidPathCheckoutTest extends RepositoryTestCase {
+
+ private DirCacheEntry brokenEntry(String fileName, RevBlob blob) {
+ DirCacheEntry entry = new DirCacheEntry("XXXX/" + fileName);
+ entry.path[0] = '.';
+ entry.path[1] = 'g';
+ entry.path[2] = 'i';
+ entry.path[3] = 't';
+ entry.setFileMode(FileMode.REGULAR_FILE);
+ entry.setObjectId(blob);
+ return entry;
+ }
+
+ @Test
+ public void testCheckoutIntoDotGit() throws Exception {
+ try (TestRepository<Repository> repo = new TestRepository<>(db)) {
+ db.incrementOpen();
+ // DirCacheEntry does not allow any path component to contain
+ // ".git". C git also forbids this. But what if somebody creates
+ // such an entry explicitly?
+ RevCommit base = repo
+ .commit(repo.tree(brokenEntry("b", repo.blob("test"))));
+ try (Git git = new Git(db)) {
+ assertThrows(InvalidPathException.class, () -> git.reset()
+ .setMode(ResetType.HARD).setRef(base.name()).call());
+ File b = new File(new File(trash, ".git"), "b");
+ assertFalse(".git/b should not exist", b.exists());
+ }
+ }
+ }
+
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/BareSuperprojectWriterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/BareSuperprojectWriterTest.java
index c3b93879b2..5065b57840 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/BareSuperprojectWriterTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/BareSuperprojectWriterTest.java
@@ -12,6 +12,7 @@ package org.eclipse.jgit.gitrepo;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -22,6 +23,7 @@ import java.util.List;
import org.eclipse.jgit.gitrepo.BareSuperprojectWriter.BareWriterConfig;
import org.eclipse.jgit.gitrepo.RepoCommand.RemoteReader;
import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
@@ -68,6 +70,49 @@ public class BareSuperprojectWriterTest extends RepositoryTestCase {
}
@Test
+ public void write_setGitModulesContents_pinned() throws Exception {
+ try (Repository bareRepo = createBareRepository()) {
+ RepoProject pinWithUpstream = new RepoProject("pinWithUpstream",
+ "path/x", "cbc0fae7e1911d27e1de37d364698dba4411c78b",
+ "remote", "");
+ pinWithUpstream.setUrl("http://example.com/a");
+ pinWithUpstream.setUpstream("branchX");
+
+ RepoProject pinWithoutUpstream = new RepoProject(
+ "pinWithoutUpstream", "path/y",
+ "cbc0fae7e1911d27e1de37d364698dba4411c78b", "remote", "");
+ pinWithoutUpstream.setUrl("http://example.com/b");
+
+ RemoteReader mockRemoteReader = mock(RemoteReader.class);
+
+ BareSuperprojectWriter w = new BareSuperprojectWriter(bareRepo,
+ null, "refs/heads/master", author, mockRemoteReader,
+ BareWriterConfig.getDefault(), List.of());
+
+ RevCommit commit = w
+ .write(Arrays.asList(pinWithUpstream, pinWithoutUpstream));
+
+ String contents = readContents(bareRepo, commit, ".gitmodules");
+ Config cfg = new Config();
+ cfg.fromText(contents);
+
+ assertThat(cfg.getString("submodule", "pinWithUpstream", "path"),
+ is("path/x"));
+ assertThat(cfg.getString("submodule", "pinWithUpstream", "url"),
+ is("http://example.com/a"));
+ assertThat(cfg.getString("submodule", "pinWithUpstream", "ref"),
+ is("branchX"));
+
+ assertThat(cfg.getString("submodule", "pinWithoutUpstream", "path"),
+ is("path/y"));
+ assertThat(cfg.getString("submodule", "pinWithoutUpstream", "url"),
+ is("http://example.com/b"));
+ assertThat(cfg.getString("submodule", "pinWithoutUpstream", "ref"),
+ nullValue());
+ }
+ }
+
+ @Test
public void write_setExtraContents() throws Exception {
try (Repository bareRepo = createBareRepository()) {
RepoProject repoProject = new RepoProject("subprojectX", "path/to",
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/ManifestParserTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/ManifestParserTest.java
index 20958a812c..0949d040e9 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/ManifestParserTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/ManifestParserTest.java
@@ -11,14 +11,21 @@ package org.eclipse.jgit.gitrepo;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.ByteArrayInputStream;
+import java.io.File;
import java.io.IOException;
import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.StandardOpenOption;
import java.util.HashSet;
+import java.util.Map;
import java.util.Set;
+import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -138,6 +145,72 @@ public class ManifestParserTest {
.collect(Collectors.toSet()));
}
+ @Test
+ public void testPinProjectWithUpstream() throws Exception {
+ StringBuilder xmlContent = new StringBuilder();
+ xmlContent.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
+ .append("<manifest>")
+ .append("<remote name=\"remote1\" fetch=\".\" />")
+ .append("<default revision=\"master\" remote=\"remote1\" />")
+ .append("<project path=\"foo\" name=\"pin-with-upstream\"")
+ .append(" revision=\"9b2fe85c0279f4d5ac69f07ddcd48566c3555405\"")
+ .append(" upstream=\"branchX\"/>")
+ .append("<project path=\"bar\" name=\"pin-without-upstream\"")
+ .append(" revision=\"76ce6d91a2e07fdfcbfc8df6970c9e98a98e36a0\" />")
+ .append("</manifest>");
+
+ ManifestParser parser = new ManifestParser(null, null, "master",
+ "https://git.google.com/", null, null);
+ parser.read(new ByteArrayInputStream(
+ xmlContent.toString().getBytes(UTF_8)));
+
+ Map<String, RepoProject> repos = parser.getProjects().stream().collect(
+ Collectors.toMap(RepoProject::getName, Function.identity()));
+ assertEquals(2, repos.size());
+
+ RepoProject foo = repos.get("pin-with-upstream");
+ assertEquals("pin-with-upstream", foo.getName());
+ assertEquals("9b2fe85c0279f4d5ac69f07ddcd48566c3555405",
+ foo.getRevision());
+ assertEquals("branchX", foo.getUpstream());
+
+ RepoProject bar = repos.get("pin-without-upstream");
+ assertEquals("pin-without-upstream", bar.getName());
+ assertEquals("76ce6d91a2e07fdfcbfc8df6970c9e98a98e36a0",
+ bar.getRevision());
+ assertNull(bar.getUpstream());
+ }
+
+ @Test
+ public void testWithDestBranch() throws Exception {
+ StringBuilder xmlContent = new StringBuilder();
+ xmlContent.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
+ .append("<manifest>")
+ .append("<remote name=\"remote1\" fetch=\".\" />")
+ .append("<default revision=\"master\" remote=\"remote1\" />")
+ .append("<project path=\"foo\" name=\"foo\"")
+ .append(" dest-branch=\"branchX\"/>")
+ .append("<project path=\"bar\" name=\"bar\"/>")
+ .append("</manifest>");
+
+ ManifestParser parser = new ManifestParser(null, null, "master",
+ "https://git.google.com/", null, null);
+ parser.read(new ByteArrayInputStream(
+ xmlContent.toString().getBytes(UTF_8)));
+
+ Map<String, RepoProject> repos = parser.getProjects().stream().collect(
+ Collectors.toMap(RepoProject::getName, Function.identity()));
+ assertEquals(2, repos.size());
+
+ RepoProject foo = repos.get("foo");
+ assertEquals("foo", foo.getName());
+ assertEquals("branchX", foo.getDestBranch());
+
+ RepoProject bar = repos.get("bar");
+ assertEquals("bar", bar.getName());
+ assertNull(bar.getDestBranch());
+ }
+
void testNormalize(String in, String want) {
URI got = ManifestParser.normalizeEmptyPath(URI.create(in));
if (!got.toString().equals(want)) {
@@ -152,4 +225,33 @@ public class ManifestParserTest {
testNormalize("", "");
testNormalize("a/b", "a/b");
}
+
+ @Test
+ public void testXXE() throws Exception {
+ File externalEntity = File.createTempFile("injected", "xml");
+ externalEntity.deleteOnExit();
+ Files.write(externalEntity.toPath(),
+ "<evil>injected xml</evil>"
+ .getBytes(UTF_8),
+ StandardOpenOption.WRITE);
+ String baseUrl = "https://git.google.com/";
+ StringBuilder xmlContent = new StringBuilder();
+ xmlContent.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
+ .append("<!DOCTYPE booo [ <!ENTITY foobar SYSTEM \"")
+ .append(externalEntity.getPath()).append("\"> ]>\n")
+ .append("<manifest>")
+ .append("<remote name=\"remote1\" fetch=\".\" />")
+ .append("<default revision=\"master\" remote=\"remote1\" />")
+ .append("&foobar;")
+ .append("<project path=\"foo\" name=\"foo\" groups=\"a,test\" />")
+ .append("</manifest>");
+
+ IOException e = assertThrows(IOException.class,
+ () -> new ManifestParser(null, null, "master", baseUrl, null,
+ null)
+ .read(new ByteArrayInputStream(
+ xmlContent.toString().getBytes(UTF_8))));
+ assertTrue(e.getCause().getMessage().contains("DOCTYPE"));
+ }
+
}
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 ca6f2e1053..3162e7910b 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
@@ -1171,6 +1171,94 @@ public class RepoCommandTest extends RepositoryTestCase {
}
}
+ @Test
+ public void testRecordRemoteBranch_pinned() throws Exception {
+ Repository remoteDb = createBareRepository();
+ Repository tempDb = createWorkRepository();
+
+ StringBuilder xmlContent = new StringBuilder();
+ xmlContent.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
+ .append("<manifest>")
+ .append("<remote name=\"remote1\" fetch=\".\" />")
+ .append("<default revision=\"master\" remote=\"remote1\" />")
+ .append("<project path=\"pin-noupstream\"")
+ .append(" name=\"pin-noupstream\"")
+ .append(" revision=\"76ce6d91a2e07fdfcbfc8df6970c9e98a98e36a0\" />")
+ .append("<project path=\"pin-upstream\"")
+ .append(" name=\"pin-upstream\"")
+ .append(" upstream=\"branchX\"")
+ .append(" revision=\"76ce6d91a2e07fdfcbfc8df6970c9e98a98e36a0\" />")
+ .append("</manifest>");
+ JGitTestUtil.writeTrashFile(tempDb, "manifest.xml",
+ xmlContent.toString());
+
+ RepoCommand command = new RepoCommand(remoteDb);
+ command.setPath(
+ tempDb.getWorkTree().getAbsolutePath() + "/manifest.xml")
+ .setURI(rootUri).setRecordRemoteBranch(true).call();
+ // Clone it
+ File directory = createTempDirectory("testBareRepo");
+ try (Repository localDb = Git.cloneRepository().setDirectory(directory)
+ .setURI(remoteDb.getDirectory().toURI().toString()).call()
+ .getRepository();) {
+ // The .gitmodules file should exist
+ File gitmodules = new File(localDb.getWorkTree(), ".gitmodules");
+ assertTrue("The .gitmodules file should exist",
+ gitmodules.exists());
+ FileBasedConfig c = new FileBasedConfig(gitmodules, FS.DETECTED);
+ c.load();
+ assertEquals("Pinned submodule with upstream records the ref",
+ "branchX", c.getString("submodule", "pin-upstream", "ref"));
+ assertNull("Pinned submodule without upstream don't have ref",
+ c.getString("submodule", "pin-noupstream", "ref"));
+ }
+ }
+
+ @Test
+ public void testRecordRemoteBranch_pinned_nameConflict() throws Exception {
+ Repository remoteDb = createBareRepository();
+ Repository tempDb = createWorkRepository();
+
+ StringBuilder xmlContent = new StringBuilder();
+ xmlContent.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
+ .append("<manifest>")
+ .append("<remote name=\"remote1\" fetch=\".\" />")
+ .append("<default revision=\"master\" remote=\"remote1\" />")
+ .append("<project path=\"pin-upstream\"")
+ .append(" name=\"pin-upstream\"")
+ .append(" upstream=\"branchX\"")
+ .append(" revision=\"76ce6d91a2e07fdfcbfc8df6970c9e98a98e36a0\" />")
+ .append("<project path=\"pin-upstream-name-conflict\"")
+ .append(" name=\"pin-upstream\"")
+ .append(" upstream=\"branchX\"")
+ .append(" revision=\"76ce6d91a2e07fdfcbfc8df6970c9e98a98e36a0\" />")
+ .append("</manifest>");
+ JGitTestUtil.writeTrashFile(tempDb, "manifest.xml",
+ xmlContent.toString());
+
+ RepoCommand command = new RepoCommand(remoteDb);
+ command.setPath(
+ tempDb.getWorkTree().getAbsolutePath() + "/manifest.xml")
+ .setURI(rootUri).setRecordRemoteBranch(true).call();
+ // Clone it
+ File directory = createTempDirectory("testBareRepo");
+ try (Repository localDb = Git.cloneRepository().setDirectory(directory)
+ .setURI(remoteDb.getDirectory().toURI().toString()).call()
+ .getRepository();) {
+ // The .gitmodules file should exist
+ File gitmodules = new File(localDb.getWorkTree(), ".gitmodules");
+ assertTrue("The .gitmodules file should exist",
+ gitmodules.exists());
+ FileBasedConfig c = new FileBasedConfig(gitmodules, FS.DETECTED);
+ c.load();
+ assertEquals("Upstream is preserved in name conflict", "branchX",
+ c.getString("submodule", "pin-upstream/pin-upstream",
+ "ref"));
+ assertEquals("Upstream is preserved in name conflict (other side)",
+ "branchX", c.getString("submodule",
+ "pin-upstream/pin-upstream-name-conflict", "ref"));
+ }
+ }
@Test
public void testRecordSubmoduleLabels() throws Exception {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/BasicRuleTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/BasicRuleTest.java
index 6112952549..6983eaa354 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/BasicRuleTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/BasicRuleTest.java
@@ -31,7 +31,7 @@ public class BasicRuleTest {
assertFalse(rule1.getNegation());
assertTrue(rule3.getNegation());
assertNotEquals(rule1, null);
- assertEquals(rule1, rule1);
+ assertTrue(rule1.equals(rule1));
assertEquals(rule1, rule2);
assertNotEquals(rule1, rule3);
assertNotEquals(rule1, rule4);
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 05a953e081..ab08c99796 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
@@ -20,14 +20,18 @@ import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
+import org.eclipse.jgit.junit.MockSystemReader;
import org.eclipse.jgit.junit.RepositoryTestCase;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.treewalk.FileTreeIterator;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.WorkingTreeIterator;
+import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.FileUtils;
import org.eclipse.jgit.util.SystemReader;
import org.junit.After;
@@ -698,6 +702,110 @@ public class IgnoreNodeTest extends RepositoryTestCase {
}
@Test
+ public void testUserGitIgnoreFound() throws IOException {
+ File homeDir = FS.DETECTED.userHome();
+ Path userIgnore = homeDir.toPath().resolve(".config").resolve("git")
+ .resolve("ignore");
+ Files.createDirectories(userIgnore.getParent());
+ Files.writeString(userIgnore, "x");
+ try {
+ writeTrashFile(".foo", "");
+ writeTrashFile("a/x/file", "");
+ writeTrashFile("b/x", "");
+ writeTrashFile("x/file", "");
+
+ beginWalk();
+ assertEntry(F, tracked, ".foo");
+ assertEntry(D, tracked, "a");
+ assertEntry(D, ignored, "a/x");
+ assertEntry(F, ignored, "a/x/file");
+ assertEntry(D, tracked, "b");
+ assertEntry(F, ignored, "b/x");
+ assertEntry(D, ignored, "x");
+ assertEntry(F, ignored, "x/file");
+ endWalk();
+ } finally {
+ Files.deleteIfExists(userIgnore);
+ }
+ }
+
+ @Test
+ public void testXdgIgnoreFound() throws IOException {
+ File tmp = getTemporaryDirectory();
+ Path xdg = tmp.toPath().resolve("xdg");
+ Path userIgnore = xdg.resolve("git").resolve("ignore");
+ Files.createDirectories(userIgnore.getParent());
+ Files.writeString(userIgnore, "x");
+ SystemReader system = SystemReader.getInstance();
+ assertTrue(system instanceof MockSystemReader);
+ ((MockSystemReader) system).setProperty("XDG_CONFIG_HOME",
+ xdg.toAbsolutePath().toString());
+ // Also create the one in the home directory -- it should not be active
+ File homeDir = FS.DETECTED.userHome();
+ Path userIgnore2 = homeDir.toPath().resolve(".config").resolve("git")
+ .resolve("ignore");
+ Files.createDirectories(userIgnore2.getParent());
+ Files.writeString(userIgnore2, "a");
+ try {
+ writeTrashFile(".foo", "");
+ writeTrashFile("a/x/file", "");
+ writeTrashFile("b/x", "");
+ writeTrashFile("x/file", "");
+
+ beginWalk();
+ assertEntry(F, tracked, ".foo");
+ assertEntry(D, tracked, "a");
+ assertEntry(D, ignored, "a/x");
+ assertEntry(F, ignored, "a/x/file");
+ assertEntry(D, tracked, "b");
+ assertEntry(F, ignored, "b/x");
+ assertEntry(D, ignored, "x");
+ assertEntry(F, ignored, "x/file");
+ endWalk();
+ } finally {
+ ((MockSystemReader) system).setProperty("XDG_CONFIG_HOME", null);
+ Files.deleteIfExists(userIgnore2);
+ }
+ }
+
+ @Test
+ public void testXdgWrong() throws IOException {
+ File tmp = getTemporaryDirectory();
+ Path xdg = tmp.toPath().resolve("xdg");
+ SystemReader system = SystemReader.getInstance();
+ assertTrue(system instanceof MockSystemReader);
+ // Valid value, but the directory doesn't exist
+ ((MockSystemReader) system).setProperty("XDG_CONFIG_HOME",
+ xdg.toAbsolutePath().toString());
+ // Also create the one in the home directory -- it should not be active
+ File homeDir = FS.DETECTED.userHome();
+ Path userIgnore2 = homeDir.toPath().resolve(".config").resolve("git")
+ .resolve("ignore");
+ Files.createDirectories(userIgnore2.getParent());
+ Files.writeString(userIgnore2, "x");
+ try {
+ writeTrashFile(".foo", "");
+ writeTrashFile("a/x/file", "");
+ writeTrashFile("b/x", "");
+ writeTrashFile("x/file", "");
+
+ beginWalk();
+ assertEntry(F, tracked, ".foo");
+ assertEntry(D, tracked, "a");
+ assertEntry(D, tracked, "a/x");
+ assertEntry(F, tracked, "a/x/file");
+ assertEntry(D, tracked, "b");
+ assertEntry(F, tracked, "b/x");
+ assertEntry(D, tracked, "x");
+ assertEntry(F, tracked, "x/file");
+ endWalk();
+ } finally {
+ ((MockSystemReader) system).setProperty("XDG_CONFIG_HOME", null);
+ Files.deleteIfExists(userIgnore2);
+ }
+ }
+
+ @Test
public void testToString() throws Exception {
assertEquals(Arrays.asList("").toString(), new IgnoreNode().toString());
assertEquals(Arrays.asList("hello").toString(),
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/indexdiff/IndexDiffWithSymlinkTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/indexdiff/IndexDiffWithSymlinkTest.java
index d02bfcd3f6..1119db3712 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/indexdiff/IndexDiffWithSymlinkTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/indexdiff/IndexDiffWithSymlinkTest.java
@@ -132,7 +132,8 @@ public class IndexDiffWithSymlinkTest extends LocalDiskRepositoryTestCase {
Writer writer = new OutputStreamWriter(out, UTF_8)) {
writer.write("echo `which git` 1>&2\n");
writer.write("echo `git --version` 1>&2\n");
- writer.write("git init " + name + " && \\\n");
+ writer.write("git -c init.defaultBranch=master init " + name
+ + " && \\\n");
writer.write("cd ./" + name + " && \\\n");
writer.write("git fast-import < ../" + name + ".txt && \\\n");
writer.write("git checkout -f\n");
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/revwalk/ObjectReachabilityTestCase.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/revwalk/ObjectReachabilityTestCase.java
index 37ff40bdf7..0e73588c66 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/revwalk/ObjectReachabilityTestCase.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/revwalk/ObjectReachabilityTestCase.java
@@ -51,7 +51,6 @@ public abstract class ObjectReachabilityTestCase
}
}
- /** {@inheritDoc} */
@Override
@Before
public void setUp() throws Exception {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/revwalk/ReachabilityCheckerTestCase.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/revwalk/ReachabilityCheckerTestCase.java
index 7679c11098..eeb13cc8b9 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/revwalk/ReachabilityCheckerTestCase.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/revwalk/ReachabilityCheckerTestCase.java
@@ -32,7 +32,6 @@ public abstract class ReachabilityCheckerTestCase
TestRepository<FileRepository> repo;
- /** {@inheritDoc} */
@Override
@Before
public void setUp() throws Exception {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphTest.java
index 97976564d8..4d05360252 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphTest.java
@@ -10,24 +10,31 @@
package org.eclipse.jgit.internal.storage.commitgraph;
+import static java.nio.charset.StandardCharsets.UTF_8;
import static org.eclipse.jgit.lib.Constants.COMMIT_GENERATION_UNKNOWN;
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.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
+import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.internal.storage.file.FileRepository;
import org.eclipse.jgit.junit.RepositoryTestCase;
import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.junit.Before;
import org.junit.Test;
@@ -45,6 +52,7 @@ public class CommitGraphTest extends RepositoryTestCase {
public void setUp() throws Exception {
super.setUp();
tr = new TestRepository<>(db, new RevWalk(db), mockSystemReader);
+ mockSystemReader.setJGitConfig(new MockConfig());
}
@Test
@@ -196,11 +204,32 @@ public class CommitGraphTest extends RepositoryTestCase {
assertEquals(getGenerationNumber(c8), 5);
}
+ @Test
+ public void testGraphComputeChangedPaths() throws Exception {
+ RevCommit a = tr.commit(tr.tree(tr.file("d/f", tr.blob("a"))));
+ RevCommit b = tr.commit(tr.tree(tr.file("d/f", tr.blob("a"))), a);
+ RevCommit c = tr.commit(tr.tree(tr.file("d/f", tr.blob("b"))), b);
+
+ writeAndReadCommitGraph(Collections.singleton(c));
+ ChangedPathFilter acpf = commitGraph
+ .getChangedPathFilter(commitGraph.findGraphPosition(a));
+ assertTrue(acpf.maybeContains("d".getBytes(UTF_8)));
+ assertTrue(acpf.maybeContains("d/f".getBytes(UTF_8)));
+ ChangedPathFilter bcpf = commitGraph
+ .getChangedPathFilter(commitGraph.findGraphPosition(b));
+ assertFalse(bcpf.maybeContains("d".getBytes(UTF_8)));
+ assertFalse(bcpf.maybeContains("d/f".getBytes(UTF_8)));
+ ChangedPathFilter ccpf = commitGraph
+ .getChangedPathFilter(commitGraph.findGraphPosition(c));
+ assertTrue(ccpf.maybeContains("d".getBytes(UTF_8)));
+ assertTrue(ccpf.maybeContains("d/f".getBytes(UTF_8)));
+ }
+
void writeAndReadCommitGraph(Set<ObjectId> wants) throws Exception {
NullProgressMonitor m = NullProgressMonitor.INSTANCE;
try (RevWalk walk = new RevWalk(db)) {
CommitGraphWriter writer = new CommitGraphWriter(
- GraphCommits.fromWalk(m, wants, walk));
+ GraphCommits.fromWalk(m, wants, walk), true);
ByteArrayOutputStream os = new ByteArrayOutputStream();
writer.write(m, os);
InputStream inputStream = new ByteArrayInputStream(
@@ -252,4 +281,41 @@ public class CommitGraphTest extends RepositoryTestCase {
RevCommit commit(RevCommit... parents) throws Exception {
return tr.commit(parents);
}
+
+ private static final class MockConfig extends FileBasedConfig {
+ private MockConfig() {
+ super(null, null);
+ }
+
+ @Override
+ public void load() throws IOException, ConfigInvalidException {
+ // Do nothing
+ }
+
+ @Override
+ public void save() throws IOException {
+ // Do nothing
+ }
+
+ @Override
+ public boolean isOutdated() {
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "MockConfig";
+ }
+
+ @Override
+ public boolean getBoolean(final String section, final String name,
+ final boolean defaultValue) {
+ if (section.equals(ConfigConstants.CONFIG_COMMIT_GRAPH_SECTION)
+ && name.equals(
+ ConfigConstants.CONFIG_KEY_READ_CHANGED_PATHS)) {
+ return true;
+ }
+ return defaultValue;
+ }
+ }
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphWriterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphWriterTest.java
index 6c5e5e5605..80a0f0cea5 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphWriterTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/commitgraph/CommitGraphWriterTest.java
@@ -10,21 +10,38 @@
package org.eclipse.jgit.internal.storage.commitgraph;
+import static java.util.stream.Collectors.toList;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsInAnyOrder;
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.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
import java.util.Set;
+import org.eclipse.jgit.dircache.DirCacheEntry;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.internal.storage.file.GC;
import org.eclipse.jgit.junit.RepositoryTestCase;
import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.revwalk.RevBlob;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.eclipse.jgit.util.NB;
import org.junit.Before;
import org.junit.Test;
@@ -46,6 +63,7 @@ public class CommitGraphWriterTest extends RepositoryTestCase {
os = new ByteArrayOutputStream();
tr = new TestRepository<>(db, new RevWalk(db), mockSystemReader);
walk = new RevWalk(db);
+ mockSystemReader.setJGitConfig(new MockConfig());
}
@Test
@@ -68,7 +86,7 @@ public class CommitGraphWriterTest extends RepositoryTestCase {
Set<ObjectId> wants = Collections.singleton(tip);
NullProgressMonitor m = NullProgressMonitor.INSTANCE;
GraphCommits graphCommits = GraphCommits.fromWalk(m, wants, walk);
- writer = new CommitGraphWriter(graphCommits);
+ writer = new CommitGraphWriter(graphCommits, true);
writer.write(m, os);
assertEquals(5, graphCommits.size());
@@ -76,11 +94,20 @@ public class CommitGraphWriterTest extends RepositoryTestCase {
assertTrue(data.length > 0);
byte[] headers = new byte[8];
System.arraycopy(data, 0, headers, 0, 8);
- assertArrayEquals(new byte[] {'C', 'G', 'P', 'H', 1, 1, 4, 0}, headers);
- assertEquals(CommitGraphConstants.CHUNK_ID_OID_FANOUT, NB.decodeInt32(data, 8));
- assertEquals(CommitGraphConstants.CHUNK_ID_OID_LOOKUP, NB.decodeInt32(data, 20));
- assertEquals(CommitGraphConstants.CHUNK_ID_COMMIT_DATA, NB.decodeInt32(data, 32));
- assertEquals(CommitGraphConstants.CHUNK_ID_EXTRA_EDGE_LIST, NB.decodeInt32(data, 44));
+ assertArrayEquals(new byte[] { 'C', 'G', 'P', 'H', 1, 1, 6, 0 },
+ headers);
+ assertEquals(CommitGraphConstants.CHUNK_ID_OID_FANOUT,
+ NB.decodeInt32(data, 8));
+ assertEquals(CommitGraphConstants.CHUNK_ID_OID_LOOKUP,
+ NB.decodeInt32(data, 20));
+ assertEquals(CommitGraphConstants.CHUNK_ID_COMMIT_DATA,
+ NB.decodeInt32(data, 32));
+ assertEquals(CommitGraphConstants.CHUNK_ID_EXTRA_EDGE_LIST,
+ NB.decodeInt32(data, 44));
+ assertEquals(CommitGraphConstants.CHUNK_ID_BLOOM_FILTER_INDEX,
+ NB.decodeInt32(data, 56));
+ assertEquals(CommitGraphConstants.CHUNK_ID_BLOOM_FILTER_DATA,
+ NB.decodeInt32(data, 68));
}
@Test
@@ -93,7 +120,7 @@ public class CommitGraphWriterTest extends RepositoryTestCase {
Set<ObjectId> wants = Collections.singleton(tip);
NullProgressMonitor m = NullProgressMonitor.INSTANCE;
GraphCommits graphCommits = GraphCommits.fromWalk(m, wants, walk);
- writer = new CommitGraphWriter(graphCommits);
+ writer = new CommitGraphWriter(graphCommits, true);
writer.write(m, os);
assertEquals(4, graphCommits.size());
@@ -101,13 +128,355 @@ public class CommitGraphWriterTest extends RepositoryTestCase {
assertTrue(data.length > 0);
byte[] headers = new byte[8];
System.arraycopy(data, 0, headers, 0, 8);
- assertArrayEquals(new byte[] {'C', 'G', 'P', 'H', 1, 1, 3, 0}, headers);
- assertEquals(CommitGraphConstants.CHUNK_ID_OID_FANOUT, NB.decodeInt32(data, 8));
- assertEquals(CommitGraphConstants.CHUNK_ID_OID_LOOKUP, NB.decodeInt32(data, 20));
- assertEquals(CommitGraphConstants.CHUNK_ID_COMMIT_DATA, NB.decodeInt32(data, 32));
+ assertArrayEquals(new byte[] { 'C', 'G', 'P', 'H', 1, 1, 5, 0 },
+ headers);
+ assertEquals(CommitGraphConstants.CHUNK_ID_OID_FANOUT,
+ NB.decodeInt32(data, 8));
+ assertEquals(CommitGraphConstants.CHUNK_ID_OID_LOOKUP,
+ NB.decodeInt32(data, 20));
+ assertEquals(CommitGraphConstants.CHUNK_ID_COMMIT_DATA,
+ NB.decodeInt32(data, 32));
+ assertEquals(CommitGraphConstants.CHUNK_ID_BLOOM_FILTER_INDEX,
+ NB.decodeInt32(data, 44));
+ assertEquals(CommitGraphConstants.CHUNK_ID_BLOOM_FILTER_DATA,
+ NB.decodeInt32(data, 56));
+ }
+
+ @Test
+ public void testProgressMonitor() throws Exception {
+ RevCommit root = commit();
+ RevCommit a = commit(root);
+ RevCommit b = commit(root);
+ RevCommit tip = commit(a, b);
+ Set<ObjectId> wants = Collections.singleton(tip);
+
+ NonNestedTasksProgressMonitor nonNested = new NonNestedTasksProgressMonitor();
+ GraphCommits graphCommits = GraphCommits.fromWalk(nonNested, wants,
+ walk);
+ writer = new CommitGraphWriter(graphCommits, true);
+ writer.write(nonNested, os);
+ }
+
+ private static class NonNestedTasksProgressMonitor
+ implements ProgressMonitor {
+
+ boolean inTask;
+
+ @Override
+ public void start(int totalTasks) {
+ // empty
+ }
+
+ @Override
+ public void beginTask(String title, int totalWork) {
+ assertFalse("Previous monitoring task is not closed", inTask);
+ inTask = true;
+ }
+
+ @Override
+ public void update(int completed) {
+ // empty
+ }
+
+ @Override
+ public void endTask() {
+ assertTrue("Closing task that wasn't started", inTask);
+ inTask = false;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return false;
+ }
+
+ @Override
+ public void showDuration(boolean enabled) {
+ // empty
+ }
+ }
+
+ static HashSet<String> changedPathStrings(byte[] data) {
+ int oidf_offset = -1;
+ int bidx_offset = -1;
+ int bdat_offset = -1;
+ for (int i = 8; i < data.length - 4; i += 12) {
+ switch (NB.decodeInt32(data, i)) {
+ case CommitGraphConstants.CHUNK_ID_OID_FANOUT:
+ oidf_offset = (int) NB.decodeInt64(data, i + 4);
+ break;
+ case CommitGraphConstants.CHUNK_ID_BLOOM_FILTER_INDEX:
+ bidx_offset = (int) NB.decodeInt64(data, i + 4);
+ break;
+ case CommitGraphConstants.CHUNK_ID_BLOOM_FILTER_DATA:
+ bdat_offset = (int) NB.decodeInt64(data, i + 4);
+ break;
+ }
+ }
+ assertTrue(oidf_offset > 0);
+ assertTrue(bidx_offset > 0);
+ assertTrue(bdat_offset > 0);
+ bdat_offset += 12; // skip version, hash count, bits per entry
+ int commit_count = NB.decodeInt32(data, oidf_offset + 255 * 4);
+ int[] changed_path_length_cumuls = new int[commit_count];
+ for (int i = 0; i < commit_count; i++) {
+ changed_path_length_cumuls[i] = NB.decodeInt32(data,
+ bidx_offset + i * 4);
+ }
+ HashSet<String> changed_paths = new HashSet<>();
+ for (int i = 0; i < commit_count; i++) {
+ int prior_cumul = i == 0 ? 0 : changed_path_length_cumuls[i - 1];
+ String changed_path = "";
+ for (int j = prior_cumul; j < changed_path_length_cumuls[i]; j++) {
+ changed_path += data[bdat_offset + j] + ",";
+ }
+ changed_paths.add(changed_path);
+ }
+ return changed_paths;
+ }
+
+ /**
+ * Expected value generated using the following:
+ *
+ * <pre>
+ * # apply into git-repo: https://lore.kernel.org/git/cover.1684790529.git.jonathantanmy@google.com/
+ * (cd git-repo; make)
+ * git-repo/bin-wrappers/git init tested
+ * (cd tested; touch foo.txt; mkdir -p onedir/twodir; touch onedir/twodir/bar.txt)
+ * git-repo/bin-wrappers/git -C tested add foo.txt onedir
+ * git-repo/bin-wrappers/git -C tested commit -m first_commit
+ * (cd tested; mv foo.txt foo-new.txt; mv onedir/twodir/bar.txt onedir/twodir/bar-new.txt)
+ * git-repo/bin-wrappers/git -C tested add foo-new.txt onedir
+ * git-repo/bin-wrappers/git -C tested commit -a -m second_commit
+ * git-repo/bin-wrappers/git -C tested maintenance run
+ * git-repo/bin-wrappers/git -C tested commit-graph write --changed-paths
+ * (cd tested; $JGIT debug-read-changed-path-filter .git/objects/info/commit-graph)
+ * </pre>
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testChangedPathFilterRootAndNested() throws Exception {
+ RevBlob emptyBlob = tr.blob(new byte[] {});
+ RevCommit root = tr.commit(tr.tree(tr.file("foo.txt", emptyBlob),
+ tr.file("onedir/twodir/bar.txt", emptyBlob)));
+ RevCommit tip = tr.commit(tr.tree(tr.file("foo-new.txt", emptyBlob),
+ tr.file("onedir/twodir/bar-new.txt", emptyBlob)), root);
+
+ Set<ObjectId> wants = Collections.singleton(tip);
+ NullProgressMonitor m = NullProgressMonitor.INSTANCE;
+ GraphCommits graphCommits = GraphCommits.fromWalk(m, wants, walk);
+ writer = new CommitGraphWriter(graphCommits, true);
+ writer.write(m, os);
+
+ HashSet<String> changedPaths = changedPathStrings(os.toByteArray());
+ assertThat(changedPaths, containsInAnyOrder(
+ "109,-33,2,60,20,79,-11,116,",
+ "119,69,63,-8,0,"));
+ }
+
+ /**
+ * Expected value generated using the following:
+ *
+ * <pre>
+ * git -C git-repo checkout todo get version number when it is merged
+ * (cd git-repo; make)
+ * git-repo/bin-wrappers/git init tested
+ * (cd tested; mkdir -p onedir/twodir; touch onedir/twodir/a.txt; touch onedir/twodir/b.txt)
+ * git-repo/bin-wrappers/git -C tested add onedir
+ * git-repo/bin-wrappers/git -C tested commit -m first_commit
+ * (cd tested; mv onedir/twodir/a.txt onedir/twodir/c.txt; mv onedir/twodir/b.txt onedir/twodir/d.txt)
+ * git-repo/bin-wrappers/git -C tested add onedir
+ * git-repo/bin-wrappers/git -C tested commit -a -m second_commit
+ * git-repo/bin-wrappers/git -C tested maintenance run
+ * git-repo/bin-wrappers/git -C tested commit-graph write --changed-paths
+ * (cd tested; $JGIT debug-read-changed-path-filter .git/objects/info/commit-graph)
+ * </pre>
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testChangedPathFilterOverlappingNested() throws Exception {
+ RevBlob emptyBlob = tr.blob(new byte[] {});
+ RevCommit root = tr
+ .commit(tr.tree(tr.file("onedir/twodir/a.txt", emptyBlob),
+ tr.file("onedir/twodir/b.txt", emptyBlob)));
+ RevCommit tip = tr
+ .commit(tr.tree(tr.file("onedir/twodir/c.txt", emptyBlob),
+ tr.file("onedir/twodir/d.txt", emptyBlob)), root);
+
+ Set<ObjectId> wants = Collections.singleton(tip);
+ NullProgressMonitor m = NullProgressMonitor.INSTANCE;
+ GraphCommits graphCommits = GraphCommits.fromWalk(m, wants, walk);
+ writer = new CommitGraphWriter(graphCommits, true);
+ writer.write(m, os);
+
+ HashSet<String> changedPaths = changedPathStrings(os.toByteArray());
+ assertThat(changedPaths, containsInAnyOrder("61,30,23,-24,1,",
+ "-58,-51,-46,60,29,-121,113,90,"));
+ }
+
+ /**
+ * Expected value generated using the following:
+ *
+ * <pre>
+ * git -C git-repo checkout todo get version number when it is merged
+ * (cd git-repo; make)
+ * git-repo/bin-wrappers/git init tested
+ * (cd tested; touch 你好)
+ * git-repo/bin-wrappers/git -C tested add 你好
+ * git-repo/bin-wrappers/git -C tested commit -m first_commit
+ * git-repo/bin-wrappers/git -C tested maintenance run
+ * git-repo/bin-wrappers/git -C tested commit-graph write --changed-paths
+ * (cd tested; $JGIT debug-read-changed-path-filter .git/objects/info/commit-graph)
+ * </pre>
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testChangedPathFilterHighBit() throws Exception {
+ RevBlob emptyBlob = tr.blob(new byte[] {});
+ // tr.file encodes using UTF-8
+ RevCommit root = tr.commit(tr.tree(tr.file("你好", emptyBlob)));
+
+ Set<ObjectId> wants = Collections.singleton(root);
+ NullProgressMonitor m = NullProgressMonitor.INSTANCE;
+ GraphCommits graphCommits = GraphCommits.fromWalk(m, wants, walk);
+ writer = new CommitGraphWriter(graphCommits, true);
+ writer.write(m, os);
+
+ HashSet<String> changedPaths = changedPathStrings(os.toByteArray());
+ assertThat(changedPaths, containsInAnyOrder("16,16,"));
+ }
+
+ @Test
+ public void testChangedPathFilterEmptyChange() throws Exception {
+ RevCommit root = commit();
+
+ Set<ObjectId> wants = Collections.singleton(root);
+ NullProgressMonitor m = NullProgressMonitor.INSTANCE;
+ GraphCommits graphCommits = GraphCommits.fromWalk(m, wants, walk);
+ writer = new CommitGraphWriter(graphCommits, true);
+ writer.write(m, os);
+
+ HashSet<String> changedPaths = changedPathStrings(os.toByteArray());
+ assertThat(changedPaths, containsInAnyOrder("0,"));
+ }
+
+ @Test
+ public void testChangedPathFilterManyChanges() throws Exception {
+ RevBlob emptyBlob = tr.blob(new byte[] {});
+ DirCacheEntry[] entries = new DirCacheEntry[513];
+ for (int i = 0; i < entries.length; i++) {
+ entries[i] = tr.file(i + ".txt", emptyBlob);
+ }
+
+ RevCommit root = tr.commit(tr.tree(entries));
+
+ Set<ObjectId> wants = Collections.singleton(root);
+ NullProgressMonitor m = NullProgressMonitor.INSTANCE;
+ GraphCommits graphCommits = GraphCommits.fromWalk(m, wants, walk);
+ writer = new CommitGraphWriter(graphCommits, true);
+ writer.write(m, os);
+
+ HashSet<String> changedPaths = changedPathStrings(os.toByteArray());
+ assertThat(changedPaths, containsInAnyOrder("-1,"));
+ }
+
+ @Test
+ public void testReuseBloomFilters() throws Exception {
+ RevBlob emptyBlob = tr.blob(new byte[] {});
+ RevCommit root = tr.commit(tr.tree(tr.file("foo.txt", emptyBlob),
+ tr.file("onedir/twodir/bar.txt", emptyBlob)));
+ tr.branch("master").update(root);
+
+ db.getConfig().setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
+ ConfigConstants.CONFIG_COMMIT_GRAPH, true);
+ db.getConfig().setBoolean(ConfigConstants.CONFIG_GC_SECTION, null,
+ ConfigConstants.CONFIG_KEY_WRITE_COMMIT_GRAPH, true);
+ db.getConfig().setBoolean(ConfigConstants.CONFIG_GC_SECTION, null,
+ ConfigConstants.CONFIG_KEY_WRITE_CHANGED_PATHS, true);
+ GC gc = new GC(db);
+ gc.gc().get();
+
+ RevCommit tip = tr.commit(tr.tree(tr.file("foo-new.txt", emptyBlob),
+ tr.file("onedir/twodir/bar-new.txt", emptyBlob)), root);
+
+ Set<ObjectId> wants = Collections.singleton(tip);
+ NullProgressMonitor m = NullProgressMonitor.INSTANCE;
+ GraphCommits graphCommits = GraphCommits.fromWalk(m, wants, walk);
+ writer = new CommitGraphWriter(graphCommits, true);
+ CommitGraphWriter.Stats stats = writer.write(m, os);
+
+ assertEquals(1, stats.getChangedPathFiltersReused());
+ assertEquals(1, stats.getChangedPathFiltersComputed());
+
+ // Expected strings are the same as in
+ // #testChangedPathFilterRootAndNested
+ HashSet<String> changedPaths = changedPathStrings(os.toByteArray());
+ assertThat(changedPaths, containsInAnyOrder(
+ "109,-33,2,60,20,79,-11,116,",
+ "119,69,63,-8,0,"));
+ }
+
+ @Test
+ public void testPathDiffCalculator_skipUnchangedTree() throws Exception {
+ RevCommit root = tr.commit(tr.tree(
+ tr.file("d/sd1/f1", tr.blob("f1")),
+ tr.file("d/sd2/f2", tr.blob("f2"))));
+ RevCommit tip = tr.commit(tr.tree(
+ tr.file("d/sd1/f1", tr.blob("f1")),
+ tr.file("d/sd2/f2", tr.blob("f2B"))), root);
+ CommitGraphWriter.PathDiffCalculator c = new CommitGraphWriter.PathDiffCalculator();
+
+ Optional<HashSet<ByteBuffer>> byteBuffers = c.changedPaths(walk.getObjectReader(), tip);
+
+ assertTrue(byteBuffers.isPresent());
+ List<String> asString = byteBuffers.get().stream()
+ .map(b -> StandardCharsets.UTF_8.decode(b).toString())
+ .collect(toList());
+ assertThat(asString, containsInAnyOrder("d", "d/sd2", "d/sd2/f2"));
+ // We don't walk into d/sd1/f1
+ assertEquals(1, c.stepCounter);
}
RevCommit commit(RevCommit... parents) throws Exception {
return tr.commit(parents);
}
-}
+
+ private static final class MockConfig extends FileBasedConfig {
+ private MockConfig() {
+ super(null, null);
+ }
+
+ @Override
+ public void load() throws IOException, ConfigInvalidException {
+ // Do nothing
+ }
+
+ @Override
+ public void save() throws IOException {
+ // Do nothing
+ }
+
+ @Override
+ public boolean isOutdated() {
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "MockConfig";
+ }
+
+ @Override
+ public boolean getBoolean(final String section, final String name,
+ final boolean defaultValue) {
+ if (section.equals(ConfigConstants.CONFIG_COMMIT_GRAPH_SECTION)
+ && name.equals(
+ ConfigConstants.CONFIG_KEY_READ_CHANGED_PATHS)) {
+ return true;
+ }
+ return defaultValue;
+ }
+ }
+} \ No newline at end of file
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/commitgraph/GraphObjectIndexTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/commitgraph/GraphObjectIndexTest.java
new file mode 100644
index 0000000000..b533d5c985
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/commitgraph/GraphObjectIndexTest.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2023, Google LLC
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.storage.commitgraph;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+
+public class GraphObjectIndexTest {
+
+ @Test
+ public void findGraphPosition_noObjInBucket() throws CommitGraphFormatException {
+ GraphObjectIndex idx = new GraphObjectIndex(100,
+ new byte[256 * 4], new byte[] {});
+ int graphPosition = idx.findGraphPosition(
+ ObjectId.fromString("731dfd4c5eb6f88b98e983b9b0551b3562a0c46c"));
+ assertEquals(-1, graphPosition);
+ }
+
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/AggregatedBlockCacheStatsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/AggregatedBlockCacheStatsTest.java
new file mode 100644
index 0000000000..2c4b432a01
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/AggregatedBlockCacheStatsTest.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (c) 2024, Google LLC and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.storage.dfs;
+
+import static org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheTable.BlockCacheStats;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.junit.Assert.assertArrayEquals;
+
+import java.util.List;
+
+import org.eclipse.jgit.internal.storage.pack.PackExt;
+import org.junit.Test;
+
+public class AggregatedBlockCacheStatsTest {
+ @Test
+ public void getName() {
+ BlockCacheStats aggregatedBlockCacheStats = AggregatedBlockCacheStats
+ .fromStatsList(List.of());
+
+ assertThat(aggregatedBlockCacheStats.getName(),
+ equalTo(AggregatedBlockCacheStats.class.getName()));
+ }
+
+ @Test
+ public void getCurrentSize_aggregatesCurrentSizes() {
+ long[] currentSizes = createEmptyStatsArray();
+
+ DfsBlockCacheStats packStats = new DfsBlockCacheStats();
+ packStats.addToLiveBytes(new TestKey(PackExt.PACK), 5);
+ currentSizes[PackExt.PACK.getPosition()] = 5;
+
+ DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats();
+ bitmapStats.addToLiveBytes(new TestKey(PackExt.BITMAP_INDEX), 6);
+ currentSizes[PackExt.BITMAP_INDEX.getPosition()] = 6;
+
+ DfsBlockCacheStats indexStats = new DfsBlockCacheStats();
+ indexStats.addToLiveBytes(new TestKey(PackExt.INDEX), 7);
+ currentSizes[PackExt.INDEX.getPosition()] = 7;
+
+ BlockCacheStats aggregatedBlockCacheStats = AggregatedBlockCacheStats
+ .fromStatsList(List.of(packStats, bitmapStats, indexStats));
+
+ assertArrayEquals(aggregatedBlockCacheStats.getCurrentSize(),
+ currentSizes);
+ }
+
+ @Test
+ public void getHitCount_aggregatesHitCounts() {
+ long[] hitCounts = createEmptyStatsArray();
+
+ DfsBlockCacheStats packStats = new DfsBlockCacheStats();
+ incrementCounter(5,
+ () -> packStats.incrementHit(new TestKey(PackExt.PACK)));
+ hitCounts[PackExt.PACK.getPosition()] = 5;
+
+ DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats();
+ incrementCounter(6, () -> bitmapStats
+ .incrementHit(new TestKey(PackExt.BITMAP_INDEX)));
+ hitCounts[PackExt.BITMAP_INDEX.getPosition()] = 6;
+
+ DfsBlockCacheStats indexStats = new DfsBlockCacheStats();
+ incrementCounter(7,
+ () -> indexStats.incrementHit(new TestKey(PackExt.INDEX)));
+ hitCounts[PackExt.INDEX.getPosition()] = 7;
+
+ BlockCacheStats aggregatedBlockCacheStats = AggregatedBlockCacheStats
+ .fromStatsList(List.of(packStats, bitmapStats, indexStats));
+
+ assertArrayEquals(aggregatedBlockCacheStats.getHitCount(), hitCounts);
+ }
+
+ @Test
+ public void getMissCount_aggregatesMissCounts() {
+ long[] missCounts = createEmptyStatsArray();
+
+ DfsBlockCacheStats packStats = new DfsBlockCacheStats();
+ incrementCounter(5,
+ () -> packStats.incrementMiss(new TestKey(PackExt.PACK)));
+ missCounts[PackExt.PACK.getPosition()] = 5;
+
+ DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats();
+ incrementCounter(6, () -> bitmapStats
+ .incrementMiss(new TestKey(PackExt.BITMAP_INDEX)));
+ missCounts[PackExt.BITMAP_INDEX.getPosition()] = 6;
+
+ DfsBlockCacheStats indexStats = new DfsBlockCacheStats();
+ incrementCounter(7,
+ () -> indexStats.incrementMiss(new TestKey(PackExt.INDEX)));
+ missCounts[PackExt.INDEX.getPosition()] = 7;
+
+ BlockCacheStats aggregatedBlockCacheStats = AggregatedBlockCacheStats
+ .fromStatsList(List.of(packStats, bitmapStats, indexStats));
+
+ assertArrayEquals(aggregatedBlockCacheStats.getMissCount(), missCounts);
+ }
+
+ @Test
+ public void getTotalRequestCount_aggregatesRequestCounts() {
+ long[] totalRequestCounts = createEmptyStatsArray();
+
+ DfsBlockCacheStats packStats = new DfsBlockCacheStats();
+ incrementCounter(5, () -> {
+ packStats.incrementHit(new TestKey(PackExt.PACK));
+ packStats.incrementMiss(new TestKey(PackExt.PACK));
+ });
+ totalRequestCounts[PackExt.PACK.getPosition()] = 10;
+
+ DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats();
+ incrementCounter(6, () -> {
+ bitmapStats.incrementHit(new TestKey(PackExt.BITMAP_INDEX));
+ bitmapStats.incrementMiss(new TestKey(PackExt.BITMAP_INDEX));
+ });
+ totalRequestCounts[PackExt.BITMAP_INDEX.getPosition()] = 12;
+
+ DfsBlockCacheStats indexStats = new DfsBlockCacheStats();
+ incrementCounter(7, () -> {
+ indexStats.incrementHit(new TestKey(PackExt.INDEX));
+ indexStats.incrementMiss(new TestKey(PackExt.INDEX));
+ });
+ totalRequestCounts[PackExt.INDEX.getPosition()] = 14;
+
+ BlockCacheStats aggregatedBlockCacheStats = AggregatedBlockCacheStats
+ .fromStatsList(List.of(packStats, bitmapStats, indexStats));
+
+ assertArrayEquals(aggregatedBlockCacheStats.getTotalRequestCount(),
+ totalRequestCounts);
+ }
+
+ @Test
+ public void getHitRatio_aggregatesHitRatios() {
+ long[] hitRatios = createEmptyStatsArray();
+
+ DfsBlockCacheStats packStats = new DfsBlockCacheStats();
+ incrementCounter(5,
+ () -> packStats.incrementHit(new TestKey(PackExt.PACK)));
+ hitRatios[PackExt.PACK.getPosition()] = 100;
+
+ DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats();
+ incrementCounter(6, () -> {
+ bitmapStats.incrementHit(new TestKey(PackExt.BITMAP_INDEX));
+ bitmapStats.incrementMiss(new TestKey(PackExt.BITMAP_INDEX));
+ });
+ hitRatios[PackExt.BITMAP_INDEX.getPosition()] = 50;
+
+ DfsBlockCacheStats indexStats = new DfsBlockCacheStats();
+ incrementCounter(7,
+ () -> indexStats.incrementMiss(new TestKey(PackExt.INDEX)));
+ hitRatios[PackExt.INDEX.getPosition()] = 0;
+
+ BlockCacheStats aggregatedBlockCacheStats = AggregatedBlockCacheStats
+ .fromStatsList(List.of(packStats, bitmapStats, indexStats));
+
+ assertArrayEquals(aggregatedBlockCacheStats.getHitRatio(), hitRatios);
+ }
+
+ @Test
+ public void getEvictions_aggregatesEvictions() {
+ long[] evictions = createEmptyStatsArray();
+
+ DfsBlockCacheStats packStats = new DfsBlockCacheStats();
+ incrementCounter(5,
+ () -> packStats.incrementEvict(new TestKey(PackExt.PACK)));
+ evictions[PackExt.PACK.getPosition()] = 5;
+
+ DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats();
+ incrementCounter(6, () -> bitmapStats
+ .incrementEvict(new TestKey(PackExt.BITMAP_INDEX)));
+ evictions[PackExt.BITMAP_INDEX.getPosition()] = 6;
+
+ DfsBlockCacheStats indexStats = new DfsBlockCacheStats();
+ incrementCounter(7,
+ () -> indexStats.incrementEvict(new TestKey(PackExt.INDEX)));
+ evictions[PackExt.INDEX.getPosition()] = 7;
+
+ BlockCacheStats aggregatedBlockCacheStats = AggregatedBlockCacheStats
+ .fromStatsList(List.of(packStats, bitmapStats, indexStats));
+
+ assertArrayEquals(aggregatedBlockCacheStats.getEvictions(), evictions);
+ }
+
+ private static void incrementCounter(int amount, Runnable fn) {
+ for (int i = 0; i < amount; i++) {
+ fn.run();
+ }
+ }
+
+ private static long[] createEmptyStatsArray() {
+ return new long[PackExt.values().length];
+ }
+
+ private static class TestKey extends DfsStreamKey {
+ TestKey(PackExt packExt) {
+ super(0, packExt);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return false;
+ }
+ }
+} \ No newline at end of file
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/ClockBlockCacheTableTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/ClockBlockCacheTableTest.java
new file mode 100644
index 0000000000..2e2f86bf80
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/ClockBlockCacheTableTest.java
@@ -0,0 +1,67 @@
+package org.eclipse.jgit.internal.storage.dfs;
+
+import static org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheConfig.DEFAULT_NAME;
+import static org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheTable.BlockCacheStats;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.isA;
+
+import java.util.List;
+
+import org.junit.Test;
+
+public class ClockBlockCacheTableTest {
+ private static final String NAME = "name";
+
+ @Test
+ public void getName_nameNotConfigured_returnsDefaultName() {
+ ClockBlockCacheTable cacheTable = new ClockBlockCacheTable(
+ createBlockCacheConfig());
+
+ assertThat(cacheTable.getName(), equalTo(DEFAULT_NAME));
+ }
+
+ @Test
+ public void getName_nameConfigured_returnsConfiguredName() {
+ ClockBlockCacheTable cacheTable = new ClockBlockCacheTable(
+ createBlockCacheConfig().setName(NAME));
+
+ assertThat(cacheTable.getName(), equalTo(NAME));
+ }
+
+ @Test
+ public void getBlockCacheStats_nameNotConfigured_returnsBlockCacheStatsWithDefaultName() {
+ ClockBlockCacheTable cacheTable = new ClockBlockCacheTable(
+ createBlockCacheConfig());
+
+ assertThat(cacheTable.getBlockCacheStats(), hasSize(1));
+ assertThat(cacheTable.getBlockCacheStats().get(0).getName(),
+ equalTo(DEFAULT_NAME));
+ }
+
+ @Test
+ public void getBlockCacheStats_nameConfigured_returnsBlockCacheStatsWithConfiguredName() {
+ ClockBlockCacheTable cacheTable = new ClockBlockCacheTable(
+ createBlockCacheConfig().setName(NAME));
+
+ assertThat(cacheTable.getBlockCacheStats(), hasSize(1));
+ assertThat(cacheTable.getBlockCacheStats().get(0).getName(),
+ equalTo(NAME));
+ }
+
+ @Test
+ public void getAllBlockCacheStats() {
+ ClockBlockCacheTable cacheTable = new ClockBlockCacheTable(
+ createBlockCacheConfig());
+
+ List<BlockCacheStats> blockCacheStats = cacheTable.getBlockCacheStats();
+ assertThat(blockCacheStats, contains(isA(BlockCacheStats.class)));
+ }
+
+ private static DfsBlockCacheConfig createBlockCacheConfig() {
+ return new DfsBlockCacheConfig().setBlockSize(512)
+ .setConcurrencyLevel(4).setBlockLimit(1024);
+ }
+} \ No newline at end of file
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfigTest.java
index 2df0ba1b05..afa3179cde 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfigTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfigTest.java
@@ -38,13 +38,37 @@
package org.eclipse.jgit.internal.storage.dfs;
+import static org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheConfig.DEFAULT_NAME;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_CORE_SECTION;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DFS_CACHE_PREFIX;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DFS_SECTION;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_BLOCK_LIMIT;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_BLOCK_SIZE;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_CONCURRENCY_LEVEL;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PACK_EXTENSIONS;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_STREAM_RATIO;
import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.closeTo;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThrows;
+import java.io.ByteArrayOutputStream;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheConfig.DfsBlockCachePackExtConfig;
+import org.eclipse.jgit.internal.storage.pack.PackExt;
+import org.eclipse.jgit.lib.Config;
import org.junit.Test;
+@SuppressWarnings("boxing")
public class DfsBlockCacheConfigTest {
@Test
@@ -55,7 +79,6 @@ public class DfsBlockCacheConfigTest {
}
@Test
- @SuppressWarnings("boxing")
public void negativeBlockSizeIsConvertedToDefault() {
DfsBlockCacheConfig config = new DfsBlockCacheConfig();
config.setBlockSize(-1);
@@ -64,7 +87,6 @@ public class DfsBlockCacheConfigTest {
}
@Test
- @SuppressWarnings("boxing")
public void tooSmallBlockSizeIsConvertedToDefault() {
DfsBlockCacheConfig config = new DfsBlockCacheConfig();
config.setBlockSize(10);
@@ -73,11 +95,295 @@ public class DfsBlockCacheConfigTest {
}
@Test
- @SuppressWarnings("boxing")
public void validBlockSize() {
DfsBlockCacheConfig config = new DfsBlockCacheConfig();
config.setBlockSize(65536);
assertThat(config.getBlockSize(), is(65536));
}
+
+ @Test
+ public void fromConfigs() {
+ Config config = new Config();
+ config.setLong(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION,
+ CONFIG_KEY_BLOCK_LIMIT, 50 * 1024);
+ config.setInt(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION,
+ CONFIG_KEY_BLOCK_SIZE, 1024);
+ config.setInt(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION,
+ CONFIG_KEY_CONCURRENCY_LEVEL, 3);
+ config.setString(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION,
+ CONFIG_KEY_STREAM_RATIO, "0.5");
+
+ DfsBlockCacheConfig cacheConfig = new DfsBlockCacheConfig()
+ .fromConfig(config);
+ assertThat(cacheConfig.getBlockLimit(), is(50L * 1024L));
+ assertThat(cacheConfig.getBlockSize(), is(1024));
+ assertThat(cacheConfig.getConcurrencyLevel(), is(3));
+ assertThat(cacheConfig.getStreamRatio(), closeTo(0.5, 0.0001));
+ }
+
+ @Test
+ public void fromConfig_blockLimitNotAMultipleOfBlockSize_throws() {
+ Config config = new Config();
+ config.setLong(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION,
+ CONFIG_KEY_BLOCK_LIMIT, 1025);
+ config.setInt(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION,
+ CONFIG_KEY_BLOCK_SIZE, 1024);
+
+ assertThrows(IllegalArgumentException.class,
+ () -> new DfsBlockCacheConfig().fromConfig(config));
+ }
+
+ @Test
+ public void fromConfig_streamRatioInvalidFormat_throws() {
+ Config config = new Config();
+ config.setString(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION,
+ CONFIG_KEY_STREAM_RATIO, "0.a5");
+
+ assertThrows(IllegalArgumentException.class,
+ () -> new DfsBlockCacheConfig().fromConfig(config));
+ }
+
+ @Test
+ public void fromConfig_generatesDfsBlockCachePackExtConfigs() {
+ Config config = new Config();
+ addPackExtConfigEntry(config, "pack", List.of(PackExt.PACK),
+ /* blockLimit= */ 20 * 512, /* blockSize= */ 512);
+
+ addPackExtConfigEntry(config, "bitmap", List.of(PackExt.BITMAP_INDEX),
+ /* blockLimit= */ 25 * 1024, /* blockSize= */ 1024);
+
+ addPackExtConfigEntry(config, "index",
+ List.of(PackExt.INDEX, PackExt.OBJECT_SIZE_INDEX,
+ PackExt.REVERSE_INDEX),
+ /* blockLimit= */ 30 * 1024, /* blockSize= */ 1024);
+
+ DfsBlockCacheConfig cacheConfig = new DfsBlockCacheConfig()
+ .fromConfig(config);
+ var configs = cacheConfig.getPackExtCacheConfigurations();
+ assertThat(configs, hasSize(3));
+ var packConfig = getConfigForExt(configs, PackExt.PACK);
+ assertThat(packConfig.getBlockLimit(), is(20L * 512L));
+ assertThat(packConfig.getBlockSize(), is(512));
+
+ var bitmapConfig = getConfigForExt(configs, PackExt.BITMAP_INDEX);
+ assertThat(bitmapConfig.getBlockLimit(), is(25L * 1024L));
+ assertThat(bitmapConfig.getBlockSize(), is(1024));
+
+ var indexConfig = getConfigForExt(configs, PackExt.INDEX);
+ assertThat(indexConfig.getBlockLimit(), is(30L * 1024L));
+ assertThat(indexConfig.getBlockSize(), is(1024));
+ assertThat(getConfigForExt(configs, PackExt.OBJECT_SIZE_INDEX),
+ is(indexConfig));
+ assertThat(getConfigForExt(configs, PackExt.REVERSE_INDEX),
+ is(indexConfig));
+ }
+
+ @Test
+ public void fromConfig_withExistingCacheHotMap_configWithPackExtConfigsHasHotMaps() {
+ Config config = new Config();
+ addPackExtConfigEntry(config, "pack", List.of(PackExt.PACK),
+ /* blockLimit= */ 20 * 512, /* blockSize= */ 512);
+
+ addPackExtConfigEntry(config, "bitmap", List.of(PackExt.BITMAP_INDEX),
+ /* blockLimit= */ 25 * 1024, /* blockSize= */ 1024);
+
+ addPackExtConfigEntry(config, "index",
+ List.of(PackExt.INDEX, PackExt.OBJECT_SIZE_INDEX,
+ PackExt.REVERSE_INDEX),
+ /* blockLimit= */ 30 * 1024, /* blockSize= */ 1024);
+
+ Map<PackExt, Integer> cacheHotMap = Map.of(PackExt.PACK, 1,
+ PackExt.BITMAP_INDEX, 2, PackExt.INDEX, 3, PackExt.REFTABLE, 4);
+
+ DfsBlockCacheConfig cacheConfig = new DfsBlockCacheConfig();
+ cacheConfig.setCacheHotMap(cacheHotMap);
+ cacheConfig.fromConfig(config);
+
+ var configs = cacheConfig.getPackExtCacheConfigurations();
+ assertThat(cacheConfig.getCacheHotMap(), is(cacheHotMap));
+ assertThat(configs, hasSize(3));
+ var packConfig = getConfigForExt(configs, PackExt.PACK);
+ assertThat(packConfig.getCacheHotMap(), is(Map.of(PackExt.PACK, 1)));
+
+ var bitmapConfig = getConfigForExt(configs, PackExt.BITMAP_INDEX);
+ assertThat(bitmapConfig.getCacheHotMap(),
+ is(Map.of(PackExt.BITMAP_INDEX, 2)));
+
+ var indexConfig = getConfigForExt(configs, PackExt.INDEX);
+ assertThat(indexConfig.getCacheHotMap(), is(Map.of(PackExt.INDEX, 3)));
+ }
+
+ @Test
+ public void setCacheHotMap_configWithPackExtConfigs_setsHotMaps() {
+ Config config = new Config();
+ addPackExtConfigEntry(config, "pack", List.of(PackExt.PACK),
+ /* blockLimit= */ 20 * 512, /* blockSize= */ 512);
+
+ addPackExtConfigEntry(config, "bitmap", List.of(PackExt.BITMAP_INDEX),
+ /* blockLimit= */ 25 * 1024, /* blockSize= */ 1024);
+
+ addPackExtConfigEntry(config, "index",
+ List.of(PackExt.INDEX, PackExt.OBJECT_SIZE_INDEX,
+ PackExt.REVERSE_INDEX),
+ /* blockLimit= */ 30 * 1024, /* blockSize= */ 1024);
+
+ Map<PackExt, Integer> cacheHotMap = Map.of(PackExt.PACK, 1,
+ PackExt.BITMAP_INDEX, 2, PackExt.INDEX, 3, PackExt.REFTABLE, 4);
+
+ DfsBlockCacheConfig cacheConfig = new DfsBlockCacheConfig()
+ .fromConfig(config);
+ cacheConfig.setCacheHotMap(cacheHotMap);
+
+ var configs = cacheConfig.getPackExtCacheConfigurations();
+ assertThat(cacheConfig.getCacheHotMap(), is(cacheHotMap));
+ assertThat(configs, hasSize(3));
+ var packConfig = getConfigForExt(configs, PackExt.PACK);
+ assertThat(packConfig.getCacheHotMap(), is(Map.of(PackExt.PACK, 1)));
+
+ var bitmapConfig = getConfigForExt(configs, PackExt.BITMAP_INDEX);
+ assertThat(bitmapConfig.getCacheHotMap(),
+ is(Map.of(PackExt.BITMAP_INDEX, 2)));
+
+ var indexConfig = getConfigForExt(configs, PackExt.INDEX);
+ assertThat(indexConfig.getCacheHotMap(), is(Map.of(PackExt.INDEX, 3)));
+ }
+
+ @Test
+ public void fromConfigs_baseConfigOnly_nameSetFromConfigDfsSubSection() {
+ Config config = new Config();
+
+ DfsBlockCacheConfig blockCacheConfig = new DfsBlockCacheConfig()
+ .fromConfig(config);
+ assertThat(blockCacheConfig.getName(), equalTo(DEFAULT_NAME));
+ }
+
+ @Test
+ public void fromConfigs_namesSetFromConfigDfsCachePrefixSubSections() {
+ Config config = new Config();
+ config.setString(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION,
+ CONFIG_KEY_STREAM_RATIO, "0.5");
+ config.setString(CONFIG_CORE_SECTION, CONFIG_DFS_CACHE_PREFIX + "name1",
+ CONFIG_KEY_PACK_EXTENSIONS, PackExt.PACK.name());
+ config.setString(CONFIG_CORE_SECTION, CONFIG_DFS_CACHE_PREFIX + "name2",
+ CONFIG_KEY_PACK_EXTENSIONS, PackExt.BITMAP_INDEX.name());
+
+ DfsBlockCacheConfig blockCacheConfig = new DfsBlockCacheConfig()
+ .fromConfig(config);
+ assertThat(blockCacheConfig.getName(), equalTo("dfs"));
+ assertThat(
+ blockCacheConfig.getPackExtCacheConfigurations().get(0)
+ .getPackExtCacheConfiguration().getName(),
+ equalTo("dfs.name1"));
+ assertThat(
+ blockCacheConfig.getPackExtCacheConfigurations().get(1)
+ .getPackExtCacheConfiguration().getName(),
+ equalTo("dfs.name2"));
+ }
+
+ @Test
+ public void fromConfigs_dfsBlockCachePackExtConfigWithDuplicateExtensions_throws() {
+ Config config = new Config();
+ config.setString(CONFIG_CORE_SECTION, CONFIG_DFS_CACHE_PREFIX + "pack1",
+ CONFIG_KEY_PACK_EXTENSIONS, PackExt.PACK.name());
+
+ config.setString(CONFIG_CORE_SECTION, CONFIG_DFS_CACHE_PREFIX + "pack2",
+ CONFIG_KEY_PACK_EXTENSIONS, PackExt.PACK.name());
+
+ assertThrows(IllegalArgumentException.class,
+ () -> new DfsBlockCacheConfig().fromConfig(config));
+ }
+
+ @Test
+ public void fromConfigs_dfsBlockCachePackExtConfigWithEmptyExtensions_throws() {
+ Config config = new Config();
+ config.setString(CONFIG_CORE_SECTION, CONFIG_DFS_CACHE_PREFIX + "pack1",
+ CONFIG_KEY_PACK_EXTENSIONS, "");
+
+ assertThrows(IllegalArgumentException.class,
+ () -> new DfsBlockCacheConfig().fromConfig(config));
+ }
+
+ @Test
+ public void fromConfigs_dfsBlockCachePackExtConfigWithNoExtensions_throws() {
+ Config config = new Config();
+ config.setInt(CONFIG_CORE_SECTION, CONFIG_DFS_CACHE_PREFIX + "pack1",
+ CONFIG_KEY_BLOCK_SIZE, 0);
+
+ assertThrows(IllegalArgumentException.class,
+ () -> new DfsBlockCacheConfig().fromConfig(config));
+ }
+
+ @Test
+ public void fromConfigs_dfsBlockCachePackExtConfigWithUnknownExtensions_throws() {
+ Config config = new Config();
+ config.setString(CONFIG_CORE_SECTION,
+ CONFIG_DFS_CACHE_PREFIX + "unknownExt",
+ CONFIG_KEY_PACK_EXTENSIONS, "NotAKnownExt");
+
+ assertThrows(IllegalArgumentException.class,
+ () -> new DfsBlockCacheConfig().fromConfig(config));
+ }
+
+ @Test
+ public void writeConfigurationDebug_writesConfigsToWriter()
+ throws Exception {
+ Config config = new Config();
+ config.setLong(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION,
+ CONFIG_KEY_BLOCK_LIMIT, 50 * 1024);
+ config.setInt(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION,
+ CONFIG_KEY_BLOCK_SIZE, 1024);
+ config.setInt(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION,
+ CONFIG_KEY_CONCURRENCY_LEVEL, 3);
+ config.setString(CONFIG_CORE_SECTION, CONFIG_DFS_SECTION,
+ CONFIG_KEY_STREAM_RATIO, "0.5");
+ addPackExtConfigEntry(config, "pack", List.of(PackExt.PACK),
+ /* blockLimit= */ 20 * 512, /* blockSize= */ 512);
+
+ DfsBlockCacheConfig cacheConfig = new DfsBlockCacheConfig()
+ .fromConfig(config);
+ Map<PackExt, Integer> hotmap = Map.of(PackExt.PACK, 10);
+ cacheConfig.setCacheHotMap(hotmap);
+
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ cacheConfig.print(new PrintWriter(byteArrayOutputStream, true,
+ StandardCharsets.UTF_8));
+
+ String writenConfig = byteArrayOutputStream
+ .toString(StandardCharsets.UTF_8);
+
+ List<String> writenLines = Arrays.asList(writenConfig.split("\n"));
+ assertThat(writenLines,
+ equalTo(List.of("Name: dfs", " BlockLimit: " + (50 * 1024),
+ " BlockSize: 1024", " StreamRatio: 0.5",
+ " ConcurrencyLevel: 3",
+ " CacheHotMapEntry: " + PackExt.PACK + " : " + 10,
+ " Name: dfs.pack", " BlockLimit: " + 20 * 512,
+ " BlockSize: 512", " StreamRatio: 0.3",
+ " ConcurrencyLevel: 32",
+ " CacheHotMapEntry: " + PackExt.PACK + " : " + 10,
+ " PackExts: " + List.of(PackExt.PACK))));
+ }
+
+ private static void addPackExtConfigEntry(Config config, String configName,
+ List<PackExt> packExts, long blockLimit, int blockSize) {
+ String packExtConfigName = CONFIG_DFS_CACHE_PREFIX + configName;
+ config.setString(CONFIG_CORE_SECTION, packExtConfigName,
+ CONFIG_KEY_PACK_EXTENSIONS, packExts.stream().map(PackExt::name)
+ .collect(Collectors.joining(" ")));
+ config.setLong(CONFIG_CORE_SECTION, packExtConfigName,
+ CONFIG_KEY_BLOCK_LIMIT, blockLimit);
+ config.setInt(CONFIG_CORE_SECTION, packExtConfigName,
+ CONFIG_KEY_BLOCK_SIZE, blockSize);
+ }
+
+ private static DfsBlockCacheConfig getConfigForExt(
+ List<DfsBlockCachePackExtConfig> configs, PackExt packExt) {
+ for (DfsBlockCachePackExtConfig config : configs) {
+ if (config.getPackExts().contains(packExt)) {
+ return config.getPackExtCacheConfiguration();
+ }
+ }
+ return null;
+ }
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTest.java
index fef0563f48..3c7cc075d2 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTest.java
@@ -13,20 +13,24 @@ package org.eclipse.jgit.internal.storage.dfs;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.time.Duration;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.stream.LongStream;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.LongStream;
+import org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheConfig.DfsBlockCachePackExtConfig;
import org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheConfig.IndexEventConsumer;
import org.eclipse.jgit.internal.storage.pack.PackExt;
import org.eclipse.jgit.junit.TestRepository;
@@ -39,14 +43,35 @@ import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+@RunWith(Parameterized.class)
public class DfsBlockCacheTest {
@Rule
public TestName testName = new TestName();
+
private TestRng rng;
+
private DfsBlockCache cache;
+
private ExecutorService pool;
+ private enum CacheType {
+ SINGLE_TABLE_CLOCK_BLOCK_CACHE, EXT_SPLIT_TABLE_CLOCK_BLOCK_CACHE
+ }
+
+ @Parameters(name = "cache type: {0}")
+ public static Iterable<? extends Object> data() {
+ return Arrays.asList(CacheType.SINGLE_TABLE_CLOCK_BLOCK_CACHE,
+ CacheType.EXT_SPLIT_TABLE_CLOCK_BLOCK_CACHE);
+ }
+
+ @Parameter
+ public CacheType cacheType;
+
@Before
public void setUp() {
rng = new TestRng(testName.getMethodName());
@@ -448,8 +473,28 @@ public class DfsBlockCacheTest {
}
private void resetCache(int concurrencyLevel) {
- DfsBlockCache.reconfigure(new DfsBlockCacheConfig().setBlockSize(512)
- .setConcurrencyLevel(concurrencyLevel).setBlockLimit(1 << 20));
+ DfsBlockCacheConfig cacheConfig = new DfsBlockCacheConfig()
+ .setBlockSize(512).setConcurrencyLevel(concurrencyLevel)
+ .setBlockLimit(1 << 20);
+ switch (cacheType) {
+ case SINGLE_TABLE_CLOCK_BLOCK_CACHE:
+ // SINGLE_TABLE_CLOCK_BLOCK_CACHE doesn't modify the config.
+ break;
+ case EXT_SPLIT_TABLE_CLOCK_BLOCK_CACHE:
+ List<DfsBlockCachePackExtConfig> packExtCacheConfigs = new ArrayList<>();
+ for (PackExt packExt : PackExt.values()) {
+ DfsBlockCacheConfig extCacheConfig = new DfsBlockCacheConfig()
+ .setBlockSize(512).setConcurrencyLevel(concurrencyLevel)
+ .setBlockLimit(1 << 20)
+ .setPackExtCacheConfigurations(packExtCacheConfigs);
+ packExtCacheConfigs.add(new DfsBlockCachePackExtConfig(
+ EnumSet.of(packExt), extCacheConfig));
+ }
+ cacheConfig.setPackExtCacheConfigurations(packExtCacheConfigs);
+ break;
+ }
+ assertNotNull(cacheConfig);
+ DfsBlockCache.reconfigure(cacheConfig);
cache = DfsBlockCache.getInstance();
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollectorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollectorTest.java
index ab998951f3..80bd689084 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollectorTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollectorTest.java
@@ -6,6 +6,7 @@ import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.IN
import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.UNREACHABLE_GARBAGE;
import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK;
import static org.eclipse.jgit.internal.storage.pack.PackExt.REFTABLE;
+import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@@ -15,11 +16,18 @@ import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.IOException;
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.util.Arrays;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.internal.storage.commitgraph.CommitGraph;
+import org.eclipse.jgit.internal.storage.commitgraph.CommitGraphWriter;
import org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource;
+import org.eclipse.jgit.internal.storage.file.PackBitmapIndex;
+import org.eclipse.jgit.internal.storage.pack.PackExt;
+import org.eclipse.jgit.internal.storage.reftable.LogCursor;
import org.eclipse.jgit.internal.storage.reftable.RefCursor;
import org.eclipse.jgit.internal.storage.reftable.ReftableConfig;
import org.eclipse.jgit.internal.storage.reftable.ReftableReader;
@@ -28,9 +36,12 @@ import org.eclipse.jgit.junit.MockSystemReader;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevBlob;
@@ -38,6 +49,7 @@ import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.storage.pack.PackConfig;
import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.util.GitTimeParser;
import org.eclipse.jgit.util.SystemReader;
import org.junit.After;
import org.junit.Before;
@@ -978,7 +990,7 @@ public class DfsGarbageCollectorTest {
}
@Test
- public void produceCommitGraphAllRefsIncludedFromDisk() throws Exception {
+ public void produceCommitGraphOnlyHeadsAndTags() throws Exception {
String tag = "refs/tags/tag1";
String head = "refs/heads/head1";
String nonHead = "refs/something/nonHead";
@@ -1000,19 +1012,20 @@ public class DfsGarbageCollectorTest {
CommitGraph cg = gcPack.getCommitGraph(reader);
assertNotNull(cg);
- assertTrue("all commits in commit graph", cg.getCommitCnt() == 3);
+ assertTrue("Only heads and tags reachable commits in commit graph",
+ cg.getCommitCnt() == 2);
// GC packed
assertTrue("tag referenced commit is in graph",
cg.findGraphPosition(rootCommitTagged) != -1);
assertTrue("head referenced commit is in graph",
cg.findGraphPosition(headTip) != -1);
- // GC_REST packed
- assertTrue("nonHead referenced commit is in graph",
- cg.findGraphPosition(nonHeadTip) != -1);
+ // GC_REST not in commit graph
+ assertEquals("nonHead referenced commit is NOT in graph",
+ -1, cg.findGraphPosition(nonHeadTip));
}
@Test
- public void produceCommitGraphAllRefsIncludedFromCache() throws Exception {
+ public void produceCommitGraphOnlyHeadsAndTagsIncludedFromCache() throws Exception {
String tag = "refs/tags/tag1";
String head = "refs/heads/head1";
String nonHead = "refs/something/nonHead";
@@ -1042,15 +1055,16 @@ public class DfsGarbageCollectorTest {
assertTrue("commit graph read time is recorded",
reader.stats.readCommitGraphMicros > 0);
- assertTrue("all commits in commit graph", cachedCG.getCommitCnt() == 3);
+ assertTrue("Only heads and tags reachable commits in commit graph",
+ cachedCG.getCommitCnt() == 2);
// GC packed
assertTrue("tag referenced commit is in graph",
cachedCG.findGraphPosition(rootCommitTagged) != -1);
assertTrue("head referenced commit is in graph",
cachedCG.findGraphPosition(headTip) != -1);
- // GC_REST packed
- assertTrue("nonHead referenced commit is in graph",
- cachedCG.findGraphPosition(nonHeadTip) != -1);
+ // GC_REST not in commit graph
+ assertEquals("nonHead referenced commit is not in graph",
+ -1, cachedCG.findGraphPosition(nonHeadTip));
}
@Test
@@ -1100,6 +1114,270 @@ public class DfsGarbageCollectorTest {
}
}
+ @Test
+ public void produceCommitGraphAndBloomFilter() throws Exception {
+ String head = "refs/heads/head1";
+
+ git.branch(head).commit().message("0").noParents().create();
+
+ gcWithCommitGraphAndBloomFilter();
+
+ assertEquals(1, odb.getPacks().length);
+ DfsPackFile pack = odb.getPacks()[0];
+ DfsPackDescription desc = pack.getPackDescription();
+ CommitGraphWriter.Stats stats = desc.getCommitGraphStats();
+ assertNotNull(stats);
+ assertEquals(1, stats.getChangedPathFiltersComputed());
+ }
+
+ @Test
+ public void testReadChangedPathConfigAsFalse() throws Exception {
+ String head = "refs/heads/head1";
+ git.branch(head).commit().message("0").noParents().create();
+ gcWithCommitGraphAndBloomFilter();
+
+ Config repoConfig = odb.getRepository().getConfig();
+ repoConfig.setBoolean(ConfigConstants.CONFIG_COMMIT_GRAPH_SECTION, null,
+ ConfigConstants.CONFIG_KEY_READ_CHANGED_PATHS, false);
+
+ DfsPackFile gcPack = odb.getPacks()[0];
+ try (DfsReader reader = odb.newReader()) {
+ CommitGraph cg = gcPack.getCommitGraph(reader);
+ assertNull(cg.getChangedPathFilter(0));
+ }
+ }
+
+ @Test
+ public void testReadChangedPathConfigAsTrue() throws Exception {
+ String head = "refs/heads/head1";
+ git.branch(head).commit().message("0").noParents().create();
+ gcWithCommitGraphAndBloomFilter();
+
+ Config repoConfig = odb.getRepository().getConfig();
+ repoConfig.setBoolean(ConfigConstants.CONFIG_COMMIT_GRAPH_SECTION, null,
+ ConfigConstants.CONFIG_KEY_READ_CHANGED_PATHS, true);
+
+ DfsPackFile gcPack = odb.getPacks()[0];
+ try (DfsReader reader = odb.newReader()) {
+ CommitGraph cg = gcPack.getCommitGraph(reader);
+ assertNotNull(cg.getChangedPathFilter(0));
+ }
+ }
+
+ @Test
+ public void objectSizeIdx_reachableBlob_bigEnough_indexed() throws Exception {
+ String master = "refs/heads/master";
+ RevCommit root = git.branch(master).commit().message("root").noParents()
+ .create();
+ RevBlob headsBlob = git.blob("twelve bytes");
+ git.branch(master).commit()
+ .message("commit on head")
+ .add("file.txt", headsBlob)
+ .parent(root)
+ .create();
+
+ gcWithObjectSizeIndex(10);
+
+ odb.getReaderOptions().setUseObjectSizeIndex(true);
+ DfsReader reader = odb.newReader();
+ DfsPackFile gcPack = findFirstBySource(odb.getPacks(), GC);
+ assertTrue(gcPack.hasObjectSizeIndex(reader));
+ assertEquals(12, gcPack.getIndexedObjectSize(reader,
+ gcPack.findIdxPosition(reader, headsBlob)));
+ }
+
+ @Test
+ public void objectSizeIdx_reachableBlob_tooSmall_notIndexed() throws Exception {
+ String master = "refs/heads/master";
+ RevCommit root = git.branch(master).commit().message("root").noParents()
+ .create();
+ RevBlob tooSmallBlob = git.blob("small");
+ git.branch(master).commit()
+ .message("commit on head")
+ .add("small.txt", tooSmallBlob)
+ .parent(root)
+ .create();
+
+ gcWithObjectSizeIndex(10);
+
+ odb.getReaderOptions().setUseObjectSizeIndex(true);
+ DfsReader reader = odb.newReader();
+ DfsPackFile gcPack = findFirstBySource(odb.getPacks(), GC);
+ assertTrue(gcPack.hasObjectSizeIndex(reader));
+ assertEquals(-1, gcPack.getIndexedObjectSize(reader,
+ gcPack.findIdxPosition(reader, tooSmallBlob)));
+ }
+
+ @Test
+ public void objectSizeIndex_unreachableGarbage_noIdx() throws Exception {
+ String master = "refs/heads/master";
+ RevCommit root = git.branch(master).commit().message("root").noParents()
+ .create();
+ git.branch(master).commit()
+ .message("commit on head")
+ .add("file.txt", git.blob("a blob"))
+ .parent(root)
+ .create();
+ git.update(master, root); // blob is unreachable
+ gcWithObjectSizeIndex(0);
+
+ DfsReader reader = odb.newReader();
+ DfsPackFile gcRestPack = findFirstBySource(odb.getPacks(), UNREACHABLE_GARBAGE);
+ assertFalse(gcRestPack.hasObjectSizeIndex(reader));
+ }
+
+ @Test
+ public void bitmapIndexWrittenDuringGc() throws Exception {
+ int numBranches = 2;
+ int commitsPerBranch = 50;
+
+ RevCommit commit0 = commit().message("0").create();
+ git.update("branch0", commit0);
+ RevCommit branch1 = commitChain(commit0, commitsPerBranch);
+ git.update("branch1", branch1);
+ RevCommit branch2 = commitChain(commit0, commitsPerBranch);
+ git.update("branch2", branch2);
+
+ int contiguousCommitCount = 5;
+ int recentCommitSpan = 2;
+ int recentCommitCount = 10;
+ int distantCommitSpan = 5;
+
+ PackConfig packConfig = new PackConfig();
+ packConfig.setBitmapContiguousCommitCount(contiguousCommitCount);
+ packConfig.setBitmapRecentCommitSpan(recentCommitSpan);
+ packConfig.setBitmapRecentCommitCount(recentCommitCount);
+ packConfig.setBitmapDistantCommitSpan(distantCommitSpan);
+
+ DfsGarbageCollector gc = new DfsGarbageCollector(repo);
+ gc.setPackConfig(packConfig);
+ run(gc);
+
+ DfsPackFile pack = odb.getPacks()[0];
+ PackBitmapIndex bitmapIndex = pack.getBitmapIndex(odb.newReader());
+ assertTrue("pack file has bitmap index extension",
+ pack.getPackDescription().hasFileExt(PackExt.BITMAP_INDEX));
+
+ int recentCommitsPerBranch = (recentCommitCount - contiguousCommitCount
+ - 1) / recentCommitSpan;
+ assertEquals("expected recent commits", 2, recentCommitsPerBranch);
+
+ int distantCommitsPerBranch = (commitsPerBranch - 1 - recentCommitCount)
+ / distantCommitSpan;
+ assertEquals("expected distant commits", 7, distantCommitsPerBranch);
+
+ int branchBitmapsCount = contiguousCommitCount
+ + numBranches
+ * (recentCommitsPerBranch
+ + distantCommitsPerBranch);
+ assertEquals("expected bitmaps count", 23, branchBitmapsCount);
+ assertEquals("bitmap index has expected number of bitmaps",
+ branchBitmapsCount,
+ bitmapIndex.getBitmapCount());
+
+ // The count is just a function of whether any bitmaps happen to
+ // compress efficiently against the others in the index. We expect for
+ // this test that this there will be at least one like this, but the
+ // actual count is situation-specific
+ assertTrue("bitmap index has xor-compressed bitmaps",
+ bitmapIndex.getXorBitmapCount() > 0);
+ }
+
+ @Test
+ public void gitGCWithRefLogExpire() throws Exception {
+ String master = "refs/heads/master";
+ RevCommit commit0 = commit().message("0").create();
+ RevCommit commit1 = commit().message("1").parent(commit0).create();
+ git.update(master, commit1);
+ DfsGarbageCollector gc = new DfsGarbageCollector(repo);
+ gc.setReftableConfig(new ReftableConfig());
+ run(gc);
+ DfsPackDescription t1 = odb.newPack(INSERT);
+ Ref next = new ObjectIdRef.PeeledNonTag(Ref.Storage.LOOSE,
+ "refs/heads/next", commit0.copy());
+ Instant currentDay = Instant.now();
+ Instant ten_days_ago = GitTimeParser.parseInstant("10 days ago");
+ Instant twenty_days_ago = GitTimeParser.parseInstant("20 days ago");
+ Instant thirty_days_ago = GitTimeParser.parseInstant("30 days ago");
+ Instant fifty_days_ago = GitTimeParser.parseInstant("50 days ago");
+ final ZoneOffset offset = ZoneOffset.ofHours(-8);
+ PersonIdent who2 = new PersonIdent("J.Author", "authemail", currentDay,
+ offset);
+ PersonIdent who3 = new PersonIdent("J.Author", "authemail",
+ ten_days_ago, offset);
+ PersonIdent who4 = new PersonIdent("J.Author", "authemail",
+ twenty_days_ago, offset);
+ PersonIdent who5 = new PersonIdent("J.Author", "authemail",
+ thirty_days_ago, offset);
+ PersonIdent who6 = new PersonIdent("J.Author", "authemail",
+ fifty_days_ago, offset);
+
+ try (DfsOutputStream out = odb.writeFile(t1, REFTABLE)) {
+ ReftableWriter w = new ReftableWriter(out);
+ w.setMinUpdateIndex(42);
+ w.setMaxUpdateIndex(42);
+ w.begin();
+ w.sortAndWriteRefs(Collections.singleton(next));
+ w.writeLog("refs/heads/branch", 1, who2, ObjectId.zeroId(),id(2), "Branch Message");
+ w.writeLog("refs/heads/branch1", 2, who3, ObjectId.zeroId(),id(3), "Branch Message1");
+ w.writeLog("refs/heads/branch2", 2, who4, ObjectId.zeroId(),id(4), "Branch Message2");
+ w.writeLog("refs/heads/branch3", 2, who5, ObjectId.zeroId(),id(5), "Branch Message3");
+ w.writeLog("refs/heads/branch4", 2, who6, ObjectId.zeroId(),id(6), "Branch Message4");
+ w.finish();
+ t1.addFileExt(REFTABLE);
+ t1.setReftableStats(w.getStats());
+ }
+ odb.commitPack(Collections.singleton(t1), null);
+
+ gc = new DfsGarbageCollector(repo);
+ gc.setReftableConfig(new ReftableConfig());
+ // Expire ref log entries older than 30 days
+ gc.setRefLogExpire(thirty_days_ago);
+ run(gc);
+
+ // Single GC pack present with all objects.
+ assertEquals(1, odb.getPacks().length);
+ DfsPackFile pack = odb.getPacks()[0];
+ DfsPackDescription desc = pack.getPackDescription();
+
+ DfsReftable table = new DfsReftable(DfsBlockCache.getInstance(), desc);
+ try (DfsReader ctx = odb.newReader();
+ ReftableReader rr = table.open(ctx);
+ RefCursor rc = rr.allRefs();
+ LogCursor lc = rr.allLogs()) {
+ assertTrue(rc.next());
+ assertEquals(master, rc.getRef().getName());
+ assertEquals(commit1, rc.getRef().getObjectId());
+ assertTrue(rc.next());
+ assertEquals(next.getName(), rc.getRef().getName());
+ assertEquals(commit0, rc.getRef().getObjectId());
+ assertFalse(rc.next());
+ assertTrue(lc.next());
+ assertEquals(lc.getRefName(),"refs/heads/branch");
+ assertTrue(lc.next());
+ assertEquals(lc.getRefName(),"refs/heads/branch1");
+ assertTrue(lc.next());
+ assertEquals(lc.getRefName(),"refs/heads/branch2");
+ // Old entries are purged
+ assertFalse(lc.next());
+ }
+ }
+
+
+ private RevCommit commitChain(RevCommit parent, int length)
+ throws Exception {
+ for (int i = 0; i < length; i++) {
+ parent = commit().message("" + i).parent(parent).create();
+ }
+ return parent;
+ }
+
+ private static DfsPackFile findFirstBySource(DfsPackFile[] packs, PackSource source) {
+ return Arrays.stream(packs)
+ .filter(p -> p.getPackDescription().getPackSource() == source)
+ .findFirst().get();
+ }
+
private TestRepository<InMemoryRepository>.CommitBuilder commit() {
return git.commit();
}
@@ -1110,6 +1388,19 @@ public class DfsGarbageCollectorTest {
run(gc);
}
+ private void gcWithCommitGraphAndBloomFilter() throws IOException {
+ DfsGarbageCollector gc = new DfsGarbageCollector(repo);
+ gc.setWriteCommitGraph(true);
+ gc.setWriteBloomFilter(true);
+ run(gc);
+ }
+
+ private void gcWithObjectSizeIndex(int threshold) throws IOException {
+ DfsGarbageCollector gc = new DfsGarbageCollector(repo);
+ gc.getPackConfig().setMinBytesForObjSizeIndex(threshold);
+ run(gc);
+ }
+
private void gcNoTtl() throws IOException {
DfsGarbageCollector gc = new DfsGarbageCollector(repo);
gc.setGarbageTtl(0, TimeUnit.MILLISECONDS); // disable TTL
@@ -1162,4 +1453,12 @@ public class DfsGarbageCollectorTest {
}
return cnt;
}
+ private static ObjectId id(int i) {
+ byte[] buf = new byte[OBJECT_ID_LENGTH];
+ buf[0] = (byte) (i & 0xff);
+ buf[1] = (byte) ((i >>> 8) & 0xff);
+ buf[2] = (byte) ((i >>> 16) & 0xff);
+ buf[3] = (byte) (i >>> 24);
+ return ObjectId.fromRaw(buf);
+ }
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsInserterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsInserterTest.java
index adf577b0f7..efa98de549 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsInserterTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsInserterTest.java
@@ -10,6 +10,8 @@
package org.eclipse.jgit.internal.storage.dfs;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_MIN_BYTES_OBJ_SIZE_INDEX;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_PACK_SECTION;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
@@ -29,11 +31,16 @@ import org.eclipse.jgit.internal.storage.pack.PackExt;
import org.eclipse.jgit.junit.JGitTestUtil;
import org.eclipse.jgit.junit.TestRng;
import org.eclipse.jgit.lib.AbbreviatedObjectId;
+import org.eclipse.jgit.lib.CommitBuilder;
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.ObjectLoader;
import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.TagBuilder;
+import org.eclipse.jgit.lib.TreeFormatter;
import org.eclipse.jgit.util.IO;
import org.eclipse.jgit.util.RawParseUtils;
import org.junit.Before;
@@ -207,7 +214,7 @@ public class DfsInserterTest {
}
@Test
- public void testNoCheckExisting() throws IOException {
+ public void testNoDuplicates() throws IOException {
byte[] contents = Constants.encode("foo");
ObjectId fooId;
try (ObjectInserter ins = db.newObjectInserter()) {
@@ -217,21 +224,20 @@ public class DfsInserterTest {
assertEquals(1, db.getObjectDatabase().listPacks().size());
try (ObjectInserter ins = db.newObjectInserter()) {
- ((DfsInserter) ins).checkExisting(false);
+ ins.insert(Constants.OBJ_BLOB, Constants.encode("bar"));
assertEquals(fooId, ins.insert(Constants.OBJ_BLOB, contents));
ins.flush();
}
assertEquals(2, db.getObjectDatabase().listPacks().size());
- // Verify that we have a foo in both INSERT packs.
+ // Newer packs are first. Verify that foo is only in the second pack
try (DfsReader reader = new DfsReader(db.getObjectDatabase())) {
DfsPackFile packs[] = db.getObjectDatabase().getPacks();
-
assertEquals(2, packs.length);
DfsPackFile p1 = packs[0];
assertEquals(PackSource.INSERT,
p1.getPackDescription().getPackSource());
- assertTrue(p1.hasObject(reader, fooId));
+ assertFalse(p1.hasObject(reader, fooId));
DfsPackFile p2 = packs[1];
assertEquals(PackSource.INSERT,
@@ -240,6 +246,73 @@ public class DfsInserterTest {
}
}
+ @Test
+ public void testObjectSizePopulated() throws IOException {
+ // Blob
+ byte[] contents = Constants.encode("foo");
+
+ // Commit
+ PersonIdent person = new PersonIdent("Committer a", "jgit@eclipse.org");
+ CommitBuilder c = new CommitBuilder();
+ c.setAuthor(person);
+ c.setCommitter(person);
+ c.setTreeId(ObjectId
+ .fromString("45c4c6767a3945815371a7016532751dd558be40"));
+ c.setMessage("commit message");
+
+ // Tree
+ TreeFormatter treeBuilder = new TreeFormatter(2);
+ treeBuilder.append("filea", FileMode.REGULAR_FILE, ObjectId
+ .fromString("45c4c6767a3945815371a7016532751dd558be40"));
+ treeBuilder.append("fileb", FileMode.GITLINK, ObjectId
+ .fromString("1c458e25ca624bb8d4735bec1379a4a29ba786d0"));
+
+ // Tag
+ TagBuilder tagBuilder = new TagBuilder();
+ tagBuilder.setObjectId(
+ ObjectId.fromString("c97fe131649e80de55bd153e9a8d8629f7ca6932"),
+ Constants.OBJ_COMMIT);
+ tagBuilder.setTag("short name");
+
+ try (DfsInserter ins = (DfsInserter) db.newObjectInserter()) {
+ ObjectId aBlob = ins.insert(Constants.OBJ_BLOB, contents);
+ assertEquals(contents.length,
+ ins.objectMap.get(aBlob).getFullSize());
+
+ ObjectId aCommit = ins.insert(c);
+ assertEquals(174, ins.objectMap.get(aCommit).getFullSize());
+
+ ObjectId tree = ins.insert(treeBuilder);
+ assertEquals(66, ins.objectMap.get(tree).getFullSize());
+
+ ObjectId tag = ins.insert(tagBuilder);
+ assertEquals(76, ins.objectMap.get(tag).getFullSize());
+ }
+ }
+
+ @Test
+ public void testObjectSizeIndexOnInsert() throws IOException {
+ db.getConfig().setInt(CONFIG_PACK_SECTION, null,
+ CONFIG_KEY_MIN_BYTES_OBJ_SIZE_INDEX, 0);
+ db.getObjectDatabase().getReaderOptions().setUseObjectSizeIndex(true);
+
+ byte[] contents = Constants.encode("foo");
+ ObjectId fooId;
+ try (ObjectInserter ins = db.newObjectInserter()) {
+ fooId = ins.insert(Constants.OBJ_BLOB, contents);
+ ins.flush();
+ }
+
+ DfsReader reader = db.getObjectDatabase().newReader();
+ assertEquals(1, db.getObjectDatabase().listPacks().size());
+ DfsPackFile insertPack = db.getObjectDatabase().getPacks()[0];
+ assertEquals(PackSource.INSERT,
+ insertPack.getPackDescription().getPackSource());
+ assertTrue(insertPack.hasObjectSizeIndex(reader));
+ assertEquals(contents.length, insertPack.getIndexedObjectSize(reader,
+ insertPack.findIdxPosition(reader, fooId)));
+ }
+
private static String readString(ObjectLoader loader) throws IOException {
return RawParseUtils.decode(readStream(loader));
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackCompacterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackCompacterTest.java
index c516e30f50..c3b6aa85a2 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackCompacterTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackCompacterTest.java
@@ -12,13 +12,18 @@ package org.eclipse.jgit.internal.storage.dfs;
import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.COMPACT;
import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.INSERT;
+import static org.eclipse.jgit.internal.storage.pack.PackExt.OBJECT_SIZE_INDEX;
import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
+import java.util.Arrays;
+import java.util.Optional;
import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Before;
import org.junit.Test;
@@ -98,6 +103,40 @@ public class DfsPackCompacterTest {
pack.getPackDescription().getEstimatedPackSize());
}
+ @Test
+ public void testObjectSizeIndexWritten() throws Exception {
+ writeObjectSizeIndex(repo, true);
+ RevCommit commit0 = commit().message("0").create();
+ RevCommit commit1 = commit().message("1").parent(commit0).create();
+ git.update("master", commit1);
+
+ compact();
+
+ Optional<DfsPackFile> compactPack = Arrays.stream(odb.getPacks())
+ .filter(pack -> pack.getPackDescription()
+ .getPackSource() == COMPACT)
+ .findFirst();
+ assertTrue(compactPack.isPresent());
+ assertTrue(compactPack.get().getPackDescription().hasFileExt(OBJECT_SIZE_INDEX));
+ }
+
+ @Test
+ public void testObjectSizeIndexNotWritten() throws Exception {
+ writeObjectSizeIndex(repo, false);
+ RevCommit commit0 = commit().message("0").create();
+ RevCommit commit1 = commit().message("1").parent(commit0).create();
+ git.update("master", commit1);
+
+ compact();
+
+ Optional<DfsPackFile> compactPack = Arrays.stream(odb.getPacks())
+ .filter(pack -> pack.getPackDescription()
+ .getPackSource() == COMPACT)
+ .findFirst();
+ assertTrue(compactPack.isPresent());
+ assertFalse(compactPack.get().getPackDescription().hasFileExt(OBJECT_SIZE_INDEX));
+ }
+
private TestRepository<InMemoryRepository>.CommitBuilder commit() {
return git.commit();
}
@@ -108,4 +147,9 @@ public class DfsPackCompacterTest {
compactor.compact(null);
odb.clearCache();
}
+
+ private static void writeObjectSizeIndex(DfsRepository repo, boolean should) {
+ repo.getConfig().setInt(ConfigConstants.CONFIG_PACK_SECTION, null,
+ ConfigConstants.CONFIG_KEY_MIN_BYTES_OBJ_SIZE_INDEX, should ? 0 : -1);
+ }
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackFileTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackFileTest.java
index ea5787309b..f2129fd3c5 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackFileTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackFileTest.java
@@ -10,19 +10,39 @@
package org.eclipse.jgit.internal.storage.dfs;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_COMMIT_GRAPH_SECTION;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_MIN_BYTES_OBJ_SIZE_INDEX;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_READ_CHANGED_PATHS;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_PACK_SECTION;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
import java.util.zip.Deflater;
+import org.eclipse.jgit.internal.storage.commitgraph.CommitGraph;
+import org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource;
+import org.eclipse.jgit.internal.storage.dfs.DfsReader.PackLoadListener;
+import org.eclipse.jgit.internal.storage.file.PackBitmapIndex;
import org.eclipse.jgit.internal.storage.pack.PackExt;
import org.eclipse.jgit.internal.storage.pack.PackOutputStream;
import org.eclipse.jgit.internal.storage.pack.PackWriter;
import org.eclipse.jgit.junit.JGitTestUtil;
+import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.junit.TestRng;
+import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.storage.pack.PackConfig;
+import org.eclipse.jgit.transport.ReceiveCommand;
import org.junit.Before;
import org.junit.Test;
@@ -100,7 +120,178 @@ public class DfsPackFileTest {
assertPackSize();
}
- private void setupPack(int bs, int ps) throws IOException {
+ @Test
+ public void testLoadObjectSizeIndex() throws IOException {
+ bypassCache = false;
+ clearCache = true;
+ setObjectSizeIndexMinBytes(0);
+ ObjectId blobId = setupPack(512, 800);
+
+ db.getObjectDatabase().getReaderOptions().setUseObjectSizeIndex(true);
+ DfsReader reader = db.getObjectDatabase().newReader();
+ DfsPackFile pack = db.getObjectDatabase().getPacks()[0];
+ assertTrue(pack.hasObjectSizeIndex(reader));
+ assertEquals(800, pack.getIndexedObjectSize(reader,
+ pack.findIdxPosition(reader, blobId)));
+ }
+
+ @Test
+ public void testGetBitmapIndex() throws IOException {
+ bypassCache = false;
+ clearCache = true;
+ ObjectId objectId = setupPack(512, 800);
+
+ // Add a ref for GC
+ BatchRefUpdate batchRefUpdate = db.getRefDatabase().newBatchUpdate();
+ batchRefUpdate.addCommand(new ReceiveCommand(ObjectId.zeroId(),
+ objectId, "refs/heads/master"));
+ try (RevWalk rw = new RevWalk(db)) {
+ batchRefUpdate.execute(rw, NullProgressMonitor.INSTANCE);
+ }
+ DfsGarbageCollector gc = new DfsGarbageCollector(db);
+ gc.pack(NullProgressMonitor.INSTANCE);
+
+ DfsReader reader = db.getObjectDatabase().newReader();
+ PackBitmapIndex bitmapIndex = db.getObjectDatabase().getPacks()[0]
+ .getBitmapIndex(reader);
+ assertNotNull(bitmapIndex);
+ assertEquals(1, bitmapIndex.getObjectCount());
+ }
+
+ @Test
+ public void testGetBitmapIndex_noBitmaps() throws IOException {
+ bypassCache = false;
+ clearCache = true;
+ setupPack(512, 800);
+
+ DfsReader reader = db.getObjectDatabase().newReader();
+ PackBitmapIndex bitmapIndex = db.getObjectDatabase().getPacks()[0]
+ .getBitmapIndex(reader);
+ assertNull(bitmapIndex);
+ }
+
+ @Test
+ public void testLoadObjectSizeIndex_noIndex() throws IOException {
+ bypassCache = false;
+ clearCache = true;
+ setObjectSizeIndexMinBytes(-1);
+ setupPack(512, 800);
+
+ DfsReader reader = db.getObjectDatabase().newReader();
+ DfsPackFile pack = db.getObjectDatabase().getPacks()[0];
+ assertFalse(pack.hasObjectSizeIndex(reader));
+ }
+
+ private static class TestPackLoadListener implements PackLoadListener {
+ final Map<PackExt, Integer> indexLoadCount = new HashMap<>();
+
+ int blockLoadCount;
+
+ @SuppressWarnings("boxing")
+ @Override
+ public void onIndexLoad(String packName, PackSource src, PackExt ext,
+ long size, Object loadedIdx) {
+ indexLoadCount.merge(ext, 1, Integer::sum);
+ }
+
+ @Override
+ public void onBlockLoad(String packName, PackSource src, PackExt ext, long position,
+ DfsBlockData dfsBlockData) {
+ blockLoadCount += 1;
+ }
+ }
+
+ @Test
+ public void testIndexLoadCallback_indexNotInCache() throws IOException {
+ bypassCache = false;
+ clearCache = true;
+ setObjectSizeIndexMinBytes(-1);
+ setupPack(512, 800);
+
+ TestPackLoadListener tal = new TestPackLoadListener();
+ DfsReader reader = db.getObjectDatabase().newReader();
+ reader.addPackLoadListener(tal);
+ DfsPackFile pack = db.getObjectDatabase().getPacks()[0];
+ pack.getPackIndex(reader);
+
+ assertEquals(1, tal.indexLoadCount.get(PackExt.INDEX).intValue());
+ }
+
+ @Test
+ public void testIndexLoadCallback_indexInCache() throws IOException {
+ bypassCache = false;
+ clearCache = false;
+ setObjectSizeIndexMinBytes(-1);
+ setupPack(512, 800);
+
+ TestPackLoadListener tal = new TestPackLoadListener();
+ DfsReader reader = db.getObjectDatabase().newReader();
+ reader.addPackLoadListener(tal);
+ DfsPackFile pack = db.getObjectDatabase().getPacks()[0];
+ pack.getPackIndex(reader);
+ pack.getPackIndex(reader);
+ pack.getPackIndex(reader);
+
+ assertEquals(1, tal.indexLoadCount.get(PackExt.INDEX).intValue());
+ }
+
+ @Test
+ public void testIndexLoadCallback_multipleReads() throws IOException {
+ bypassCache = false;
+ clearCache = true;
+ setObjectSizeIndexMinBytes(-1);
+ setupPack(512, 800);
+
+ TestPackLoadListener tal = new TestPackLoadListener();
+ DfsReader reader = db.getObjectDatabase().newReader();
+ reader.addPackLoadListener(tal);
+ DfsPackFile pack = db.getObjectDatabase().getPacks()[0];
+ pack.getPackIndex(reader);
+ pack.getPackIndex(reader);
+ pack.getPackIndex(reader);
+
+ assertEquals(1, tal.indexLoadCount.get(PackExt.INDEX).intValue());
+ }
+
+
+ @Test
+ public void testBlockLoadCallback_loadInCache() throws IOException {
+ bypassCache = false;
+ clearCache = true;
+ setObjectSizeIndexMinBytes(-1);
+ setupPack(512, 800);
+
+ TestPackLoadListener tal = new TestPackLoadListener();
+ DfsReader reader = db.getObjectDatabase().newReader();
+ reader.addPackLoadListener(tal);
+ DfsPackFile pack = db.getObjectDatabase().getPacks()[0];
+ ObjectId anObject = pack.getPackIndex(reader).getObjectId(0);
+ pack.get(reader, anObject).getBytes();
+ assertEquals(2, tal.blockLoadCount);
+ }
+
+ @Test
+ public void testExistenceOfBloomFilterAlongWithCommitGraph()
+ throws Exception {
+ try (TestRepository<InMemoryRepository> repository = new TestRepository<>(
+ db)) {
+ repository.branch("/refs/heads/main").commit().add("blob1", "blob1")
+ .create();
+ }
+ setReadChangedPaths(true);
+ DfsGarbageCollector gc = new DfsGarbageCollector(db);
+ gc.setWriteCommitGraph(true).setWriteBloomFilter(true)
+ .pack(NullProgressMonitor.INSTANCE);
+
+ DfsReader reader = db.getObjectDatabase().newReader();
+ CommitGraph cg = db.getObjectDatabase().getPacks()[0]
+ .getCommitGraph(reader);
+ assertNotNull(cg);
+ assertEquals(1, cg.getCommitCnt());
+ assertNotNull(cg.getChangedPathFilter(0));
+ }
+
+ private ObjectId setupPack(int bs, int ps) throws IOException {
DfsBlockCacheConfig cfg = new DfsBlockCacheConfig().setBlockSize(bs)
.setBlockLimit(bs * 100).setStreamRatio(bypassCache ? 0F : 1F);
DfsBlockCache.reconfigure(cfg);
@@ -108,18 +299,19 @@ public class DfsPackFileTest {
byte[] data = new TestRng(JGitTestUtil.getName()).nextBytes(ps);
DfsInserter ins = (DfsInserter) db.newObjectInserter();
ins.setCompressionLevel(Deflater.NO_COMPRESSION);
- ins.insert(Constants.OBJ_BLOB, data);
+ ObjectId blobId = ins.insert(Constants.OBJ_BLOB, data);
ins.flush();
if (clearCache) {
DfsBlockCache.reconfigure(cfg);
db.getObjectDatabase().clearCache();
}
+ return blobId;
}
private void assertPackSize() throws IOException {
try (DfsReader ctx = db.getObjectDatabase().newReader();
- PackWriter pw = new PackWriter(ctx);
+ PackWriter pw = new PackWriter(new PackConfig(), ctx);
ByteArrayOutputStream os = new ByteArrayOutputStream();
PackOutputStream out = new PackOutputStream(
NullProgressMonitor.INSTANCE, os, pw)) {
@@ -129,4 +321,14 @@ public class DfsPackFileTest {
assertEquals(packSize - (12 + 20), os.size());
}
}
+
+ private void setObjectSizeIndexMinBytes(int threshold) {
+ db.getConfig().setInt(CONFIG_PACK_SECTION, null,
+ CONFIG_KEY_MIN_BYTES_OBJ_SIZE_INDEX, threshold);
+ }
+
+ private void setReadChangedPaths(boolean enable) {
+ db.getConfig().setBoolean(CONFIG_COMMIT_GRAPH_SECTION, null,
+ CONFIG_KEY_READ_CHANGED_PATHS, enable);
+ }
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackParserTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackParserTest.java
new file mode 100644
index 0000000000..9d26978d66
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackParserTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2023, Google LLC and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.internal.storage.dfs;
+
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_MIN_BYTES_OBJ_SIZE_INDEX;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_PACK_SECTION;
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+
+import org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackList;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.transport.InMemoryPack;
+import org.eclipse.jgit.transport.PackParser;
+import org.junit.Before;
+import org.junit.Test;
+
+public class DfsPackParserTest {
+ private InMemoryRepository repo;
+
+
+ @Before
+ public void setUp() throws Exception {
+ DfsRepositoryDescription desc = new DfsRepositoryDescription("test");
+ repo = new InMemoryRepository(desc);
+ repo.getConfig().setInt(CONFIG_PACK_SECTION, null,
+ CONFIG_KEY_MIN_BYTES_OBJ_SIZE_INDEX, 0);
+ }
+
+ @Test
+ public void parse_writeObjSizeIdx() throws IOException {
+ InMemoryPack pack = new InMemoryPack();
+
+ // Sha1 of the blob "a"
+ ObjectId blobA = ObjectId
+ .fromString("2e65efe2a145dda7ee51d1741299f848e5bf752e");
+
+ pack.header(2);
+ pack.write(Constants.OBJ_BLOB << 4 | 1);
+ pack.deflate(new byte[] { 'a' });
+
+ pack.write(Constants.OBJ_REF_DELTA << 4 | 4);
+ pack.copyRaw(blobA);
+ pack.deflate(new byte[] { 0x1, 0x1, 0x1, 'b' });
+ pack.digest();
+
+ try (ObjectInserter ins = repo.newObjectInserter()) {
+ PackParser parser = ins.newPackParser(pack.toInputStream());
+ parser.parse(NullProgressMonitor.INSTANCE,
+ NullProgressMonitor.INSTANCE);
+ ins.flush();
+ }
+
+ repo.getObjectDatabase().getReaderOptions().setUseObjectSizeIndex(true);
+ DfsReader reader = repo.getObjectDatabase().newReader();
+ PackList packList = repo.getObjectDatabase().getPackList();
+ assertEquals(1, packList.packs.length);
+ assertEquals(1, packList.packs[0].getIndexedObjectSize(reader,
+ packList.packs[0].findIdxPosition(reader, blobA)));
+ }
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsReaderTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsReaderTest.java
new file mode 100644
index 0000000000..a0c228906e
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsReaderTest.java
@@ -0,0 +1,323 @@
+/*
+ * Copyright (C) 2023, Google LLC. and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.internal.storage.dfs;
+
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_MIN_BYTES_OBJ_SIZE_INDEX;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_PACK_SECTION;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource;
+import org.eclipse.jgit.internal.storage.dfs.DfsReader.PackLoadListener;
+import org.eclipse.jgit.internal.storage.pack.PackExt;
+import org.eclipse.jgit.junit.JGitTestUtil;
+import org.eclipse.jgit.junit.TestRng;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.junit.Before;
+import org.junit.Test;
+
+public class DfsReaderTest {
+ InMemoryRepository db;
+
+ @Before
+ public void setUp() {
+ db = new InMemoryRepository(new DfsRepositoryDescription("test"));
+ // These tests assume the object size index is enabled.
+ db.getObjectDatabase().getReaderOptions().setUseObjectSizeIndex(true);
+ }
+
+ @Test
+ public void getObjectSize_noIndex_blob() throws IOException {
+ ObjectId obj = insertBlobWithSize(100);
+ try (DfsReader ctx = db.getObjectDatabase().newReader()) {
+ long size = ctx.getObjectSize(obj, OBJ_BLOB);
+ assertEquals(100, size);
+ }
+ }
+
+ @Test
+ public void getObjectSize_noIndex_commit() throws IOException {
+ ObjectId obj = insertObjectWithSize(OBJ_COMMIT, 110);
+ try (DfsReader ctx = db.getObjectDatabase().newReader()) {
+ long size = ctx.getObjectSize(obj, OBJ_COMMIT);
+ assertEquals(110, size);
+ }
+ }
+
+ @Test
+ public void getObjectSize_index_indexedBlob() throws IOException {
+ setObjectSizeIndexMinBytes(100);
+ ObjectId obj = insertBlobWithSize(200);
+ try (DfsReader ctx = db.getObjectDatabase().newReader()) {
+ long size = ctx.getObjectSize(obj, OBJ_BLOB);
+ assertEquals(200, size);
+ }
+ }
+
+ @Test
+ public void getObjectSize_index_nonIndexedBlob() throws IOException {
+ setObjectSizeIndexMinBytes(100);
+ ObjectId obj = insertBlobWithSize(50);
+ try (DfsReader ctx = db.getObjectDatabase().newReader()) {
+ long size = ctx.getObjectSize(obj, OBJ_BLOB);
+ assertEquals(50, size);
+ }
+ }
+
+ @Test
+ public void getObjectSize_index_commit() throws IOException {
+ setObjectSizeIndexMinBytes(100);
+ insertBlobWithSize(110);
+ ObjectId obj = insertObjectWithSize(OBJ_COMMIT, 120);
+ try (DfsReader ctx = db.getObjectDatabase().newReader()) {
+ long size = ctx.getObjectSize(obj, OBJ_COMMIT);
+ assertEquals(120, size);
+ }
+ }
+
+ @Test
+ public void isNotLargerThan_objAboveThreshold() throws IOException {
+ setObjectSizeIndexMinBytes(100);
+ ObjectId obj = insertBlobWithSize(200);
+ try (DfsReader ctx = db.getObjectDatabase().newReader()) {
+ assertFalse("limit < threshold < obj",
+ ctx.isNotLargerThan(obj, OBJ_BLOB, 50));
+ assertEquals(1, ctx.stats.isNotLargerThanCallCount);
+ assertEquals(1, ctx.stats.objectSizeIndexHit);
+ assertEquals(0, ctx.stats.objectSizeIndexMiss);
+
+ assertFalse("limit = threshold < obj",
+ ctx.isNotLargerThan(obj, OBJ_BLOB, 100));
+ assertEquals(2, ctx.stats.isNotLargerThanCallCount);
+ assertEquals(2, ctx.stats.objectSizeIndexHit);
+ assertEquals(0, ctx.stats.objectSizeIndexMiss);
+
+ assertFalse("threshold < limit < obj",
+ ctx.isNotLargerThan(obj, OBJ_BLOB, 150));
+ assertEquals(3, ctx.stats.isNotLargerThanCallCount);
+ assertEquals(3, ctx.stats.objectSizeIndexHit);
+ assertEquals(0, ctx.stats.objectSizeIndexMiss);
+
+ assertTrue("threshold < limit = obj",
+ ctx.isNotLargerThan(obj, OBJ_BLOB, 200));
+ assertEquals(4, ctx.stats.isNotLargerThanCallCount);
+ assertEquals(4, ctx.stats.objectSizeIndexHit);
+ assertEquals(0, ctx.stats.objectSizeIndexMiss);
+
+ assertTrue("threshold < obj < limit",
+ ctx.isNotLargerThan(obj, OBJ_BLOB, 250));
+ assertEquals(5, ctx.stats.isNotLargerThanCallCount);
+ assertEquals(5, ctx.stats.objectSizeIndexHit);
+ assertEquals(0, ctx.stats.objectSizeIndexMiss);
+ }
+ }
+
+ @Test
+ public void isNotLargerThan_objBelowThreshold() throws IOException {
+ setObjectSizeIndexMinBytes(100);
+ insertBlobWithSize(1000); // index not empty
+ ObjectId obj = insertBlobWithSize(50);
+ try (DfsReader ctx = db.getObjectDatabase().newReader()) {
+ assertFalse("limit < obj < threshold",
+ ctx.isNotLargerThan(obj, OBJ_BLOB, 10));
+ assertEquals(1, ctx.stats.isNotLargerThanCallCount);
+ assertEquals(0, ctx.stats.objectSizeIndexHit);
+ assertEquals(1, ctx.stats.objectSizeIndexMiss);
+
+ assertTrue("limit = obj < threshold",
+ ctx.isNotLargerThan(obj, OBJ_BLOB, 50));
+ assertEquals(2, ctx.stats.isNotLargerThanCallCount);
+ assertEquals(0, ctx.stats.objectSizeIndexHit);
+ assertEquals(2, ctx.stats.objectSizeIndexMiss);
+
+ assertTrue("obj < limit < threshold",
+ ctx.isNotLargerThan(obj, OBJ_BLOB, 80));
+ assertEquals(3, ctx.stats.isNotLargerThanCallCount);
+ assertEquals(0, ctx.stats.objectSizeIndexHit);
+ assertEquals(3, ctx.stats.objectSizeIndexMiss);
+
+ assertTrue("obj < limit = threshold",
+ ctx.isNotLargerThan(obj, OBJ_BLOB, 100));
+ assertEquals(4, ctx.stats.isNotLargerThanCallCount);
+ assertEquals(0, ctx.stats.objectSizeIndexHit);
+ assertEquals(4, ctx.stats.objectSizeIndexMiss);
+
+ assertTrue("obj < threshold < limit",
+ ctx.isNotLargerThan(obj, OBJ_BLOB, 120));
+ assertEquals(5, ctx.stats.isNotLargerThanCallCount);
+ assertEquals(0, ctx.stats.objectSizeIndexHit);
+ assertEquals(5, ctx.stats.objectSizeIndexMiss);
+ }
+ }
+
+ @Test
+ public void isNotLargerThan_emptyIdx() throws IOException {
+ setObjectSizeIndexMinBytes(100);
+ ObjectId obj = insertBlobWithSize(10);
+ try (DfsReader ctx = db.getObjectDatabase().newReader()) {
+ assertFalse(ctx.isNotLargerThan(obj, OBJ_BLOB, 0));
+ assertTrue(ctx.isNotLargerThan(obj, OBJ_BLOB, 10));
+ assertTrue(ctx.isNotLargerThan(obj, OBJ_BLOB, 40));
+ assertTrue(ctx.isNotLargerThan(obj, OBJ_BLOB, 50));
+ assertTrue(ctx.isNotLargerThan(obj, OBJ_BLOB, 100));
+
+ assertEquals(5, ctx.stats.isNotLargerThanCallCount);
+ assertEquals(5, ctx.stats.objectSizeIndexMiss);
+ assertEquals(0, ctx.stats.objectSizeIndexHit);
+ }
+ }
+
+ @Test
+ public void isNotLargerThan_noObjectSizeIndex() throws IOException {
+ setObjectSizeIndexMinBytes(-1);
+ ObjectId obj = insertBlobWithSize(10);
+ try (DfsReader ctx = db.getObjectDatabase().newReader()) {
+ assertFalse(ctx.isNotLargerThan(obj, OBJ_BLOB, 0));
+ assertTrue(ctx.isNotLargerThan(obj, OBJ_BLOB, 10));
+ assertTrue(ctx.isNotLargerThan(obj, OBJ_BLOB, 40));
+ assertTrue(ctx.isNotLargerThan(obj, OBJ_BLOB, 50));
+ assertTrue(ctx.isNotLargerThan(obj, OBJ_BLOB, 100));
+
+ assertEquals(5, ctx.stats.isNotLargerThanCallCount);
+ assertEquals(0, ctx.stats.objectSizeIndexMiss);
+ assertEquals(0, ctx.stats.objectSizeIndexHit);
+ }
+ }
+
+ @Test
+ public void packLoadListener_noInvocations() throws IOException {
+ insertBlobWithSize(100);
+ try (DfsReader ctx = db.getObjectDatabase().newReader()) {
+ CounterPackLoadListener listener = new CounterPackLoadListener();
+ ctx.addPackLoadListener(listener);
+ assertEquals(null, listener.callsPerExt.get(PackExt.INDEX));
+ }
+ }
+
+ @Test
+ public void packLoadListener_has_openIdx() throws IOException {
+ ObjectId obj = insertBlobWithSize(100);
+ try (DfsReader ctx = db.getObjectDatabase().newReader()) {
+ CounterPackLoadListener listener = new CounterPackLoadListener();
+ ctx.addPackLoadListener(listener);
+ boolean has = ctx.has(obj);
+ assertTrue(has);
+ assertEquals(Integer.valueOf(1),
+ listener.callsPerExt.get(PackExt.INDEX));
+ }
+ }
+
+ @Test
+ public void packLoadListener_notLargerThan_openMultipleIndices()
+ throws IOException {
+ setObjectSizeIndexMinBytes(100);
+ ObjectId obj = insertBlobWithSize(200);
+ try (DfsReader ctx = db.getObjectDatabase().newReader()) {
+ CounterPackLoadListener listener = new CounterPackLoadListener();
+ ctx.addPackLoadListener(listener);
+ boolean notLargerThan = ctx.isNotLargerThan(obj, OBJ_BLOB, 1000);
+ assertTrue(notLargerThan);
+ assertEquals(Integer.valueOf(1),
+ listener.callsPerExt.get(PackExt.INDEX));
+ assertEquals(Integer.valueOf(1),
+ listener.callsPerExt.get(PackExt.OBJECT_SIZE_INDEX));
+ }
+ }
+
+ @Test
+ public void packLoadListener_has_openMultipleIndices() throws IOException {
+ setObjectSizeIndexMinBytes(100);
+ insertBlobWithSize(200);
+ insertBlobWithSize(230);
+ insertBlobWithSize(100);
+ try (DfsReader ctx = db.getObjectDatabase().newReader()) {
+ CounterPackLoadListener listener = new CounterPackLoadListener();
+ ctx.addPackLoadListener(listener);
+ ObjectId oid = ObjectId
+ .fromString("aa48de2aa61d9dffa8a05439dc115fe82f10f129");
+ boolean has = ctx.has(oid);
+ assertFalse(has);
+ // Open 3 indices trying to find the pack
+ assertEquals(Integer.valueOf(3),
+ listener.callsPerExt.get(PackExt.INDEX));
+ }
+ }
+
+ @Test
+ public void packLoadListener_has_repeatedCalls_openMultipleIndices()
+ throws IOException {
+ // Two objects NOT in the repo
+ ObjectId oid = ObjectId
+ .fromString("aa48de2aa61d9dffa8a05439dc115fe82f10f129");
+ ObjectId oid2 = ObjectId
+ .fromString("aa48de2aa61d9dffa8a05439dc115fe82f10f130");
+
+ setObjectSizeIndexMinBytes(100);
+ insertBlobWithSize(200);
+ insertBlobWithSize(230);
+ insertBlobWithSize(100);
+ CounterPackLoadListener listener = new CounterPackLoadListener();
+ try (DfsReader ctx = db.getObjectDatabase().newReader()) {
+ ctx.addPackLoadListener(listener);
+ boolean has = ctx.has(oid);
+ ctx.has(oid);
+ ctx.has(oid2);
+ assertFalse(has);
+ // The 3 indices were loaded only once each
+ assertEquals(Integer.valueOf(3),
+ listener.callsPerExt.get(PackExt.INDEX));
+ }
+ }
+
+ private static class CounterPackLoadListener implements PackLoadListener {
+ final Map<PackExt, Integer> callsPerExt = new HashMap<>();
+
+ @SuppressWarnings("boxing")
+ @Override
+ public void onIndexLoad(String packName, PackSource src, PackExt ext,
+ long size, Object loadedIdx) {
+ callsPerExt.merge(ext, 1, Integer::sum);
+ }
+
+ @Override
+ public void onBlockLoad(String packName, PackSource src, PackExt ext,
+ long size, DfsBlockData dfsBlockData) {
+ // empty
+ }
+ }
+
+ private ObjectId insertBlobWithSize(int size) throws IOException {
+ return insertObjectWithSize(OBJ_BLOB, size);
+ }
+
+ private ObjectId insertObjectWithSize(int object_type, int size)
+ throws IOException {
+ TestRng testRng = new TestRng(JGitTestUtil.getName());
+ ObjectId oid;
+ try (ObjectInserter ins = db.newObjectInserter()) {
+ oid = ins.insert(object_type, testRng.nextBytes(size));
+ ins.flush();
+ }
+ return oid;
+ }
+
+ private void setObjectSizeIndexMinBytes(int threshold) {
+ db.getConfig().setInt(CONFIG_PACK_SECTION, null,
+ CONFIG_KEY_MIN_BYTES_OBJ_SIZE_INDEX, threshold);
+ }
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/PackExtBlockCacheTableTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/PackExtBlockCacheTableTest.java
new file mode 100644
index 0000000000..e7627bc4ab
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/PackExtBlockCacheTableTest.java
@@ -0,0 +1,679 @@
+/*
+ * Copyright (c) 2024, Google LLC and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.storage.dfs;
+
+import static org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheTable.BlockCacheStats;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.sameInstance;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.when;
+
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jgit.internal.storage.dfs.DfsBlockCache.Ref;
+import org.eclipse.jgit.internal.storage.dfs.DfsBlockCache.RefLoader;
+import org.eclipse.jgit.internal.storage.dfs.DfsBlockCacheConfig.DfsBlockCachePackExtConfig;
+import org.eclipse.jgit.internal.storage.pack.PackExt;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+@SuppressWarnings({ "boxing", "unchecked" })
+public class PackExtBlockCacheTableTest {
+ private static final String CACHE_NAME = "CacheName";
+
+ @Test
+ public void fromBlockCacheConfigs_createsDfsPackExtBlockCacheTables() {
+ DfsBlockCacheConfig cacheConfig = new DfsBlockCacheConfig();
+ cacheConfig.setPackExtCacheConfigurations(
+ List.of(new DfsBlockCachePackExtConfig(EnumSet.of(PackExt.PACK),
+ new DfsBlockCacheConfig())));
+ assertNotNull(
+ PackExtBlockCacheTable.fromBlockCacheConfigs(cacheConfig));
+ }
+
+ @Test
+ public void fromBlockCacheConfigs_noPackExtConfigurationGiven_packExtCacheConfigurationsIsEmpty_throws() {
+ DfsBlockCacheConfig cacheConfig = new DfsBlockCacheConfig();
+ cacheConfig.setPackExtCacheConfigurations(List.of());
+ assertThrows(IllegalArgumentException.class,
+ () -> PackExtBlockCacheTable
+ .fromBlockCacheConfigs(cacheConfig));
+ }
+
+ @Test
+ public void hasBlock0_packExtMapsToCacheTable_callsBitmapIndexCacheTable() {
+ DfsStreamKey streamKey = new TestKey(PackExt.BITMAP_INDEX);
+ DfsBlockCacheTable defaultBlockCacheTable = mock(
+ DfsBlockCacheTable.class);
+ DfsBlockCacheTable bitmapIndexCacheTable = mock(
+ DfsBlockCacheTable.class);
+ when(bitmapIndexCacheTable.hasBlock0(any(DfsStreamKey.class)))
+ .thenReturn(true);
+
+ PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables(
+ defaultBlockCacheTable,
+ Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable));
+
+ assertTrue(tables.hasBlock0(streamKey));
+ }
+
+ @Test
+ public void hasBlock0_packExtDoesNotMapToCacheTable_callsDefaultCache() {
+ DfsStreamKey streamKey = new TestKey(PackExt.PACK);
+ DfsBlockCacheTable defaultBlockCacheTable = mock(
+ DfsBlockCacheTable.class);
+ when(defaultBlockCacheTable.hasBlock0(any(DfsStreamKey.class)))
+ .thenReturn(true);
+ DfsBlockCacheTable bitmapIndexCacheTable = mock(
+ DfsBlockCacheTable.class);
+
+ PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables(
+ defaultBlockCacheTable,
+ Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable));
+
+ assertTrue(tables.hasBlock0(streamKey));
+ }
+
+ @Test
+ public void getOrLoad_packExtMapsToCacheTable_callsBitmapIndexCacheTable()
+ throws Exception {
+ BlockBasedFile blockBasedFile = new BlockBasedFile(null,
+ mock(DfsPackDescription.class), PackExt.BITMAP_INDEX) {
+ // empty
+ };
+ DfsBlock dfsBlock = mock(DfsBlock.class);
+ DfsBlockCacheTable defaultBlockCacheTable = mock(
+ DfsBlockCacheTable.class);
+ when(defaultBlockCacheTable.getOrLoad(any(BlockBasedFile.class),
+ anyLong(), any(DfsReader.class),
+ any(DfsBlockCache.ReadableChannelSupplier.class)))
+ .thenReturn(mock(DfsBlock.class));
+ DfsBlockCacheTable bitmapIndexCacheTable = mock(
+ DfsBlockCacheTable.class);
+ when(bitmapIndexCacheTable.getOrLoad(any(BlockBasedFile.class),
+ anyLong(), any(DfsReader.class),
+ any(DfsBlockCache.ReadableChannelSupplier.class)))
+ .thenReturn(dfsBlock);
+
+ PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables(
+ defaultBlockCacheTable,
+ Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable));
+
+ assertThat(
+ tables.getOrLoad(blockBasedFile, 0, mock(DfsReader.class),
+ mock(DfsBlockCache.ReadableChannelSupplier.class)),
+ sameInstance(dfsBlock));
+ }
+
+ @Test
+ public void getOrLoad_packExtDoesNotMapToCacheTable_callsDefaultCache()
+ throws Exception {
+ BlockBasedFile blockBasedFile = new BlockBasedFile(null,
+ mock(DfsPackDescription.class), PackExt.PACK) {
+ // empty
+ };
+ DfsBlock dfsBlock = mock(DfsBlock.class);
+ DfsBlockCacheTable defaultBlockCacheTable = mock(
+ DfsBlockCacheTable.class);
+ when(defaultBlockCacheTable.getOrLoad(any(BlockBasedFile.class),
+ anyLong(), any(DfsReader.class),
+ any(DfsBlockCache.ReadableChannelSupplier.class)))
+ .thenReturn(dfsBlock);
+ DfsBlockCacheTable bitmapIndexCacheTable = mock(
+ DfsBlockCacheTable.class);
+ when(bitmapIndexCacheTable.getOrLoad(any(BlockBasedFile.class),
+ anyLong(), any(DfsReader.class),
+ any(DfsBlockCache.ReadableChannelSupplier.class)))
+ .thenReturn(mock(DfsBlock.class));
+
+ PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables(
+ defaultBlockCacheTable,
+ Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable));
+
+ assertThat(
+ tables.getOrLoad(blockBasedFile, 0, mock(DfsReader.class),
+ mock(DfsBlockCache.ReadableChannelSupplier.class)),
+ sameInstance(dfsBlock));
+ }
+
+ @Test
+ public void getOrLoadRef_packExtMapsToCacheTable_callsBitmapIndexCacheTable()
+ throws Exception {
+ Ref<Integer> ref = mock(Ref.class);
+ DfsStreamKey dfsStreamKey = new TestKey(PackExt.BITMAP_INDEX);
+ DfsBlockCacheTable defaultBlockCacheTable = mock(
+ DfsBlockCacheTable.class);
+ when(defaultBlockCacheTable.getOrLoadRef(any(DfsStreamKey.class),
+ anyLong(), any(RefLoader.class))).thenReturn(mock(Ref.class));
+ DfsBlockCacheTable bitmapIndexCacheTable = mock(
+ DfsBlockCacheTable.class);
+ when(bitmapIndexCacheTable.getOrLoadRef(any(DfsStreamKey.class),
+ anyLong(), any(RefLoader.class))).thenReturn(ref);
+
+ PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables(
+ defaultBlockCacheTable,
+ Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable));
+
+ assertThat(tables.getOrLoadRef(dfsStreamKey, 0, mock(RefLoader.class)),
+ sameInstance(ref));
+ }
+
+ @Test
+ public void getOrLoadRef_packExtDoesNotMapToCacheTable_callsDefaultCache()
+ throws Exception {
+ Ref<Integer> ref = mock(Ref.class);
+ DfsStreamKey dfsStreamKey = new TestKey(PackExt.PACK);
+ DfsBlockCacheTable defaultBlockCacheTable = mock(
+ DfsBlockCacheTable.class);
+ when(defaultBlockCacheTable.getOrLoadRef(any(DfsStreamKey.class),
+ anyLong(), any(RefLoader.class))).thenReturn(ref);
+ DfsBlockCacheTable bitmapIndexCacheTable = mock(
+ DfsBlockCacheTable.class);
+ when(bitmapIndexCacheTable.getOrLoadRef(any(DfsStreamKey.class),
+ anyLong(), any(RefLoader.class))).thenReturn(mock(Ref.class));
+
+ PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables(
+ defaultBlockCacheTable,
+ Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable));
+
+ assertThat(tables.getOrLoadRef(dfsStreamKey, 0, mock(RefLoader.class)),
+ sameInstance(ref));
+ }
+
+ @Test
+ public void putDfsBlock_packExtMapsToCacheTable_callsBitmapIndexCacheTable() {
+ DfsStreamKey dfsStreamKey = new TestKey(PackExt.BITMAP_INDEX);
+ DfsBlock dfsBlock = new DfsBlock(dfsStreamKey, 0, new byte[0]);
+ DfsBlockCacheTable defaultBlockCacheTable = mock(
+ DfsBlockCacheTable.class);
+ DfsBlockCacheTable bitmapIndexCacheTable = mock(
+ DfsBlockCacheTable.class);
+
+ PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables(
+ defaultBlockCacheTable,
+ Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable));
+
+ tables.put(dfsBlock);
+ Mockito.verify(bitmapIndexCacheTable, times(1)).put(dfsBlock);
+ }
+
+ @Test
+ public void putDfsBlock_packExtDoesNotMapToCacheTable_callsDefaultCache() {
+ DfsStreamKey dfsStreamKey = new TestKey(PackExt.PACK);
+ DfsBlock dfsBlock = new DfsBlock(dfsStreamKey, 0, new byte[0]);
+ DfsBlockCacheTable defaultBlockCacheTable = mock(
+ DfsBlockCacheTable.class);
+ DfsBlockCacheTable bitmapIndexCacheTable = mock(
+ DfsBlockCacheTable.class);
+
+ PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables(
+ defaultBlockCacheTable,
+ Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable));
+
+ tables.put(dfsBlock);
+ Mockito.verify(defaultBlockCacheTable, times(1)).put(dfsBlock);
+ }
+
+ @Test
+ public void putDfsStreamKey_packExtMapsToCacheTable_callsBitmapIndexCacheTable() {
+ DfsStreamKey dfsStreamKey = new TestKey(PackExt.BITMAP_INDEX);
+ Ref<Integer> ref = mock(Ref.class);
+ DfsBlockCacheTable defaultBlockCacheTable = mock(
+ DfsBlockCacheTable.class);
+ when(defaultBlockCacheTable.put(any(DfsStreamKey.class), anyLong(),
+ anyLong(), anyInt())).thenReturn(mock(Ref.class));
+ DfsBlockCacheTable bitmapIndexCacheTable = mock(
+ DfsBlockCacheTable.class);
+ when(bitmapIndexCacheTable.put(any(DfsStreamKey.class), anyLong(),
+ anyLong(), anyInt())).thenReturn(ref);
+
+ PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables(
+ defaultBlockCacheTable,
+ Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable));
+
+ assertThat(tables.put(dfsStreamKey, 0, 0, 0), sameInstance(ref));
+ }
+
+ @Test
+ public void putDfsStreamKey_packExtDoesNotMapToCacheTable_callsDefaultCache() {
+ DfsStreamKey dfsStreamKey = new TestKey(PackExt.PACK);
+ Ref<Integer> ref = mock(Ref.class);
+ DfsBlockCacheTable defaultBlockCacheTable = mock(
+ DfsBlockCacheTable.class);
+ when(defaultBlockCacheTable.put(any(DfsStreamKey.class), anyLong(),
+ anyLong(), anyInt())).thenReturn(ref);
+ DfsBlockCacheTable bitmapIndexCacheTable = mock(
+ DfsBlockCacheTable.class);
+ when(bitmapIndexCacheTable.put(any(DfsStreamKey.class), anyLong(),
+ anyLong(), anyInt())).thenReturn(mock(Ref.class));
+
+ PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables(
+ defaultBlockCacheTable,
+ Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable));
+
+ assertThat(tables.put(dfsStreamKey, 0, 0, 0), sameInstance(ref));
+ }
+
+ @Test
+ public void putRef_packExtMapsToCacheTable_callsBitmapIndexCacheTable() {
+ DfsStreamKey dfsStreamKey = new TestKey(PackExt.BITMAP_INDEX);
+ Ref<Integer> ref = mock(Ref.class);
+ DfsBlockCacheTable defaultBlockCacheTable = mock(
+ DfsBlockCacheTable.class);
+ when(defaultBlockCacheTable.putRef(any(DfsStreamKey.class), anyLong(),
+ anyInt())).thenReturn(mock(Ref.class));
+ DfsBlockCacheTable bitmapIndexCacheTable = mock(
+ DfsBlockCacheTable.class);
+ when(bitmapIndexCacheTable.putRef(any(DfsStreamKey.class), anyLong(),
+ anyInt())).thenReturn(ref);
+
+ PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables(
+ defaultBlockCacheTable,
+ Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable));
+
+ assertThat(tables.putRef(dfsStreamKey, 0, 0), sameInstance(ref));
+ }
+
+ @Test
+ public void putRef_packExtDoesNotMapToCacheTable_callsDefaultCache() {
+ DfsStreamKey dfsStreamKey = new TestKey(PackExt.PACK);
+ Ref<Integer> ref = mock(Ref.class);
+ DfsBlockCacheTable defaultBlockCacheTable = mock(
+ DfsBlockCacheTable.class);
+ when(defaultBlockCacheTable.putRef(any(DfsStreamKey.class), anyLong(),
+ anyInt())).thenReturn(ref);
+ DfsBlockCacheTable bitmapIndexCacheTable = mock(
+ DfsBlockCacheTable.class);
+ when(bitmapIndexCacheTable.putRef(any(DfsStreamKey.class), anyLong(),
+ anyInt())).thenReturn(mock(Ref.class));
+
+ PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables(
+ defaultBlockCacheTable,
+ Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable));
+
+ assertThat(tables.putRef(dfsStreamKey, 0, 0), sameInstance(ref));
+ }
+
+ @Test
+ public void contains_packExtMapsToCacheTable_callsBitmapIndexCacheTable() {
+ DfsStreamKey streamKey = new TestKey(PackExt.BITMAP_INDEX);
+ DfsBlockCacheTable defaultBlockCacheTable = mock(
+ DfsBlockCacheTable.class);
+ DfsBlockCacheTable bitmapIndexCacheTable = mock(
+ DfsBlockCacheTable.class);
+ when(bitmapIndexCacheTable.contains(any(DfsStreamKey.class), anyLong()))
+ .thenReturn(true);
+
+ PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables(
+ defaultBlockCacheTable,
+ Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable));
+
+ assertTrue(tables.contains(streamKey, 0));
+ }
+
+ @Test
+ public void contains_packExtDoesNotMapToCacheTable_callsDefaultCache() {
+ DfsStreamKey streamKey = new TestKey(PackExt.PACK);
+ DfsBlockCacheTable defaultBlockCacheTable = mock(
+ DfsBlockCacheTable.class);
+ when(defaultBlockCacheTable.contains(any(DfsStreamKey.class),
+ anyLong())).thenReturn(true);
+ DfsBlockCacheTable bitmapIndexCacheTable = mock(
+ DfsBlockCacheTable.class);
+
+ PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables(
+ defaultBlockCacheTable,
+ Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable));
+
+ assertTrue(tables.contains(streamKey, 0));
+ }
+
+ @Test
+ public void get_packExtMapsToCacheTable_callsBitmapIndexCacheTable() {
+ DfsStreamKey dfsStreamKey = new TestKey(PackExt.BITMAP_INDEX);
+ Ref<Integer> ref = mock(Ref.class);
+ DfsBlockCacheTable defaultBlockCacheTable = mock(
+ DfsBlockCacheTable.class);
+ when(defaultBlockCacheTable.get(any(DfsStreamKey.class), anyLong()))
+ .thenReturn(mock(Ref.class));
+ DfsBlockCacheTable bitmapIndexCacheTable = mock(
+ DfsBlockCacheTable.class);
+ when(bitmapIndexCacheTable.get(any(DfsStreamKey.class), anyLong()))
+ .thenReturn(ref);
+
+ PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables(
+ defaultBlockCacheTable,
+ Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable));
+
+ assertThat(tables.get(dfsStreamKey, 0), sameInstance(ref));
+ }
+
+ @Test
+ public void get_packExtDoesNotMapToCacheTable_callsDefaultCache() {
+ DfsStreamKey dfsStreamKey = new TestKey(PackExt.PACK);
+ Ref<Integer> ref = mock(Ref.class);
+ DfsBlockCacheTable defaultBlockCacheTable = mock(
+ DfsBlockCacheTable.class);
+ when(defaultBlockCacheTable.get(any(DfsStreamKey.class), anyLong()))
+ .thenReturn(ref);
+ DfsBlockCacheTable bitmapIndexCacheTable = mock(
+ DfsBlockCacheTable.class);
+ when(bitmapIndexCacheTable.get(any(DfsStreamKey.class), anyLong()))
+ .thenReturn(mock(Ref.class));
+
+ PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables(
+ defaultBlockCacheTable,
+ Map.of(PackExt.BITMAP_INDEX, bitmapIndexCacheTable));
+
+ assertThat(tables.get(dfsStreamKey, 0), sameInstance(ref));
+ }
+
+ @Test
+ public void getName() {
+ DfsBlockCacheStats packStats = new DfsBlockCacheStats();
+ PackExtBlockCacheTable tables = PackExtBlockCacheTable.fromCacheTables(
+ cacheTableWithStats(/* name= */ "defaultName", packStats),
+ Map.of(PackExt.PACK, cacheTableWithStats(/* name= */ "packName",
+ packStats)));
+
+ assertThat(tables.getName(), equalTo("defaultName,packName"));
+ }
+
+ @Test
+ public void getAllBlockCacheStats() {
+ String defaultTableName = "default table";
+ DfsBlockCacheStats defaultStats = new DfsBlockCacheStats(
+ defaultTableName);
+ incrementCounter(4,
+ () -> defaultStats.incrementHit(new TestKey(PackExt.REFTABLE)));
+
+ String packTableName = "pack table";
+ DfsBlockCacheStats packStats = new DfsBlockCacheStats(packTableName);
+ incrementCounter(5,
+ () -> packStats.incrementHit(new TestKey(PackExt.PACK)));
+
+ String bitmapTableName = "bitmap table";
+ DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats(
+ bitmapTableName);
+ incrementCounter(6, () -> bitmapStats
+ .incrementHit(new TestKey(PackExt.BITMAP_INDEX)));
+
+ DfsBlockCacheTable defaultTable = cacheTableWithStats(defaultStats);
+ DfsBlockCacheTable packTable = cacheTableWithStats(packStats);
+ DfsBlockCacheTable bitmapTable = cacheTableWithStats(bitmapStats);
+ PackExtBlockCacheTable tables = PackExtBlockCacheTable
+ .fromCacheTables(defaultTable, Map.of(PackExt.PACK, packTable,
+ PackExt.BITMAP_INDEX, bitmapTable));
+
+ List<BlockCacheStats> statsList = tables.getBlockCacheStats();
+ assertThat(statsList, hasSize(3));
+
+ long[] defaultTableHitCounts = createEmptyStatsArray();
+ defaultTableHitCounts[PackExt.REFTABLE.getPosition()] = 4;
+ assertArrayEquals(
+ getCacheStatsByName(statsList, defaultTableName).getHitCount(),
+ defaultTableHitCounts);
+
+ long[] packTableHitCounts = createEmptyStatsArray();
+ packTableHitCounts[PackExt.PACK.getPosition()] = 5;
+ assertArrayEquals(
+ getCacheStatsByName(statsList, packTableName).getHitCount(),
+ packTableHitCounts);
+
+ long[] bitmapHitCounts = createEmptyStatsArray();
+ bitmapHitCounts[PackExt.BITMAP_INDEX.getPosition()] = 6;
+ assertArrayEquals(
+ getCacheStatsByName(statsList, bitmapTableName).getHitCount(),
+ bitmapHitCounts);
+ }
+
+ @Test
+ public void getBlockCacheStats_getCurrentSize_consolidatesAllTableCurrentSizes() {
+ long[] currentSizes = createEmptyStatsArray();
+
+ DfsBlockCacheStats packStats = new DfsBlockCacheStats();
+ packStats.addToLiveBytes(new TestKey(PackExt.PACK), 5);
+ currentSizes[PackExt.PACK.getPosition()] = 5;
+
+ DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats();
+ bitmapStats.addToLiveBytes(new TestKey(PackExt.BITMAP_INDEX), 6);
+ currentSizes[PackExt.BITMAP_INDEX.getPosition()] = 6;
+
+ DfsBlockCacheStats indexStats = new DfsBlockCacheStats();
+ indexStats.addToLiveBytes(new TestKey(PackExt.INDEX), 7);
+ currentSizes[PackExt.INDEX.getPosition()] = 7;
+
+ PackExtBlockCacheTable tables = PackExtBlockCacheTable
+ .fromCacheTables(cacheTableWithStats(packStats),
+ Map.of(PackExt.BITMAP_INDEX,
+ cacheTableWithStats(bitmapStats), PackExt.INDEX,
+ cacheTableWithStats(indexStats)));
+
+ assertArrayEquals(AggregatedBlockCacheStats
+ .fromStatsList(tables.getBlockCacheStats()).getCurrentSize(),
+ currentSizes);
+ }
+
+ @Test
+ public void getBlockCacheStats_GetHitCount_consolidatesAllTableHitCounts() {
+ long[] hitCounts = createEmptyStatsArray();
+
+ DfsBlockCacheStats packStats = new DfsBlockCacheStats();
+ incrementCounter(5,
+ () -> packStats.incrementHit(new TestKey(PackExt.PACK)));
+ hitCounts[PackExt.PACK.getPosition()] = 5;
+
+ DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats();
+ incrementCounter(6, () -> bitmapStats
+ .incrementHit(new TestKey(PackExt.BITMAP_INDEX)));
+ hitCounts[PackExt.BITMAP_INDEX.getPosition()] = 6;
+
+ DfsBlockCacheStats indexStats = new DfsBlockCacheStats();
+ incrementCounter(7,
+ () -> indexStats.incrementHit(new TestKey(PackExt.INDEX)));
+ hitCounts[PackExt.INDEX.getPosition()] = 7;
+
+ PackExtBlockCacheTable tables = PackExtBlockCacheTable
+ .fromCacheTables(cacheTableWithStats(packStats),
+ Map.of(PackExt.BITMAP_INDEX,
+ cacheTableWithStats(bitmapStats), PackExt.INDEX,
+ cacheTableWithStats(indexStats)));
+
+ assertArrayEquals(AggregatedBlockCacheStats
+ .fromStatsList(tables.getBlockCacheStats()).getHitCount(),
+ hitCounts);
+ }
+
+ @Test
+ public void getBlockCacheStats_getMissCount_consolidatesAllTableMissCounts() {
+ long[] missCounts = createEmptyStatsArray();
+
+ DfsBlockCacheStats packStats = new DfsBlockCacheStats();
+ incrementCounter(5,
+ () -> packStats.incrementMiss(new TestKey(PackExt.PACK)));
+ missCounts[PackExt.PACK.getPosition()] = 5;
+
+ DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats();
+ incrementCounter(6, () -> bitmapStats
+ .incrementMiss(new TestKey(PackExt.BITMAP_INDEX)));
+ missCounts[PackExt.BITMAP_INDEX.getPosition()] = 6;
+
+ DfsBlockCacheStats indexStats = new DfsBlockCacheStats();
+ incrementCounter(7,
+ () -> indexStats.incrementMiss(new TestKey(PackExt.INDEX)));
+ missCounts[PackExt.INDEX.getPosition()] = 7;
+
+ PackExtBlockCacheTable tables = PackExtBlockCacheTable
+ .fromCacheTables(cacheTableWithStats(packStats),
+ Map.of(PackExt.BITMAP_INDEX,
+ cacheTableWithStats(bitmapStats), PackExt.INDEX,
+ cacheTableWithStats(indexStats)));
+
+ assertArrayEquals(AggregatedBlockCacheStats
+ .fromStatsList(tables.getBlockCacheStats()).getMissCount(),
+ missCounts);
+ }
+
+ @Test
+ public void getBlockCacheStats_getTotalRequestCount_consolidatesAllTableTotalRequestCounts() {
+ long[] totalRequestCounts = createEmptyStatsArray();
+
+ DfsBlockCacheStats packStats = new DfsBlockCacheStats();
+ incrementCounter(5, () -> {
+ packStats.incrementHit(new TestKey(PackExt.PACK));
+ packStats.incrementMiss(new TestKey(PackExt.PACK));
+ });
+ totalRequestCounts[PackExt.PACK.getPosition()] = 10;
+
+ DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats();
+ incrementCounter(6, () -> {
+ bitmapStats.incrementHit(new TestKey(PackExt.BITMAP_INDEX));
+ bitmapStats.incrementMiss(new TestKey(PackExt.BITMAP_INDEX));
+ });
+ totalRequestCounts[PackExt.BITMAP_INDEX.getPosition()] = 12;
+
+ DfsBlockCacheStats indexStats = new DfsBlockCacheStats();
+ incrementCounter(7, () -> {
+ indexStats.incrementHit(new TestKey(PackExt.INDEX));
+ indexStats.incrementMiss(new TestKey(PackExt.INDEX));
+ });
+ totalRequestCounts[PackExt.INDEX.getPosition()] = 14;
+
+ PackExtBlockCacheTable tables = PackExtBlockCacheTable
+ .fromCacheTables(cacheTableWithStats(packStats),
+ Map.of(PackExt.BITMAP_INDEX,
+ cacheTableWithStats(bitmapStats), PackExt.INDEX,
+ cacheTableWithStats(indexStats)));
+
+ assertArrayEquals(AggregatedBlockCacheStats
+ .fromStatsList(tables.getBlockCacheStats())
+ .getTotalRequestCount(), totalRequestCounts);
+ }
+
+ @Test
+ public void getBlockCacheStats_getHitRatio_consolidatesAllTableHitRatios() {
+ long[] hitRatios = createEmptyStatsArray();
+
+ DfsBlockCacheStats packStats = new DfsBlockCacheStats();
+ incrementCounter(5,
+ () -> packStats.incrementHit(new TestKey(PackExt.PACK)));
+ hitRatios[PackExt.PACK.getPosition()] = 100;
+
+ DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats();
+ incrementCounter(6, () -> {
+ bitmapStats.incrementHit(new TestKey(PackExt.BITMAP_INDEX));
+ bitmapStats.incrementMiss(new TestKey(PackExt.BITMAP_INDEX));
+ });
+ hitRatios[PackExt.BITMAP_INDEX.getPosition()] = 50;
+
+ DfsBlockCacheStats indexStats = new DfsBlockCacheStats();
+ incrementCounter(7,
+ () -> indexStats.incrementMiss(new TestKey(PackExt.INDEX)));
+ hitRatios[PackExt.INDEX.getPosition()] = 0;
+
+ PackExtBlockCacheTable tables = PackExtBlockCacheTable
+ .fromCacheTables(cacheTableWithStats(packStats),
+ Map.of(PackExt.BITMAP_INDEX,
+ cacheTableWithStats(bitmapStats), PackExt.INDEX,
+ cacheTableWithStats(indexStats)));
+
+ assertArrayEquals(AggregatedBlockCacheStats
+ .fromStatsList(tables.getBlockCacheStats()).getHitRatio(),
+ hitRatios);
+ }
+
+ @Test
+ public void getBlockCacheStats_getEvictions_consolidatesAllTableEvictions() {
+ long[] evictions = createEmptyStatsArray();
+
+ DfsBlockCacheStats packStats = new DfsBlockCacheStats();
+ incrementCounter(5,
+ () -> packStats.incrementEvict(new TestKey(PackExt.PACK)));
+ evictions[PackExt.PACK.getPosition()] = 5;
+
+ DfsBlockCacheStats bitmapStats = new DfsBlockCacheStats();
+ incrementCounter(6, () -> bitmapStats
+ .incrementEvict(new TestKey(PackExt.BITMAP_INDEX)));
+ evictions[PackExt.BITMAP_INDEX.getPosition()] = 6;
+
+ DfsBlockCacheStats indexStats = new DfsBlockCacheStats();
+ incrementCounter(7,
+ () -> indexStats.incrementEvict(new TestKey(PackExt.INDEX)));
+ evictions[PackExt.INDEX.getPosition()] = 7;
+
+ PackExtBlockCacheTable tables = PackExtBlockCacheTable
+ .fromCacheTables(cacheTableWithStats(packStats),
+ Map.of(PackExt.BITMAP_INDEX,
+ cacheTableWithStats(bitmapStats), PackExt.INDEX,
+ cacheTableWithStats(indexStats)));
+
+ assertArrayEquals(AggregatedBlockCacheStats
+ .fromStatsList(tables.getBlockCacheStats()).getEvictions(),
+ evictions);
+ }
+
+ private BlockCacheStats getCacheStatsByName(
+ List<BlockCacheStats> blockCacheStats, String name) {
+ for (BlockCacheStats entry : blockCacheStats) {
+ if (entry.getName().equals(name)) {
+ return entry;
+ }
+ }
+ return null;
+ }
+
+ private static void incrementCounter(int amount, Runnable fn) {
+ for (int i = 0; i < amount; i++) {
+ fn.run();
+ }
+ }
+
+ private static long[] createEmptyStatsArray() {
+ return new long[PackExt.values().length];
+ }
+
+ private static DfsBlockCacheTable cacheTableWithStats(
+ BlockCacheStats dfsBlockCacheStats) {
+ return cacheTableWithStats(CACHE_NAME, dfsBlockCacheStats);
+ }
+
+ private static DfsBlockCacheTable cacheTableWithStats(String name,
+ BlockCacheStats dfsBlockCacheStats) {
+ DfsBlockCacheTable cacheTable = mock(DfsBlockCacheTable.class);
+ when(cacheTable.getName()).thenReturn(name);
+ when(cacheTable.getBlockCacheStats())
+ .thenReturn(List.of(dfsBlockCacheStats));
+ return cacheTable;
+ }
+
+ private static class TestKey extends DfsStreamKey {
+ TestKey(PackExt packExt) {
+ super(0, packExt);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return false;
+ }
+ }
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/AbbreviationTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/AbbreviationTest.java
index bd36337f35..41a33df0e4 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/AbbreviationTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/AbbreviationTest.java
@@ -29,6 +29,7 @@ import java.util.List;
import org.eclipse.jgit.errors.AmbiguousObjectException;
import org.eclipse.jgit.internal.storage.pack.PackExt;
+import org.eclipse.jgit.internal.storage.pack.PackIndexWriter;
import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.AbbreviatedObjectId;
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BasePackBitmapIndexTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BasePackBitmapIndexTest.java
new file mode 100644
index 0000000000..f47c385ab9
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BasePackBitmapIndexTest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (c) 2023, Google LLC and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.internal.storage.file;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.eclipse.jgit.internal.storage.file.BasePackBitmapIndex.StoredBitmap;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdOwnerMap;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.googlecode.javaewah.EWAHCompressedBitmap;
+
+public class BasePackBitmapIndexTest {
+
+ private ObjectId baseOid;
+
+ private StoredBitmap baseBitmap;
+
+ private ObjectId xorOid;
+
+ private StoredBitmap xorBitmap;
+
+ private ObjectIdOwnerMap<StoredBitmap> bitmaps;
+
+ @Before
+ public void setUp() {
+ baseOid = ObjectId
+ .fromString("c46f36f2bfc96d6d6f75bd71ee33625293aee690");
+ baseBitmap = newBaseStoredBitmap(baseOid, bitmapOf(100));
+ xorOid = ObjectId
+ .fromString("52c18ae15f8fa3787f920e68791367dae2e1af2d");
+ xorBitmap = newXorStoredBitmap(xorOid, bitmapOf(200, 300), baseBitmap);
+ bitmaps = new ObjectIdOwnerMap<>();
+ bitmaps.add(baseBitmap);
+ bitmaps.add(xorBitmap);
+ }
+
+ @Test
+ public void testBitmapCounts() {
+ TestPackBitmapIndex index = new TestPackBitmapIndex(bitmaps);
+
+ assertEquals(1, index.getBaseBitmapCount());
+ assertEquals(1, index.getXorBitmapCount());
+ assertEquals(2, index.getBitmapCount());
+ }
+
+ @Test
+ public void testBitmapCounts_xorResolved() {
+ TestPackBitmapIndex index = new TestPackBitmapIndex(bitmaps);
+ index.getBitmap(xorOid);
+
+ assertEquals(2, index.getBaseBitmapCount());
+ assertEquals(0, index.getXorBitmapCount());
+ assertEquals(2, index.getBitmapCount());
+ }
+
+ @Test
+ public void testBitmapSizes() {
+ TestPackBitmapIndex index = new TestPackBitmapIndex(bitmaps);
+
+ assertEquals(baseBitmap.getCurrentSizeInBytes(),
+ index.getBaseBitmapSizeInBytes());
+ assertEquals(xorBitmap.getCurrentSizeInBytes(),
+ index.getXorBitmapSizeInBytes());
+ }
+
+ @Test
+ public void testBitmapSizes_xorResolved() {
+ TestPackBitmapIndex index = new TestPackBitmapIndex(bitmaps);
+ index.getBitmap(xorOid);
+
+ assertTrue(baseBitmap.getCurrentSizeInBytes() < index
+ .getBaseBitmapSizeInBytes());
+ assertEquals(0, index.getXorBitmapSizeInBytes());
+ }
+
+ private static final StoredBitmap newBaseStoredBitmap(ObjectId oid,
+ EWAHCompressedBitmap base) {
+ return new StoredBitmap(oid, base, null, 0);
+ }
+
+ private static StoredBitmap newXorStoredBitmap(ObjectId oid,
+ EWAHCompressedBitmap xorMask, StoredBitmap base) {
+ return new StoredBitmap(oid, xorMask, base, 0);
+ }
+
+ private static final EWAHCompressedBitmap bitmapOf(int... bits) {
+ EWAHCompressedBitmap b = new EWAHCompressedBitmap();
+ for (int bit : bits)
+ b.set(bit);
+ return b;
+ }
+
+ private static class TestPackBitmapIndex extends BasePackBitmapIndex {
+ TestPackBitmapIndex(ObjectIdOwnerMap<StoredBitmap> bitmaps) {
+ super(bitmaps);
+ }
+
+ @Override
+ public int findPosition(AnyObjectId objectId) {
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public ObjectId getObject(int position)
+ throws IllegalArgumentException {
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public EWAHCompressedBitmap ofObjectType(EWAHCompressedBitmap bitmap,
+ int type) {
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public int getObjectCount() {
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public int getBitmapCount() {
+ return getBitmaps().size();
+ }
+ }
+}
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/BasePackWriterTest.java
index 2a403c7699..cd73c6ae83 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/BasePackWriterTest.java
@@ -19,7 +19,7 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -66,7 +66,7 @@ import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
-public class PackWriterTest extends SampleDataRepositoryTestCase {
+public class BasePackWriterTest extends SampleDataRepositoryTestCase {
private static final List<RevObject> EMPTY_LIST_REVS = Collections
.<RevObject> emptyList();
@@ -542,6 +542,39 @@ public class PackWriterTest extends SampleDataRepositoryTestCase {
}
@Test
+ public void testWriteReverseIndexConfig() {
+ assertFalse(config.isWriteReverseIndex());
+ config.setWriteReverseIndex(true);
+ assertTrue(config.isWriteReverseIndex());
+ }
+
+ @Test
+ public void testWriteReverseIndexOff() throws Exception {
+ config.setWriteReverseIndex(false);
+ writer = new PackWriter(config, db.newObjectReader());
+ ByteArrayOutputStream reverseIndexOutput = new ByteArrayOutputStream();
+
+ writer.writeReverseIndex(reverseIndexOutput);
+
+ assertEquals(0, reverseIndexOutput.size());
+ }
+
+ @Test
+ public void testWriteReverseIndexOn() throws Exception {
+ config.setWriteReverseIndex(true);
+ writeVerifyPack4(false);
+ ByteArrayOutputStream reverseIndexOutput = new ByteArrayOutputStream();
+ int headerBytes = 12;
+ int bodyBytes = 12;
+ int footerBytes = 40;
+
+ writer.writeReverseIndex(reverseIndexOutput);
+
+ assertTrue(reverseIndexOutput.size() == headerBytes + bodyBytes
+ + footerBytes);
+ }
+
+ @Test
public void testExclude() throws Exception {
// TestRepository closes repo
FileRepository repo = createBareRepository();
@@ -669,13 +702,29 @@ public class PackWriterTest extends SampleDataRepositoryTestCase {
}
@Test
- public void testTotalPackFilesScanWhenSearchForReuseTimeoutNotSet()
+ public void testTotalPackFilesScanWhenSearchForReuseTimeoutNotSetTrue()
+ throws Exception {
+ totalPackFilesScanWhenSearchForReuseTimeoutNotSet(true);
+ }
+
+ @Test
+ public void testTotalPackFilesScanWhenSearchForReuseTimeoutNotSetFalse()
throws Exception {
+ totalPackFilesScanWhenSearchForReuseTimeoutNotSet(false);
+ }
+
+ public void totalPackFilesScanWhenSearchForReuseTimeoutNotSet(boolean doReturn) throws Exception {
FileRepository fileRepository = setUpRepoWithMultiplePackfiles();
+ int numberOfPackFiles = (int) new GC(fileRepository).getStatistics().numberOfPackFiles;
+ int objectsInMultiplePacks = 2;
+ int objectsInOnePacks = 1;
+ int expectedSelectCalls = objectsInMultiplePacks * (doReturn ? numberOfPackFiles : 1)
+ + objectsInOnePacks;
+
PackWriter mockedPackWriter = Mockito
.spy(new PackWriter(config, fileRepository.newObjectReader()));
- doNothing().when(mockedPackWriter).select(any(), any());
+ doReturn(doReturn).when(mockedPackWriter).select(any(), any());
try (FileOutputStream packOS = new FileOutputStream(
getPackFileToWrite(fileRepository, mockedPackWriter))) {
@@ -683,27 +732,37 @@ public class PackWriterTest extends SampleDataRepositoryTestCase {
NullProgressMonitor.INSTANCE, packOS);
}
- long numberOfPackFiles = new GC(fileRepository)
- .getStatistics().numberOfPackFiles;
- int expectedSelectCalls =
- // Objects contained in multiple packfiles * number of packfiles
- 2 * (int) numberOfPackFiles +
- // Objects in single packfile
- 1;
verify(mockedPackWriter, times(expectedSelectCalls)).select(any(),
any());
}
@Test
- public void testTotalPackFilesScanWhenSkippingSearchForReuseTimeoutCheck()
+ public void testTotalPackFilesScanWhenSkippingSearchForReuseTimeoutCheckTrue()
+ throws Exception {
+ totalPackFilesScanWhenSkippingSearchForReuseTimeoutCheck(true);
+ }
+
+ @Test
+ public void testTotalPackFilesScanWhenSkippingSearchForReuseTimeoutCheckFalse()
throws Exception {
+ totalPackFilesScanWhenSkippingSearchForReuseTimeoutCheck(false);
+ }
+
+ public void totalPackFilesScanWhenSkippingSearchForReuseTimeoutCheck(
+ boolean doReturn) throws Exception {
FileRepository fileRepository = setUpRepoWithMultiplePackfiles();
+ int numberOfPackFiles = (int) new GC(fileRepository).getStatistics().numberOfPackFiles;
+ int objectsInMultiplePacks = 2;
+ int objectsInOnePacks = 1;
+ int expectedSelectCalls = objectsInMultiplePacks * (doReturn ? numberOfPackFiles : 1)
+ + objectsInOnePacks;
+
PackConfig packConfig = new PackConfig();
packConfig.setSearchForReuseTimeout(Duration.ofSeconds(-1));
PackWriter mockedPackWriter = Mockito.spy(
new PackWriter(packConfig, fileRepository.newObjectReader()));
- doNothing().when(mockedPackWriter).select(any(), any());
+ doReturn(doReturn).when(mockedPackWriter).select(any(), any());
try (FileOutputStream packOS = new FileOutputStream(
getPackFileToWrite(fileRepository, mockedPackWriter))) {
@@ -711,28 +770,31 @@ public class PackWriterTest extends SampleDataRepositoryTestCase {
NullProgressMonitor.INSTANCE, packOS);
}
- long numberOfPackFiles = new GC(fileRepository)
- .getStatistics().numberOfPackFiles;
- int expectedSelectCalls =
- // Objects contained in multiple packfiles * number of packfiles
- 2 * (int) numberOfPackFiles +
- // Objects contained in single packfile
- 1;
verify(mockedPackWriter, times(expectedSelectCalls)).select(any(),
any());
}
@Test
- public void testPartialPackFilesScanWhenDoingSearchForReuseTimeoutCheck()
+ public void partialPackFilesScanWhenDoingSearchForReuseTimeoutCheck()
throws Exception {
+ int objectsInMultiplePacks = 2;
+ int objectsInOnePacks = 1;
+ int expectedSelectCalls = objectsInMultiplePacks + objectsInOnePacks;
+ testPartialPackFilesScanWhenDoingSearchForReuseTimeoutCheck(true, expectedSelectCalls);
+ testPartialPackFilesScanWhenDoingSearchForReuseTimeoutCheck(false, expectedSelectCalls);
+ }
+
+ public void testPartialPackFilesScanWhenDoingSearchForReuseTimeoutCheck(
+ boolean doReturn, int expectedSelectCalls) throws Exception {
FileRepository fileRepository = setUpRepoWithMultiplePackfiles();
+
PackConfig packConfig = new PackConfig();
packConfig.setSearchForReuseTimeout(Duration.ofSeconds(-1));
PackWriter mockedPackWriter = Mockito.spy(
new PackWriter(packConfig, fileRepository.newObjectReader()));
mockedPackWriter.enableSearchForReuseTimeout();
- doNothing().when(mockedPackWriter).select(any(), any());
+ doReturn(doReturn).when(mockedPackWriter).select(any(), any());
try (FileOutputStream packOS = new FileOutputStream(
getPackFileToWrite(fileRepository, mockedPackWriter))) {
@@ -740,7 +802,6 @@ public class PackWriterTest extends SampleDataRepositoryTestCase {
NullProgressMonitor.INSTANCE, packOS);
}
- int expectedSelectCalls = 3; // Objects in packfiles
verify(mockedPackWriter, times(expectedSelectCalls)).select(any(),
any());
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BatchRefUpdateTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BatchRefUpdateTest.java
index daf4382719..a0afc3ef13 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BatchRefUpdateTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BatchRefUpdateTest.java
@@ -171,7 +171,7 @@ public class BatchRefUpdateTest extends LocalDiskRepositoryTestCase {
assertEquals(c2.getResult(), ReceiveCommand.Result.OK);
}
- File packed = new File(diskRepo.getDirectory(), "packed-refs");
+ File packed = new File(diskRepo.getCommonDirectory(), "packed-refs");
String packedStr = new String(Files.readAllBytes(packed.toPath()),
UTF_8);
@@ -1263,7 +1263,7 @@ public class BatchRefUpdateTest extends LocalDiskRepositoryTestCase {
}
private ReflogEntry getLastReflog(String name) throws IOException {
- ReflogReader r = diskRepo.getReflogReader(name);
+ ReflogReader r = diskRepo.getRefDatabase().getReflogReader(name);
if (r == null) {
return null;
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileReftableStackTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileReftableStackTest.java
index 6c7992716c..e8363ce21d 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileReftableStackTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileReftableStackTest.java
@@ -81,8 +81,7 @@ public class FileReftableStackTest {
}
public void testCompaction(int N) throws Exception {
- try (FileReftableStack stack = new FileReftableStack(
- new File(reftableDir, "refs"), reftableDir, null,
+ try (FileReftableStack stack = new FileReftableStack(reftableDir, null,
() -> new Config())) {
writeBranches(stack, "refs/heads/branch%d", 0, N);
MergedReftable table = stack.getMergedReftable();
@@ -124,8 +123,7 @@ public class FileReftableStackTest {
// Can't delete in-use files on Windows.
assumeFalse(SystemReader.getInstance().isWindows());
- try (FileReftableStack stack = new FileReftableStack(
- new File(reftableDir, "refs"), reftableDir, null,
+ try (FileReftableStack stack = new FileReftableStack(reftableDir, null,
() -> new Config())) {
outer: for (int i = 0; i < 10; i++) {
final long next = stack.getMergedReftable().maxUpdateIndex()
@@ -152,8 +150,8 @@ public class FileReftableStackTest {
}
}
assertThrows(FileNotFoundException.class,
- () -> new FileReftableStack(new File(reftableDir, "refs"),
- reftableDir, null, () -> new Config()));
+ () -> new FileReftableStack(reftableDir, null,
+ () -> new Config()));
}
@Test
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileReftableTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileReftableTest.java
index 32342e3563..5756b41442 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileReftableTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileReftableTest.java
@@ -23,6 +23,7 @@ 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 static org.junit.Assume.assumeTrue;
import java.io.File;
import java.io.FileOutputStream;
@@ -33,8 +34,15 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
-
import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.NullProgressMonitor;
@@ -51,6 +59,10 @@ import org.eclipse.jgit.lib.RepositoryCache;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.test.resources.SampleDataRepositoryTestCase;
import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.FS.ExecutionResult;
+import org.eclipse.jgit.util.RawParseUtils;
+import org.eclipse.jgit.util.TemporaryBuffer;
import org.junit.Test;
public class FileReftableTest extends SampleDataRepositoryTestCase {
@@ -66,6 +78,30 @@ public class FileReftableTest extends SampleDataRepositoryTestCase {
@SuppressWarnings("boxing")
@Test
+ public void testReloadIfNecessary() throws Exception {
+ ObjectId id = db.resolve("master");
+ try (FileRepository repo1 = new FileRepository(db.getDirectory());
+ FileRepository repo2 = new FileRepository(db.getDirectory())) {
+ ((FileReftableDatabase) repo1.getRefDatabase())
+ .setAutoRefresh(true);
+ ((FileReftableDatabase) repo2.getRefDatabase())
+ .setAutoRefresh(true);
+ FileRepository repos[] = { repo1, repo2 };
+ for (int i = 0; i < 10; i++) {
+ for (int j = 0; j < 2; j++) {
+ FileRepository repo = repos[j];
+ RefUpdate u = repo.getRefDatabase().newUpdate(
+ String.format("branch%d", i * 10 + j), false);
+ u.setNewObjectId(id);
+ RefUpdate.Result r = u.update();
+ assertEquals(Result.NEW, r);
+ }
+ }
+ }
+ }
+
+ @SuppressWarnings("boxing")
+ @Test
public void testRacyReload() throws Exception {
ObjectId id = db.resolve("master");
int retry = 0;
@@ -87,13 +123,61 @@ public class FileReftableTest extends SampleDataRepositoryTestCase {
u.setNewObjectId(id);
r = u.update();
- assertEquals(r, Result.NEW);
+ assertEquals(Result.NEW, r);
}
}
}
// only the first one succeeds
- assertEquals(retry, 19);
+ assertEquals(19, retry);
+ }
+ }
+
+ @Test
+ public void testConcurrentRacyReload() throws Exception {
+ ObjectId id = db.resolve("master");
+ final CyclicBarrier barrier = new CyclicBarrier(2);
+
+ class UpdateRef implements Callable<RefUpdate.Result> {
+
+ private RefUpdate u;
+
+ UpdateRef(FileRepository repo, String branchName)
+ throws IOException {
+ u = repo.getRefDatabase().newUpdate(branchName,
+ false);
+ u.setNewObjectId(id);
+ }
+
+ @Override
+ public RefUpdate.Result call() throws Exception {
+ barrier.await(); // wait for the other thread to prepare
+ return u.update();
+ }
+ }
+
+ ExecutorService pool = Executors.newFixedThreadPool(2);
+ try (FileRepository repo1 = new FileRepository(db.getDirectory());
+ FileRepository repo2 = new FileRepository(db.getDirectory())) {
+ ((FileReftableDatabase) repo1.getRefDatabase())
+ .setAutoRefresh(true);
+ ((FileReftableDatabase) repo2.getRefDatabase())
+ .setAutoRefresh(true);
+ for (int i = 0; i < 10; i++) {
+ String branchName = String.format("branch%d",
+ Integer.valueOf(i));
+ Future<RefUpdate.Result> ru1 = pool
+ .submit(new UpdateRef(repo1, branchName));
+ Future<RefUpdate.Result> ru2 = pool
+ .submit(new UpdateRef(repo2, branchName));
+ assertTrue((ru1.get() == Result.NEW
+ && ru2.get() == Result.LOCK_FAILURE)
+ || (ru1.get() == Result.LOCK_FAILURE
+ && ru2.get() == Result.NEW));
+ }
+ } finally {
+ pool.shutdown();
+ pool.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
}
}
@@ -105,13 +189,13 @@ public class FileReftableTest extends SampleDataRepositoryTestCase {
RefUpdate u = db.updateRef("refs/heads/master");
u.setForceUpdate(true);
u.setNewObjectId((i%2) == 0 ? c1 : c2);
- assertEquals(u.update(), FORCED);
+ assertEquals(FORCED, u.update());
}
File tableDir = new File(db.getDirectory(), Constants.REFTABLE);
assertTrue(tableDir.listFiles().length > 2);
((FileReftableDatabase)db.getRefDatabase()).compactFully();
- assertEquals(tableDir.listFiles().length,2);
+ assertEquals(2, tableDir.listFiles().length);
}
@Test
@@ -171,9 +255,10 @@ public class FileReftableTest extends SampleDataRepositoryTestCase {
v.update();
db.convertToPackedRefs(true, false);
- List<ReflogEntry> logs = db.getReflogReader("refs/heads/master").getReverseEntries(2);
- assertEquals(logs.get(0).getComment(), "banana");
- assertEquals(logs.get(1).getComment(), "apple");
+ List<ReflogEntry> logs = db.getRefDatabase()
+ .getReflogReader("refs/heads/master").getReverseEntries(2);
+ assertEquals("banana", logs.get(0).getComment());
+ assertEquals("apple", logs.get(1).getComment());
}
@Test
@@ -185,8 +270,9 @@ public class FileReftableTest extends SampleDataRepositoryTestCase {
ReceiveCommand rc1 = new ReceiveCommand(ObjectId.zeroId(), cur, "refs/heads/batch1");
ReceiveCommand rc2 = new ReceiveCommand(ObjectId.zeroId(), prev, "refs/heads/batch2");
String msg = "message";
+ RefDatabase refDb = db.getRefDatabase();
try (RevWalk rw = new RevWalk(db)) {
- db.getRefDatabase().newBatchUpdate()
+ refDb.newBatchUpdate()
.addCommand(rc1, rc2)
.setAtomic(true)
.setRefLogIdent(person)
@@ -194,15 +280,17 @@ public class FileReftableTest extends SampleDataRepositoryTestCase {
.execute(rw, NullProgressMonitor.INSTANCE);
}
- assertEquals(rc1.getResult(), ReceiveCommand.Result.OK);
- assertEquals(rc2.getResult(), ReceiveCommand.Result.OK);
+ assertEquals(ReceiveCommand.Result.OK, rc1.getResult());
+ assertEquals(ReceiveCommand.Result.OK, rc2.getResult());
- ReflogEntry e = db.getReflogReader("refs/heads/batch1").getLastEntry();
+ ReflogEntry e = refDb.getReflogReader("refs/heads/batch1")
+ .getLastEntry();
assertEquals(msg, e.getComment());
assertEquals(person, e.getWho());
assertEquals(cur, e.getNewId());
- e = db.getReflogReader("refs/heads/batch2").getLastEntry();
+ e = refDb.getReflogReader("refs/heads/batch2")
+ .getLastEntry();
assertEquals(msg, e.getComment());
assertEquals(person, e.getWho());
assertEquals(prev, e.getNewId());
@@ -267,7 +355,7 @@ public class FileReftableTest extends SampleDataRepositoryTestCase {
RefUpdate up = db.getRefDatabase().newUpdate("refs/heads/a", false);
up.setForceUpdate(true);
RefUpdate.Result res = up.delete();
- assertEquals(res, FORCED);
+ assertEquals(FORCED, res);
assertNull(db.exactRef("refs/heads/a"));
}
@@ -309,7 +397,7 @@ public class FileReftableTest extends SampleDataRepositoryTestCase {
// the branch HEAD referred to is left untouched
assertEquals(pid, db.resolve("refs/heads/master"));
- ReflogReader reflogReader = db.getReflogReader("HEAD");
+ ReflogReader reflogReader = db.getRefDatabase().getReflogReader("HEAD");
ReflogEntry e = reflogReader.getReverseEntries().get(0);
assertEquals(ppid, e.getNewId());
assertEquals("GIT_COMMITTER_EMAIL", e.getWho().getEmailAddress());
@@ -330,12 +418,13 @@ public class FileReftableTest extends SampleDataRepositoryTestCase {
updateRef.setForceUpdate(true);
RefUpdate.Result update = updateRef.update();
assertEquals(FORCED, update); // internal
- ReflogReader r = db.getReflogReader("refs/heads/master");
+ ReflogReader r = db.getRefDatabase()
+ .getReflogReader("refs/heads/master");
ReflogEntry e = r.getLastEntry();
- assertEquals(e.getNewId(), pid);
- assertEquals(e.getComment(), "REFLOG!: FORCED");
- assertEquals(e.getWho(), person);
+ assertEquals(pid, e.getNewId());
+ assertEquals("REFLOG!: FORCED", e.getComment());
+ assertEquals(person, e.getWho());
}
@Test
@@ -352,10 +441,11 @@ public class FileReftableTest extends SampleDataRepositoryTestCase {
ref = db.updateRef(newRef);
ref.setNewObjectId(db.resolve(Constants.HEAD));
- assertEquals(ref.delete(), RefUpdate.Result.NO_CHANGE);
+ assertEquals(RefUpdate.Result.NO_CHANGE, ref.delete());
// Differs from RefupdateTest. Deleting a loose ref leaves reflog trail.
- ReflogReader reader = db.getReflogReader("refs/heads/abc");
+ ReflogReader reader = db.getRefDatabase()
+ .getReflogReader("refs/heads/abc");
assertEquals(ObjectId.zeroId(), reader.getReverseEntry(1).getOldId());
assertEquals(nonZero, reader.getReverseEntry(1).getNewId());
assertEquals(nonZero, reader.getReverseEntry(0).getOldId());
@@ -382,8 +472,9 @@ public class FileReftableTest extends SampleDataRepositoryTestCase {
assertNotSame(newid, r.getObjectId());
assertSame(ObjectId.class, r.getObjectId().getClass());
assertEquals(newid, r.getObjectId());
- List<ReflogEntry> reverseEntries1 = db.getReflogReader("refs/heads/abc")
- .getReverseEntries();
+ RefDatabase refDb = db.getRefDatabase();
+ List<ReflogEntry> reverseEntries1 = refDb
+ .getReflogReader("refs/heads/abc").getReverseEntries();
ReflogEntry entry1 = reverseEntries1.get(0);
assertEquals(1, reverseEntries1.size());
assertEquals(ObjectId.zeroId(), entry1.getOldId());
@@ -392,7 +483,7 @@ public class FileReftableTest extends SampleDataRepositoryTestCase {
assertEquals(new PersonIdent(db).toString(),
entry1.getWho().toString());
assertEquals("", entry1.getComment());
- List<ReflogEntry> reverseEntries2 = db.getReflogReader("HEAD")
+ List<ReflogEntry> reverseEntries2 = refDb.getReflogReader("HEAD")
.getReverseEntries();
assertEquals(0, reverseEntries2.size());
}
@@ -431,7 +522,7 @@ public class FileReftableTest extends SampleDataRepositoryTestCase {
Ref head = db.exactRef("HEAD");
assertTrue(head.isSymbolic());
- assertEquals(head.getTarget().getName(), "refs/heads/unborn");
+ assertEquals("refs/heads/unborn", head.getTarget().getName());
}
/**
@@ -455,7 +546,7 @@ public class FileReftableTest extends SampleDataRepositoryTestCase {
// the branch HEAD referred to is left untouched
assertNull(db.resolve("refs/heads/unborn"));
- ReflogReader reflogReader = db.getReflogReader("HEAD");
+ ReflogReader reflogReader = db.getRefDatabase().getReflogReader("HEAD");
ReflogEntry e = reflogReader.getReverseEntries().get(0);
assertEquals(ObjectId.zeroId(), e.getOldId());
assertEquals(ppid, e.getNewId());
@@ -499,7 +590,7 @@ public class FileReftableTest extends SampleDataRepositoryTestCase {
names.add("refs/heads/new/name");
for (String nm : names) {
- ReflogReader rd = db.getReflogReader(nm);
+ ReflogReader rd = db.getRefDatabase().getReflogReader(nm);
assertNotNull(rd);
ReflogEntry last = rd.getLastEntry();
ObjectId id = last.getNewId();
@@ -573,10 +664,10 @@ public class FileReftableTest extends SampleDataRepositoryTestCase {
assertTrue(res == Result.NEW || res == FORCED);
}
- assertEquals(refDb.exactRef(refName).getObjectId(), bId);
+ assertEquals(bId, refDb.exactRef(refName).getObjectId());
assertTrue(randomStr.equals(refDb.getReflogReader(refName).getReverseEntry(1).getComment()));
refDb.compactFully();
- assertEquals(refDb.exactRef(refName).getObjectId(), bId);
+ assertEquals(bId, refDb.exactRef(refName).getObjectId());
assertTrue(randomStr.equals(refDb.getReflogReader(refName).getReverseEntry(1).getComment()));
}
@@ -644,6 +735,54 @@ public class FileReftableTest extends SampleDataRepositoryTestCase {
checkContainsRef(refs, db.exactRef("HEAD"));
}
+ @Test
+ public void testExternalUpdate_bug_102() throws Exception {
+ ((FileReftableDatabase) db.getRefDatabase()).setAutoRefresh(true);
+ assumeTrue(atLeastGitVersion(2, 45));
+ Git git = Git.wrap(db);
+ git.tag().setName("foo").call();
+ Ref ref = db.exactRef("refs/tags/foo");
+ assertNotNull(ref);
+ runGitCommand("tag", "--force", "foo", "e");
+ Ref e = db.exactRef("refs/heads/e");
+ Ref foo = db.exactRef("refs/tags/foo");
+ assertEquals(e.getObjectId(), foo.getObjectId());
+ }
+
+ private String toString(TemporaryBuffer b) throws IOException {
+ return RawParseUtils.decode(b.toByteArray());
+ }
+
+ private ExecutionResult runGitCommand(String... args)
+ throws IOException, InterruptedException {
+ FS fs = db.getFS();
+ ProcessBuilder pb = fs.runInShell("git", args);
+ pb.directory(db.getWorkTree());
+ System.err.println("PATH=" + pb.environment().get("PATH"));
+ ExecutionResult result = fs.execute(pb, null);
+ assertEquals(0, result.getRc());
+ String err = toString(result.getStderr());
+ if (!err.isEmpty()) {
+ System.err.println(err);
+ }
+ String out = toString(result.getStdout());
+ if (!out.isEmpty()) {
+ System.out.println(out);
+ }
+ return result;
+ }
+
+ private boolean atLeastGitVersion(int minMajor, int minMinor)
+ throws IOException, InterruptedException {
+ String version = toString(runGitCommand("version").getStdout())
+ .split(" ")[2];
+ System.out.println(version);
+ String[] digits = version.split("\\.");
+ int major = Integer.parseInt(digits[0]);
+ int minor = Integer.parseInt(digits[1]);
+ return (major >= minMajor) && (minor >= minMinor);
+ }
+
private RefUpdate updateRef(String name) throws IOException {
final RefUpdate ref = db.updateRef(name);
ref.setNewObjectId(db.resolve(Constants.HEAD));
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileRepositoryBuilderAfterOpenConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileRepositoryBuilderAfterOpenConfigTest.java
index 100bd32ad8..ed5a6990ac 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileRepositoryBuilderAfterOpenConfigTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileRepositoryBuilderAfterOpenConfigTest.java
@@ -18,7 +18,6 @@ import org.eclipse.jgit.util.SystemReader;
import org.junit.Before;
public class FileRepositoryBuilderAfterOpenConfigTest extends FileRepositoryBuilderTest {
- /** {@inheritDoc} */
@Before
@Override
public void setUp() throws Exception {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcBasicPackingTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcBasicPackingTest.java
index 6cad8b6c62..434f7e4bef 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcBasicPackingTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcBasicPackingTest.java
@@ -16,9 +16,9 @@ import static org.junit.Assert.assertTrue;
import java.io.File;
import java.io.IOException;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.Date;
import java.util.List;
import org.eclipse.jgit.junit.TestRepository.BranchBuilder;
@@ -206,7 +206,7 @@ public class GcBasicPackingTest extends GcTestCase {
// The old packfile is too young to be deleted. We should end up with
// two pack files
- gc.setExpire(new Date(oldPackfile.lastModified() - 1));
+ gc.setExpire(Instant.ofEpochMilli(oldPackfile.lastModified() - 1));
gc.gc().get();
stats = gc.getStatistics();
assertEquals(0, stats.numberOfLooseObjects);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcConcurrentTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcConcurrentTest.java
index 1519873b62..8c1b4f7b65 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcConcurrentTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcConcurrentTest.java
@@ -10,7 +10,6 @@
package org.eclipse.jgit.internal.storage.file;
-import static java.lang.Integer.valueOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
@@ -73,7 +72,7 @@ public class GcConcurrentTest extends GcTestCase {
try {
gc.setProgressMonitor(this);
gc.repack();
- return valueOf(0);
+ return Integer.valueOf(0);
} catch (IOException e) {
// leave the syncPoint in broken state so any awaiting
// threads and any threads that call await in the future get
@@ -84,7 +83,7 @@ public class GcConcurrentTest extends GcTestCase {
} catch (InterruptedException ignored) {
//
}
- return valueOf(1);
+ return Integer.valueOf(1);
}
}
}
@@ -186,13 +185,12 @@ public class GcConcurrentTest extends GcTestCase {
// make sure gc() has caused creation of a new packfile
assertNotEquals(oldPackName, newPackName);
- // Even when asking again for the set of packfiles outdated data
- // will be returned. As long as the repository can work on cached data
- // it will do so and not detect that a new packfile exists.
- assertNotEquals(getSinglePack(repository).getPackName(), newPackName);
+ // When asking again for the set of packfiles the new updated data
+ // will be returned because of the rescan of the pack directory.
+ assertEquals(getSinglePack(repository).getPackName(), newPackName);
- // Only when accessing object content it is required to rescan the pack
- // directory and the new packfile will be detected.
+ // When accessing object content the new packfile refreshed from
+ // the rescan triggered from the list of packs.
repository.getObjectDatabase().open(b).getSize();
assertEquals(getSinglePack(repository).getPackName(), newPackName);
assertNotNull(getSinglePack(repository).getBitmapIndex());
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcKeepFilesTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcKeepFilesTest.java
index 840c09896d..e1b6778c0e 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcKeepFilesTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcKeepFilesTest.java
@@ -10,8 +10,13 @@
package org.eclipse.jgit.internal.storage.file;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_BUILD_BITMAPS;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_PACK_KEPT_OBJECTS;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_PACK_SECTION;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_REPACK_SECTION;
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.util.Iterator;
@@ -19,9 +24,14 @@ import java.util.Iterator;
import org.eclipse.jgit.internal.storage.file.PackIndex.MutableEntry;
import org.eclipse.jgit.internal.storage.pack.PackExt;
import org.eclipse.jgit.junit.TestRepository.BranchBuilder;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.storage.pack.PackConfig;
import org.junit.Test;
public class GcKeepFilesTest extends GcTestCase {
+ private static final int COMMIT_AND_TREE_OBJECTS = 2;
+
@Test
public void testKeepFiles() throws Exception {
BranchBuilder bb = tr.branch("refs/heads/master");
@@ -48,11 +58,16 @@ public class GcKeepFilesTest extends GcTestCase {
assertEquals(4, stats.numberOfLooseObjects);
assertEquals(4, stats.numberOfPackedObjects);
assertEquals(1, stats.numberOfPackFiles);
+ PackFile bitmapFile = singlePack.getPackFile().create(PackExt.BITMAP_INDEX);
+ assertTrue(keepFile.exists());
+ assertTrue(bitmapFile.delete());
+ gc.setPackKeptObjects(false);
gc.gc().get();
stats = gc.getStatistics();
assertEquals(0, stats.numberOfLooseObjects);
assertEquals(8, stats.numberOfPackedObjects);
assertEquals(2, stats.numberOfPackFiles);
+ assertEquals(1, stats.numberOfBitmaps);
// check that no object is packed twice
Iterator<Pack> packs = repo.getObjectDatabase().getPacks()
@@ -68,4 +83,149 @@ public class GcKeepFilesTest extends GcTestCase {
+ e.toObjectId(),
ind2.hasObject(e.toObjectId()));
}
+
+ @Test
+ public void testKeptObjectsAreIncludedByDefault() throws Exception {
+ testKeptObjectsAreIncluded();
+ }
+
+ @Test
+ public void testKeptObjectsAreIncludedByDefaultWhenBuildBitmapsIsTrue()
+ throws Exception {
+ PackConfig packConfig = new PackConfig();
+ Config repoConfig = repo.getObjectDatabase().getConfig();
+ repoConfig.setBoolean(CONFIG_PACK_SECTION, null,
+ CONFIG_KEY_BUILD_BITMAPS, true);
+ packConfig.fromConfig(repoConfig);
+ gc.setPackConfig(packConfig);
+
+ testKeptObjectsAreIncluded();
+ }
+
+ @Test
+ public void testKeptObjectsAreIncludedWhenPackKeptObjectsIsFalseButOverriddenViaCommandLine()
+ throws Exception {
+ PackConfig packConfig = new PackConfig();
+ packConfig.setPackKeptObjects(false);
+ gc.setPackConfig(packConfig);
+ gc.setPackKeptObjects(true);
+
+ testKeptObjectsAreIncluded();
+ }
+
+ @Test
+ public void testKeptObjectsAreNotIncludedByDefaultWhenBuildBitmapsIsFalse()
+ throws Exception {
+ PackConfig packConfig = new PackConfig();
+ packConfig.setBuildBitmaps(false);
+ gc.setPackConfig(packConfig);
+
+ testKeptObjectsAreNotIncluded();
+ }
+
+ @Test
+ public void testKeptObjectsAreIncludedWhenBuildBitmapsIsFalseButPackKeptObjectsIsTrue()
+ throws Exception {
+ PackConfig packConfig = new PackConfig();
+ Config repoConfig = repo.getObjectDatabase().getConfig();
+ repoConfig.setBoolean(CONFIG_PACK_SECTION, null,
+ CONFIG_KEY_BUILD_BITMAPS, false);
+ repoConfig.setBoolean(CONFIG_REPACK_SECTION, null,
+ CONFIG_KEY_PACK_KEPT_OBJECTS, true);
+ packConfig.fromConfig(repoConfig);
+ gc.setPackConfig(packConfig);
+
+ testKeptObjectsAreIncluded();
+ }
+
+ @Test
+ public void testKeptObjectsAreNotIncludedWhenPackKeptObjectsIsTrueButOverriddenViaCommandLine()
+ throws Exception {
+ PackConfig packConfig = new PackConfig();
+ packConfig.setPackKeptObjects(true);
+ gc.setPackConfig(packConfig);
+ gc.setPackKeptObjects(false);
+
+ testKeptObjectsAreNotIncluded();
+ }
+
+ @Test
+ public void testKeptObjectsAreNotIncludedWhenPackKeptObjectsConfigIsFalse()
+ throws Exception {
+ PackConfig packConfig = new PackConfig();
+ packConfig.setPackKeptObjects(false);
+ gc.setPackConfig(packConfig);
+
+ testKeptObjectsAreNotIncluded();
+ }
+
+ private void testKeptObjectsAreIncluded() throws Exception {
+ BranchBuilder bb = tr.branch("refs/heads/master");
+ ObjectId commitObjectInLockedPack = bb.commit().create().toObjectId();
+ gc.gc().get();
+ stats = gc.getStatistics();
+ assertEquals(COMMIT_AND_TREE_OBJECTS, stats.numberOfPackedObjects);
+ assertEquals(1, stats.numberOfPackFiles);
+ assertTrue(getSinglePack().getPackFile().create(PackExt.KEEP)
+ .createNewFile());
+
+ bb.commit().create();
+ gc.gc().get();
+ stats = gc.getStatistics();
+ assertEquals(2 * COMMIT_AND_TREE_OBJECTS + 1,
+ stats.numberOfPackedObjects);
+ assertEquals(2, stats.numberOfPackFiles);
+
+ PackIndex lockedPackIdx = null;
+ PackIndex newPackIdx = null;
+ for (Pack pack : repo.getObjectDatabase().getPacks()) {
+ if (pack.getObjectCount() == COMMIT_AND_TREE_OBJECTS) {
+ lockedPackIdx = pack.getIndex();
+ } else {
+ newPackIdx = pack.getIndex();
+ }
+ }
+ assertNotNull(lockedPackIdx);
+ assertTrue(lockedPackIdx.hasObject(commitObjectInLockedPack));
+ assertNotNull(newPackIdx);
+ assertTrue(newPackIdx.hasObject(commitObjectInLockedPack));
+ }
+
+ private void testKeptObjectsAreNotIncluded() throws Exception {
+ BranchBuilder bb = tr.branch("refs/heads/master");
+ ObjectId commitObjectInLockedPack = bb.commit().create().toObjectId();
+ gc.gc().get();
+ stats = gc.getStatistics();
+ assertEquals(COMMIT_AND_TREE_OBJECTS, stats.numberOfPackedObjects);
+ assertEquals(1, stats.numberOfPackFiles);
+ assertTrue(getSinglePack().getPackFile().create(PackExt.KEEP)
+ .createNewFile());
+
+ bb.commit().create();
+ gc.gc().get();
+ stats = gc.getStatistics();
+ assertEquals(COMMIT_AND_TREE_OBJECTS + 1, stats.numberOfPackedObjects);
+ assertEquals(2, stats.numberOfPackFiles);
+
+ PackIndex lockedPackIdx = null;
+ PackIndex newPackIdx = null;
+ for (Pack pack : repo.getObjectDatabase().getPacks()) {
+ if (pack.getObjectCount() == COMMIT_AND_TREE_OBJECTS) {
+ lockedPackIdx = pack.getIndex();
+ } else {
+ newPackIdx = pack.getIndex();
+ }
+ }
+ assertNotNull(lockedPackIdx);
+ assertTrue(lockedPackIdx.hasObject(commitObjectInLockedPack));
+ assertNotNull(newPackIdx);
+ assertFalse(newPackIdx.hasObject(commitObjectInLockedPack));
+ }
+
+ private Pack getSinglePack() {
+ Iterator<Pack> packIt = repo.getObjectDatabase().getPacks().iterator();
+ Pack singlePack = packIt.next();
+ assertFalse(packIt.hasNext());
+ return singlePack;
+ }
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcNumberOfPackFilesSinceBitmapStatisticsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcNumberOfPackFilesSinceBitmapStatisticsTest.java
new file mode 100644
index 0000000000..cd1264ef55
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcNumberOfPackFilesSinceBitmapStatisticsTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2024 Jacek Centkowski <geminica.programs@gmail.com> and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.storage.file;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.stream.StreamSupport;
+
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
+public class GcNumberOfPackFilesSinceBitmapStatisticsTest extends GcTestCase {
+ @Test
+ public void testShouldReportZeroObjectsForInitializedRepo()
+ throws IOException {
+ assertEquals(0L, gc.getStatistics().numberOfPackFilesSinceBitmap);
+ }
+
+ @Test
+ public void testShouldReportAllPackFilesWhenNoGcWasPerformed()
+ throws Exception {
+ tr.packAndPrune();
+ long result = gc.getStatistics().numberOfPackFilesSinceBitmap;
+
+ assertEquals(repo.getObjectDatabase().getPacks().size(), result);
+ }
+
+ @Test
+ public void testShouldReportNoObjectsDirectlyAfterGc() throws Exception {
+ // given
+ addCommit(null);
+ gc.gc().get();
+ assertEquals(1L, repositoryBitmapFiles());
+ assertEquals(0L, gc.getStatistics().numberOfPackFilesSinceBitmap);
+ }
+
+ @Test
+ public void testShouldReportNewObjectsSinceGcWhenRepositoryProgresses()
+ throws Exception {
+ // commit & gc
+ RevCommit parent = addCommit(null);
+ gc.gc().get();
+ assertEquals(1L, repositoryBitmapFiles());
+
+ // progress & pack
+ addCommit(parent);
+ tr.packAndPrune();
+
+ assertEquals(1L, gc.getStatistics().numberOfPackFilesSinceBitmap);
+ }
+
+ @Test
+ public void testShouldReportNewObjectsFromTheLatestBitmapWhenRepositoryProgresses()
+ throws Exception {
+ // commit & gc
+ RevCommit parent = addCommit(null);
+ gc.gc().get();
+ assertEquals(1L, repositoryBitmapFiles());
+
+ // progress & gc
+ parent = addCommit(parent);
+ gc.gc().get();
+ assertEquals(2L, repositoryBitmapFiles());
+
+ // progress & pack
+ addCommit(parent);
+ tr.packAndPrune();
+
+ assertEquals(1L, gc.getStatistics().numberOfPackFilesSinceBitmap);
+ }
+
+ private RevCommit addCommit(RevCommit parent) throws Exception {
+ PersonIdent ident = new PersonIdent("repo-metrics", "repo@metrics.com");
+ TestRepository<FileRepository>.CommitBuilder builder = tr.commit()
+ .author(ident);
+ if (parent != null) {
+ builder.parent(parent);
+ }
+ RevCommit commit = builder.create();
+ tr.update("master", commit);
+ parent = commit;
+ return parent;
+ }
+
+ private long repositoryBitmapFiles() throws IOException {
+ return StreamSupport
+ .stream(Files
+ .newDirectoryStream(repo.getObjectDatabase()
+ .getPackDirectory().toPath(), "pack-*.bitmap")
+ .spliterator(), false)
+ .count();
+ }
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcObjectSizeIndexTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcObjectSizeIndexTest.java
new file mode 100644
index 0000000000..1a05d88583
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcObjectSizeIndexTest.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright (C) 2025, Google LLC. and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.internal.storage.file;
+
+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.util.Collection;
+
+import org.eclipse.jgit.internal.storage.pack.PackExt;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.revwalk.RevBlob;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.storage.pack.PackConfig;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class GcObjectSizeIndexTest extends GcTestCase {
+
+ @Test
+ public void gc_2commits_noSizeLimit_blobsInIndex() throws Exception {
+ TestRepository<FileRepository>.BranchBuilder bb = tr
+ .branch("refs/heads/master");
+ RevBlob blobA1 = tr.blob("7-bytes");
+ RevBlob blobA2 = tr.blob("11-bytes xx");
+ RevBlob blobB1 = tr.blob("B");
+ RevBlob blobB2 = tr.blob("B2");
+ bb.commit().add("A", blobA1).add("B", blobB1).create();
+ bb.commit().add("A", blobA2).add("B", blobB2).create();
+
+ stats = gc.getStatistics();
+ assertEquals(8, stats.numberOfLooseObjects);
+ assertEquals(0, stats.numberOfPackedObjects);
+ configureGc(gc, 0);
+ gc.gc().get();
+
+ stats = gc.getStatistics();
+ assertEquals(1, stats.numberOfPackFiles);
+ assertEquals(4, stats.numberOfSizeIndexedObjects);
+
+ assertTrue(getOnlyPack(repo).hasObjectSizeIndex());
+ Pack pack = getOnlyPack(repo);
+ assertEquals(7, pack.getIndexedObjectSize(blobA1));
+ assertEquals(11, pack.getIndexedObjectSize(blobA2));
+ assertEquals(1, pack.getIndexedObjectSize(blobB1));
+ assertEquals(2, pack.getIndexedObjectSize(blobB2));
+ }
+
+ @Test
+ public void gc_2commits_sizeLimit_biggerBlobsInIndex() throws Exception {
+ TestRepository<FileRepository>.BranchBuilder bb = tr
+ .branch("refs/heads/master");
+ RevBlob blobA1 = tr.blob("7-bytes");
+ RevBlob blobA2 = tr.blob("11-bytes xx");
+ RevBlob blobB1 = tr.blob("B");
+ RevBlob blobB2 = tr.blob("B2");
+ bb.commit().add("A", blobA1).add("B", blobB1).create();
+ bb.commit().add("A", blobA2).add("B", blobB2).create();
+
+ stats = gc.getStatistics();
+ assertEquals(8, stats.numberOfLooseObjects);
+ assertEquals(0, stats.numberOfPackedObjects);
+ configureGc(gc, 5);
+ gc.gc().get();
+
+ stats = gc.getStatistics();
+ assertEquals(1, stats.numberOfPackFiles);
+ assertEquals(2, stats.numberOfSizeIndexedObjects);
+
+ assertTrue(getOnlyPack(repo).hasObjectSizeIndex());
+ Pack pack = getOnlyPack(repo);
+ assertEquals(7, pack.getIndexedObjectSize(blobA1));
+ assertEquals(11, pack.getIndexedObjectSize(blobA2));
+ assertEquals(-1, pack.getIndexedObjectSize(blobB1));
+ assertEquals(-1, pack.getIndexedObjectSize(blobB2));
+ }
+
+ @Test
+ public void gc_2commits_disableSizeIdx_noIdx() throws Exception {
+ TestRepository<FileRepository>.BranchBuilder bb = tr
+ .branch("refs/heads/master");
+ RevBlob blobA1 = tr.blob("7-bytes");
+ RevBlob blobA2 = tr.blob("11-bytes xx");
+ RevBlob blobB1 = tr.blob("B");
+ RevBlob blobB2 = tr.blob("B2");
+ bb.commit().add("A", blobA1).add("B", blobB1).create();
+ bb.commit().add("A", blobA2).add("B", blobB2).create();
+
+ stats = gc.getStatistics();
+ assertEquals(8, stats.numberOfLooseObjects);
+ assertEquals(0, stats.numberOfPackedObjects);
+ configureGc(gc, -1);
+ gc.gc().get();
+
+
+ stats = gc.getStatistics();
+ assertEquals(1, stats.numberOfPackFiles);
+ assertEquals(0, stats.numberOfSizeIndexedObjects);
+ }
+
+ @Test
+ public void gc_alreadyPacked_noChanges()
+ throws Exception {
+ tr.branch("refs/heads/master").commit().add("A", "A").add("B", "B")
+ .create();
+ stats = gc.getStatistics();
+ assertEquals(4, stats.numberOfLooseObjects);
+ assertEquals(0, stats.numberOfPackedObjects);
+ configureGc(gc, 0);
+ gc.gc().get();
+
+ stats = gc.getStatistics();
+ assertEquals(4, stats.numberOfPackedObjects);
+ assertEquals(1, stats.numberOfPackFiles);
+ assertTrue(getOnlyPack(repo).hasObjectSizeIndex());
+ assertEquals(2, stats.numberOfSizeIndexedObjects);
+
+ // Do the gc again and check that it hasn't changed anything
+ gc.gc().get();
+ stats = gc.getStatistics();
+ assertEquals(4, stats.numberOfPackedObjects);
+ assertEquals(1, stats.numberOfPackFiles);
+ assertTrue(getOnlyPack(repo).hasObjectSizeIndex());
+ assertEquals(2, stats.numberOfSizeIndexedObjects);
+ }
+
+ @Test
+ public void gc_twoReachableCommits_oneUnreachable_twoPacks()
+ throws Exception {
+ TestRepository<FileRepository>.BranchBuilder bb = tr
+ .branch("refs/heads/master");
+ RevCommit first = bb.commit().add("A", "A").add("B", "B").create();
+ bb.commit().add("A", "A2").add("B", "B2").create();
+ tr.update("refs/heads/master", first);
+
+ stats = gc.getStatistics();
+ assertEquals(8, stats.numberOfLooseObjects);
+ assertEquals(0, stats.numberOfPackedObjects);
+ configureGc(gc, 0);
+ gc.gc().get();
+ stats = gc.getStatistics();
+ assertEquals(0, stats.numberOfLooseObjects);
+ assertEquals(8, stats.numberOfPackedObjects);
+ assertEquals(2, stats.numberOfPackFiles);
+ assertEquals(4, stats.numberOfSizeIndexedObjects);
+ }
+
+ @Test
+ public void gc_preserved_objSizeIdxIsPreserved() throws Exception {
+ Collection<Pack> oldPacks = preserveOldPacks();
+ assertEquals(1, oldPacks.size());
+ PackFile preserved = oldPacks.iterator().next().getPackFile()
+ .create(PackExt.OBJECT_SIZE_INDEX)
+ .createPreservedForDirectory(
+ repo.getObjectDatabase().getPreservedDirectory());
+ assertTrue(preserved.exists());
+ }
+
+ @Test
+ public void gc_preserved_prune_noPreserves() throws Exception {
+ preserveOldPacks();
+ configureGc(gc, 0).setPrunePreserved(true);
+ gc.gc().get();
+
+ assertFalse(repo.getObjectDatabase().getPreservedDirectory().exists());
+ }
+
+ private Collection<Pack> preserveOldPacks() throws Exception {
+ TestRepository<FileRepository>.BranchBuilder bb = tr
+ .branch("refs/heads/master");
+ bb.commit().message("P").add("P", "P").create();
+
+ // pack loose object into packfile
+ configureGc(gc, 0);
+ gc.setExpireAgeMillis(0);
+ gc.gc().get();
+ Collection<Pack> oldPacks = tr.getRepository().getObjectDatabase()
+ .getPacks();
+ PackFile oldPackfile = oldPacks.iterator().next().getPackFile();
+ assertTrue(oldPackfile.exists());
+
+ fsTick();
+ bb.commit().message("B").add("B", "Q").create();
+
+ // repack again but now without a grace period for packfiles. We should
+ // end up with a new packfile and the old one should be placed in the
+ // preserved directory
+ gc.setPackExpireAgeMillis(0);
+ configureGc(gc, 0).setPreserveOldPacks(true);
+ gc.gc().get();
+
+ File preservedPackFile = oldPackfile.createPreservedForDirectory(
+ repo.getObjectDatabase().getPreservedDirectory());
+ assertTrue(preservedPackFile.exists());
+ return oldPacks;
+ }
+
+ @Ignore
+ public void testPruneAndRestoreOldPacks() throws Exception {
+ String tempRef = "refs/heads/soon-to-be-unreferenced";
+ TestRepository<FileRepository>.BranchBuilder bb = tr.branch(tempRef);
+ bb.commit().add("A", "A").add("B", "B").create();
+
+ // Verify setup conditions
+ stats = gc.getStatistics();
+ assertEquals(4, stats.numberOfLooseObjects);
+ assertEquals(0, stats.numberOfPackedObjects);
+
+ // Force all referenced objects into packs (to avoid having loose objects)
+ configureGc(gc, 0);
+ gc.setExpireAgeMillis(0);
+ gc.setPackExpireAgeMillis(0);
+ gc.gc().get();
+ stats = gc.getStatistics();
+ assertEquals(0, stats.numberOfLooseObjects);
+ assertEquals(4, stats.numberOfPackedObjects);
+ assertEquals(1, stats.numberOfPackFiles);
+
+ // Delete the temp ref, orphaning its commit
+ RefUpdate update = tr.getRepository().getRefDatabase().newUpdate(tempRef, false);
+ update.setForceUpdate(true);
+ ObjectId objectId = update.getOldObjectId(); // remember it so we can restore it!
+ RefUpdate.Result result = update.delete();
+ assertEquals(RefUpdate.Result.FORCED, result);
+
+ fsTick();
+
+ // Repack with only orphaned commit, so packfile will be pruned
+ configureGc(gc, 0).setPreserveOldPacks(true);
+ gc.gc().get();
+ stats = gc.getStatistics();
+ assertEquals(0, stats.numberOfLooseObjects);
+ assertEquals(0, stats.numberOfPackedObjects);
+ assertEquals(0, stats.numberOfPackFiles);
+
+ // Restore the temp ref to the deleted commit, should restore old-packs!
+ update = tr.getRepository().getRefDatabase().newUpdate(tempRef, false);
+ update.setNewObjectId(objectId);
+ update.setExpectedOldObjectId(null);
+ result = update.update();
+ assertEquals(RefUpdate.Result.NEW, result);
+
+ stats = gc.getStatistics();
+ assertEquals(4, stats.numberOfPackedObjects);
+ assertEquals(1, stats.numberOfPackFiles);
+ }
+
+ private PackConfig configureGc(GC myGc, int minSize) {
+ PackConfig pconfig = new PackConfig(repo);
+ pconfig.setMinBytesForObjSizeIndex(minSize);
+ myGc.setPackConfig(pconfig);
+ return pconfig;
+ }
+
+ private Pack getOnlyPack(FileRepository fileRepo)
+ throws IOException {
+ Collection<Pack> packs = fileRepo.getObjectDatabase().getPacks();
+ if (packs.size() != 1) {
+ throw new IOException("More than one pack");
+ }
+
+ return packs.iterator().next();
+ }
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPackRefsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPackRefsTest.java
index 8baa3cc341..f84be21e82 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPackRefsTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPackRefsTest.java
@@ -19,7 +19,6 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import java.io.File;
-import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.BrokenBarrierException;
@@ -31,6 +30,8 @@ import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.PackRefsCommand;
+import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.junit.TestRepository.BranchBuilder;
import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.Constants;
@@ -49,7 +50,7 @@ public class GcPackRefsTest extends GcTestCase {
RevBlob a = tr.blob("a");
tr.lightweightTag("t", a);
- gc.packRefs();
+ packRefs(false);
assertSame(repo.exactRef("refs/tags/t").getStorage(), Storage.PACKED);
}
@@ -58,9 +59,9 @@ public class GcPackRefsTest extends GcTestCase {
String ref = "dir/ref";
tr.branch(ref).commit().create();
String name = repo.findRef(ref).getName();
- Path dir = repo.getDirectory().toPath().resolve(name).getParent();
+ Path dir = repo.getCommonDirectory().toPath().resolve(name).getParent();
assertNotNull(dir);
- gc.packRefs();
+ packRefs(true);
assertFalse(Files.exists(dir));
}
@@ -75,9 +76,9 @@ public class GcPackRefsTest extends GcTestCase {
Callable<Integer> packRefs = () -> {
syncPoint.await();
try {
- gc.packRefs();
+ packRefs(false);
return 0;
- } catch (IOException e) {
+ } catch (GitAPIException e) {
return 1;
}
};
@@ -102,7 +103,7 @@ public class GcPackRefsTest extends GcTestCase {
"refs/tags/t1"));
try {
refLock.lock();
- gc.packRefs();
+ packRefs(false);
} finally {
refLock.unlock();
}
@@ -145,7 +146,7 @@ public class GcPackRefsTest extends GcTestCase {
Future<Result> result2 = pool.submit(() -> {
refUpdateLockedRef.await();
- gc.packRefs();
+ packRefs(false);
packRefsDone.await();
return null;
});
@@ -173,19 +174,20 @@ public class GcPackRefsTest extends GcTestCase {
assertEquals(repo.exactRef("HEAD").getTarget().getName(),
"refs/heads/master");
assertNull(repo.exactRef("HEAD").getTarget().getObjectId());
- gc.packRefs();
+ PackRefsCommand packRefsCommand = git.packRefs().setAll(true);
+ packRefsCommand.call();
assertSame(repo.exactRef("HEAD").getStorage(), Storage.LOOSE);
assertEquals(repo.exactRef("HEAD").getTarget().getName(),
"refs/heads/master");
assertNull(repo.exactRef("HEAD").getTarget().getObjectId());
git.checkout().setName("refs/heads/side").call();
- gc.packRefs();
+ packRefsCommand.call();
assertSame(repo.exactRef("HEAD").getStorage(), Storage.LOOSE);
// check for detached HEAD
git.checkout().setName(first.getName()).call();
- gc.packRefs();
+ packRefsCommand.call();
assertSame(repo.exactRef("HEAD").getStorage(), Storage.LOOSE);
}
@@ -208,7 +210,7 @@ public class GcPackRefsTest extends GcTestCase {
assertEquals(repo.exactRef("HEAD").getTarget().getName(),
"refs/heads/master");
assertNull(repo.exactRef("HEAD").getTarget().getObjectId());
- gc.packRefs();
+ packRefs(true);
assertSame(repo.exactRef("HEAD").getStorage(), Storage.LOOSE);
assertEquals(repo.exactRef("HEAD").getTarget().getName(),
"refs/heads/master");
@@ -216,9 +218,14 @@ public class GcPackRefsTest extends GcTestCase {
// check for non-detached HEAD
repo.updateRef(Constants.HEAD).link("refs/heads/side");
- gc.packRefs();
+ packRefs(true);
assertSame(repo.exactRef("HEAD").getStorage(), Storage.LOOSE);
assertEquals(repo.exactRef("HEAD").getTarget().getObjectId(),
second.getId());
}
+
+ private void packRefs(boolean all) throws GitAPIException {
+ new PackRefsCommand(repo).setAll(all).call();
+ }
+
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPruneNonReferencedTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPruneNonReferencedTest.java
index ca0f6842fc..84ec132e24 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPruneNonReferencedTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPruneNonReferencedTest.java
@@ -16,8 +16,8 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.io.File;
+import java.time.Instant;
import java.util.Collections;
-import java.util.Date;
import org.eclipse.jgit.junit.TestRepository.BranchBuilder;
import org.eclipse.jgit.lib.ObjectId;
@@ -30,7 +30,7 @@ public class GcPruneNonReferencedTest extends GcTestCase {
@Test
public void nonReferencedNonExpiredObject_notPruned() throws Exception {
RevBlob a = tr.blob("a");
- gc.setExpire(new Date(lastModified(a)));
+ gc.setExpire(Instant.ofEpochMilli(lastModified(a)));
gc.prune(Collections.<ObjectId> emptySet());
assertTrue(repo.getObjectDatabase().has(a));
}
@@ -58,7 +58,7 @@ public class GcPruneNonReferencedTest extends GcTestCase {
@Test
public void nonReferencedObjects_onlyExpiredPruned() throws Exception {
RevBlob a = tr.blob("a");
- gc.setExpire(new Date(lastModified(a) + 1));
+ gc.setExpire(Instant.ofEpochMilli(lastModified(a) + 1));
fsTick();
RevBlob b = tr.blob("b");
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcReflogTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcReflogTest.java
index e6c1ee5fd6..29f180d76b 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcReflogTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcReflogTest.java
@@ -30,7 +30,7 @@ public class GcReflogTest extends GcTestCase {
BranchBuilder bb = tr.branch("refs/heads/master");
bb.commit().add("A", "A").add("B", "B").create();
bb.commit().add("A", "A2").add("B", "B2").create();
- new File(repo.getDirectory(), Constants.LOGS + "/refs/heads/master")
+ new File(repo.getCommonDirectory(), Constants.LOGS + "/refs/heads/master")
.delete();
stats = gc.getStatistics();
assertEquals(8, stats.numberOfLooseObjects);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcReverseIndexTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcReverseIndexTest.java
new file mode 100644
index 0000000000..cbb0943426
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcReverseIndexTest.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2023, Google LLC and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.storage.file;
+
+import static org.eclipse.jgit.internal.storage.pack.PackExt.REVERSE_INDEX;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.util.Collections;
+
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.storage.pack.PackConfig;
+import org.eclipse.jgit.util.IO;
+import org.junit.Test;
+
+public class GcReverseIndexTest extends GcTestCase {
+
+ @Test
+ public void testWriteDefault() throws Exception {
+ PackConfig config = new PackConfig(repo);
+ gc.setPackConfig(config);
+
+ RevCommit tip = commitChain(10);
+ TestRepository.BranchBuilder bb = tr.branch("refs/heads/main");
+ bb.update(tip);
+
+ gc.gc().get();
+ assertRidxDoesNotExist(repo);
+ }
+
+ @Test
+ public void testWriteDisabled() throws Exception {
+ PackConfig config = new PackConfig(repo);
+ config.setWriteReverseIndex(false);
+ gc.setPackConfig(config);
+
+ RevCommit tip = commitChain(10);
+ TestRepository.BranchBuilder bb = tr.branch("refs/heads/main");
+ bb.update(tip);
+
+ gc.gc().get();
+ assertRidxDoesNotExist(repo);
+ }
+
+ @Test
+ public void testWriteEmptyRepo() throws Exception {
+ PackConfig config = new PackConfig(repo);
+ config.setWriteReverseIndex(true);
+ gc.setPackConfig(config);
+
+ gc.gc().get();
+ assertRidxDoesNotExist(repo);
+ }
+
+ @Test
+ public void testWriteShallowRepo() throws Exception {
+ PackConfig config = new PackConfig(repo);
+ config.setWriteReverseIndex(true);
+ gc.setPackConfig(config);
+
+ RevCommit tip = commitChain(2);
+ TestRepository.BranchBuilder bb = tr.branch("refs/heads/main");
+ bb.update(tip);
+ repo.getObjectDatabase().setShallowCommits(Collections.singleton(tip));
+
+ gc.gc().get();
+ assertValidRidxExists(repo);
+ }
+
+ @Test
+ public void testWriteEnabled() throws Exception {
+ PackConfig config = new PackConfig(repo);
+ config.setWriteReverseIndex(true);
+ gc.setPackConfig(config);
+
+ RevCommit tip = commitChain(10);
+ TestRepository.BranchBuilder bb = tr.branch("refs/heads/main");
+ bb.update(tip);
+
+ gc.gc().get();
+ assertValidRidxExists(repo);
+ }
+
+ private static void assertValidRidxExists(FileRepository repo)
+ throws Exception {
+ PackFile packFile = repo.getObjectDatabase().getPacks().iterator()
+ .next().getPackFile();
+ File file = packFile.create(REVERSE_INDEX);
+ assertTrue(file.exists());
+ try (InputStream os = new FileInputStream(file)) {
+ byte[] magic = new byte[4];
+ IO.readFully(os, magic, 0, 4);
+ assertArrayEquals(new byte[] { 'R', 'I', 'D', 'X' }, magic);
+ }
+ }
+
+ private static void assertRidxDoesNotExist(FileRepository repo) {
+ File packDir = repo.getObjectDatabase().getPackDirectory();
+ String[] reverseIndexFilenames = packDir.list(
+ (dir, name) -> name.endsWith(REVERSE_INDEX.getExtension()));
+ assertEquals(0, reverseIndexFilenames.length);
+ }
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcSinceBitmapStatisticsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcSinceBitmapStatisticsTest.java
new file mode 100644
index 0000000000..af52e2cb85
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcSinceBitmapStatisticsTest.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (c) 2024 Jacek Centkowski <geminica.programs@gmail.com> and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.storage.file;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Collection;
+import java.util.stream.StreamSupport;
+
+import org.eclipse.jgit.internal.storage.file.GC.RepoStatistics;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.storage.pack.PackConfig;
+import org.junit.Test;
+
+public class GcSinceBitmapStatisticsTest extends GcTestCase {
+ @Test
+ public void testShouldReportZeroPacksAndObjectsForInitializedRepo()
+ throws IOException {
+ RepoStatistics s = gc.getStatistics();
+ assertEquals(0L, s.numberOfPackFilesSinceBitmap);
+ assertEquals(0L, s.numberOfObjectsSinceBitmap);
+ }
+
+ @Test
+ public void testShouldReportAllPackFilesWhenNoGcWasPerformed()
+ throws Exception {
+ tr.packAndPrune();
+ long result = gc.getStatistics().numberOfPackFilesSinceBitmap;
+
+ assertEquals(repo.getObjectDatabase().getPacks().size(), result);
+ }
+
+ @Test
+ public void testShouldReportAllObjectsWhenNoGcWasPerformed()
+ throws Exception {
+ tr.packAndPrune();
+
+ assertEquals(
+ getNumberOfObjectsInPacks(repo.getObjectDatabase().getPacks()),
+ gc.getStatistics().numberOfObjectsSinceBitmap);
+ }
+
+ @Test
+ public void testShouldReportNoPacksFilesSinceBitmapWhenPackfilesAreOlderThanBitmapFile()
+ throws Exception {
+ addCommit(null);
+ configureGC(/* buildBitmap */ false).gc().get();
+ assertEquals(1L, gc.getStatistics().numberOfPackFiles);
+ assertEquals(0L, repositoryBitmapFiles());
+ assertEquals(1L, gc.getStatistics().numberOfPackFilesSinceBitmap);
+
+ addCommit(null);
+ configureGC(/* buildBitmap */ true).gc().get();
+
+ assertEquals(1L, repositoryBitmapFiles());
+ assertEquals(2L, gc.getStatistics().numberOfPackFiles);
+ assertEquals(0L, gc.getStatistics().numberOfPackFilesSinceBitmap);
+ }
+
+ @Test
+ public void testShouldReportNoObjectsDirectlyAfterGc() throws Exception {
+ // given
+ addCommit(null);
+ assertEquals(2L, gc.getStatistics().numberOfObjectsSinceBitmap);
+
+ gc.gc().get();
+ assertEquals(0L, gc.getStatistics().numberOfObjectsSinceBitmap);
+ }
+
+ @Test
+ public void testShouldReportNewPacksSinceGcWhenRepositoryProgresses()
+ throws Exception {
+ // commit & gc
+ RevCommit parent = addCommit(null);
+ gc.gc().get();
+ assertEquals(1L, repositoryBitmapFiles());
+
+ // progress & pack
+ addCommit(parent);
+ assertEquals(1L, gc.getStatistics().numberOfPackFiles);
+ assertEquals(0L, gc.getStatistics().numberOfPackFilesSinceBitmap);
+
+ tr.packAndPrune();
+ assertEquals(2L, gc.getStatistics().numberOfPackFiles);
+ assertEquals(1L, gc.getStatistics().numberOfPackFilesSinceBitmap);
+ }
+
+ @Test
+ public void testShouldReportNewObjectsSinceGcWhenRepositoryProgresses()
+ throws Exception {
+ // commit & gc
+ RevCommit parent = addCommit(null);
+ gc.gc().get();
+ assertEquals(0L, gc.getStatistics().numberOfLooseObjects);
+ assertEquals(0L, gc.getStatistics().numberOfObjectsSinceBitmap);
+
+ // progress & pack
+ addCommit(parent);
+ assertEquals(1L, gc.getStatistics().numberOfLooseObjects);
+ assertEquals(1L, gc.getStatistics().numberOfObjectsSinceBitmap);
+
+ tr.packAndPrune();
+ assertEquals(0L, gc.getStatistics().numberOfLooseObjects);
+ // Number of objects contained in the newly created PackFile
+ assertEquals(3L, gc.getStatistics().numberOfObjectsSinceBitmap);
+ }
+
+ @Test
+ public void testShouldReportNewPacksFromTheLatestBitmapWhenRepositoryProgresses()
+ throws Exception {
+ // commit & gc
+ RevCommit parent = addCommit(null);
+ gc.gc().get();
+ assertEquals(1L, repositoryBitmapFiles());
+
+ // progress & gc
+ parent = addCommit(parent);
+ gc.gc().get();
+ assertEquals(2L, repositoryBitmapFiles());
+
+ // progress & pack
+ addCommit(parent);
+ tr.packAndPrune();
+
+ assertEquals(1L, gc.getStatistics().numberOfPackFilesSinceBitmap);
+ }
+
+ @Test
+ public void testShouldReportNewObjectsFromTheLatestBitmapWhenRepositoryProgresses()
+ throws Exception {
+ // commit & gc
+ RevCommit parent = addCommit(null);
+ gc.gc().get();
+
+ // progress & gc
+ parent = addCommit(parent);
+ gc.gc().get();
+ assertEquals(0L, gc.getStatistics().numberOfObjectsSinceBitmap);
+
+ // progress & pack
+ addCommit(parent);
+ assertEquals(1L, gc.getStatistics().numberOfObjectsSinceBitmap);
+
+ tr.packAndPrune();
+ assertEquals(4L, gc.getStatistics().numberOfObjectsSinceBitmap);
+ }
+
+ private RevCommit addCommit(RevCommit parent) throws Exception {
+ return tr.branch("master").commit()
+ .author(new PersonIdent("repo-metrics", "repo@metrics.com"))
+ .parent(parent).create();
+ }
+
+ private long repositoryBitmapFiles() throws IOException {
+ return StreamSupport
+ .stream(Files
+ .newDirectoryStream(repo.getObjectDatabase()
+ .getPackDirectory().toPath(), "pack-*.bitmap")
+ .spliterator(), false)
+ .count();
+ }
+
+ private long getNumberOfObjectsInPacks(Collection<Pack> packs) {
+ return packs.stream().mapToLong(pack -> {
+ try {
+ return pack.getObjectCount();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }).sum();
+ }
+
+ private GC configureGC(boolean buildBitmap) {
+ PackConfig pc = new PackConfig(repo.getObjectDatabase().getConfig());
+ pc.setBuildBitmaps(buildBitmap);
+ gc.setPackConfig(pc);
+ return gc;
+ }
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcTestCase.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcTestCase.java
index 48f6e06385..8b27b829b2 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcTestCase.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcTestCase.java
@@ -61,6 +61,7 @@ public abstract class GcTestCase extends LocalDiskRepositoryTestCase {
* the depth of the commit chain.
* @return the commit that is the tip of the commit chain
* @throws Exception
+ * if an error occurred
*/
protected RevCommit commitChain(int depth) throws Exception {
if (depth <= 0)
@@ -93,6 +94,7 @@ public abstract class GcTestCase extends LocalDiskRepositoryTestCase {
* number of files added per commit
* @return the commit that is the tip of the commit chain
* @throws Exception
+ * if an error occurred
*/
protected RevCommit commitChain(int depth, int width) throws Exception {
if (depth <= 0) {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/LockFileTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/LockFileTest.java
index 7eab1dcb09..953d624bfe 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/LockFileTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/LockFileTest.java
@@ -209,7 +209,8 @@ public class LockFileTest extends RepositoryTestCase {
lock.unlock();
lock.unlock();
} catch (Throwable e) {
- fail("unlock should be noop if not locked at all.");
+ throw new AssertionError(
+ "unlock should be noop if not locked at all.", e);
}
}
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ObjectDirectoryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ObjectDirectoryTest.java
index 746a0a1ff3..33cbc868ca 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ObjectDirectoryTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ObjectDirectoryTest.java
@@ -49,7 +49,10 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import java.io.File;
@@ -66,6 +69,7 @@ import java.util.concurrent.Future;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
@@ -207,33 +211,35 @@ public class ObjectDirectoryTest extends RepositoryTestCase {
.fromString("873fb8d667d05436d728c52b1d7a09528e6eb59b");
WindowCursor curs = new WindowCursor(db.getObjectDatabase());
- LooseObjects mock = mock(LooseObjects.class);
+ Config config = new Config();
+ config.setString("core", null, "trustLooseObjectStat", "ALWAYS");
+ LooseObjects spy = Mockito.spy(new LooseObjects(config, trash));
UnpackedObjectCache unpackedObjectCacheMock = mock(
UnpackedObjectCache.class);
- Mockito.when(mock.getObjectLoader(any(), any(), any()))
- .thenThrow(new IOException("Stale File Handle"));
- Mockito.when(mock.open(curs, id)).thenCallRealMethod();
- Mockito.when(mock.unpackedObjectCache())
- .thenReturn(unpackedObjectCacheMock);
+ doThrow(new IOException("Stale File Handle")).when(spy)
+ .getObjectLoader(any(), any(), any());
+ doReturn(unpackedObjectCacheMock).when(spy).unpackedObjectCache();
- assertNull(mock.open(curs, id));
+ assertNull(spy.open(curs, id));
verify(unpackedObjectCacheMock).remove(id);
}
- @Test
+ @Test(expected = IOException.class)
public void testOpenLooseObjectPropagatesIOExceptions() throws Exception {
ObjectId id = ObjectId
.fromString("873fb8d667d05436d728c52b1d7a09528e6eb59b");
WindowCursor curs = new WindowCursor(db.getObjectDatabase());
- LooseObjects mock = mock(LooseObjects.class);
+ Config config = new Config();
+ config.setString("core", null, "trustLooseObjectStat", "NEVER");
+ LooseObjects spy = spy(new LooseObjects(config,
+ db.getObjectDatabase().getDirectory()));
- Mockito.when(mock.getObjectLoader(any(), any(), any()))
- .thenThrow(new IOException("some IO failure"));
- Mockito.when(mock.open(curs, id)).thenCallRealMethod();
+ doThrow(new IOException("some IO failure")).when(spy)
+ .getObjectLoader(any(), any(), any());
- assertThrows(IOException.class, () -> mock.open(curs, id));
+ spy.open(curs, id);
}
@Test
@@ -243,17 +249,18 @@ public class ObjectDirectoryTest extends RepositoryTestCase {
db.getConfig().setBoolean(ConfigConstants.CONFIG_GC_SECTION, null,
ConfigConstants.CONFIG_KEY_WRITE_COMMIT_GRAPH, true);
- WindowCursor curs = new WindowCursor(db.getObjectDatabase());
- assertTrue(curs.getCommitGraph().isEmpty());
- commitFile("file.txt", "content", "master");
- GC gc = new GC(db);
- gc.gc().get();
- assertTrue(curs.getCommitGraph().isPresent());
+ try (WindowCursor curs = new WindowCursor(db.getObjectDatabase())) {
+ assertTrue(curs.getCommitGraph().isEmpty());
+ commitFile("file.txt", "content", "master");
+ GC gc = new GC(db);
+ gc.gc().get();
+ assertTrue(curs.getCommitGraph().isPresent());
- db.getConfig().setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
- ConfigConstants.CONFIG_COMMIT_GRAPH, false);
+ db.getConfig().setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
+ ConfigConstants.CONFIG_COMMIT_GRAPH, false);
- assertTrue(curs.getCommitGraph().isEmpty());
+ assertTrue(curs.getCommitGraph().isEmpty());
+ }
}
@Test
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackIndexTestCase.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackIndexTestCase.java
index 67bba18e2b..1f934acced 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackIndexTestCase.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackIndexTestCase.java
@@ -13,6 +13,7 @@ package org.eclipse.jgit.internal.storage.file;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.File;
@@ -25,6 +26,7 @@ import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.internal.storage.file.PackIndex.MutableEntry;
import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.lib.MutableObjectId;
import org.eclipse.jgit.lib.ObjectId;
import org.junit.Test;
@@ -66,7 +68,9 @@ public abstract class PackIndexTestCase extends RepositoryTestCase {
* Verify CRC32 support.
*
* @throws MissingObjectException
+ * object is missing in the underlying index
* @throws UnsupportedOperationException
+ * the index doesn't have CRC
*/
public abstract void testCRC32() throws MissingObjectException,
UnsupportedOperationException;
@@ -97,6 +101,39 @@ public abstract class PackIndexTestCase extends RepositoryTestCase {
}
}
+ @Test
+ public void testIteratorMutableEntryCompareTo() {
+ Iterator<PackIndex.MutableEntry> iterA = smallIdx.iterator();
+ Iterator<PackIndex.MutableEntry> iterB = smallIdx.iterator();
+
+ MutableEntry aEntry = iterA.next();
+ iterB.next();
+ MutableEntry bEntry = iterB.next();
+ // b is one ahead
+ assertTrue(aEntry.compareBySha1To(bEntry) < 0);
+ assertTrue(bEntry.compareBySha1To(aEntry) > 0);
+
+ // advance a, now should be equal
+ assertEquals(0, iterA.next().compareBySha1To(bEntry));
+ }
+
+ @Test
+ public void testIteratorMutableEntryCopyTo() {
+ Iterator<PackIndex.MutableEntry> it = smallIdx.iterator();
+
+ MutableObjectId firstOidCopy = new MutableObjectId();
+ MutableEntry next = it.next();
+ next.copyOidTo(firstOidCopy);
+ ObjectId firstImmutable = next.toObjectId();
+
+ MutableEntry second = it.next();
+
+ // The copy has the right value after "next"
+ assertTrue(firstImmutable.equals(firstOidCopy));
+ assertFalse("iterator has moved",
+ second.toObjectId().equals(firstImmutable));
+ }
+
/**
* Test results of iterator comparing to content of well-known (prepared)
* small index.
@@ -104,22 +141,22 @@ public abstract class PackIndexTestCase extends RepositoryTestCase {
@Test
public void testIteratorReturnedValues1() {
Iterator<PackIndex.MutableEntry> iter = smallIdx.iterator();
- assertEquals("4b825dc642cb6eb9a060e54bf8d69288fbee4904", iter.next()
- .name());
- assertEquals("540a36d136cf413e4b064c2b0e0a4db60f77feab", iter.next()
- .name());
- assertEquals("5b6e7c66c276e7610d4a73c70ec1a1f7c1003259", iter.next()
- .name());
- assertEquals("6ff87c4664981e4397625791c8ea3bbb5f2279a3", iter.next()
- .name());
- assertEquals("82c6b885ff600be425b4ea96dee75dca255b69e7", iter.next()
- .name());
- assertEquals("902d5476fa249b7abc9d84c611577a81381f0327", iter.next()
- .name());
- assertEquals("aabf2ffaec9b497f0950352b3e582d73035c2035", iter.next()
- .name());
- assertEquals("c59759f143fb1fe21c197981df75a7ee00290799", iter.next()
- .name());
+ assertEquals("4b825dc642cb6eb9a060e54bf8d69288fbee4904",
+ iter.next().name());
+ assertEquals("540a36d136cf413e4b064c2b0e0a4db60f77feab",
+ iter.next().name());
+ assertEquals("5b6e7c66c276e7610d4a73c70ec1a1f7c1003259",
+ iter.next().name());
+ assertEquals("6ff87c4664981e4397625791c8ea3bbb5f2279a3",
+ iter.next().name());
+ assertEquals("82c6b885ff600be425b4ea96dee75dca255b69e7",
+ iter.next().name());
+ assertEquals("902d5476fa249b7abc9d84c611577a81381f0327",
+ iter.next().name());
+ assertEquals("aabf2ffaec9b497f0950352b3e582d73035c2035",
+ iter.next().name());
+ assertEquals("c59759f143fb1fe21c197981df75a7ee00290799",
+ iter.next().name());
assertFalse(iter.hasNext());
}
@@ -196,16 +233,16 @@ public abstract class PackIndexTestCase extends RepositoryTestCase {
@Test
public void testIteratorReturnedValues2() {
Iterator<PackIndex.MutableEntry> iter = denseIdx.iterator();
- while (!iter.next().name().equals(
- "0a3d7772488b6b106fb62813c4d6d627918d9181")) {
+ while (!iter.next().name()
+ .equals("0a3d7772488b6b106fb62813c4d6d627918d9181")) {
// just iterating
}
- assertEquals("1004d0d7ac26fbf63050a234c9b88a46075719d3", iter.next()
- .name()); // same level-1
- assertEquals("10da5895682013006950e7da534b705252b03be6", iter.next()
- .name()); // same level-1
- assertEquals("1203b03dc816ccbb67773f28b3c19318654b0bc8", iter.next()
- .name());
+ assertEquals("1004d0d7ac26fbf63050a234c9b88a46075719d3",
+ iter.next().name()); // same level-1
+ assertEquals("10da5895682013006950e7da534b705252b03be6",
+ iter.next().name()); // same level-1
+ assertEquals("1203b03dc816ccbb67773f28b3c19318654b0bc8",
+ iter.next().name());
}
@Test
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackInserterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackInserterTest.java
index 85043034aa..cc43d3c2bb 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackInserterTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackInserterTest.java
@@ -53,6 +53,7 @@ import static org.hamcrest.Matchers.lessThan;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.ByteArrayInputStream;
@@ -77,12 +78,14 @@ import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.junit.RepositoryTestCase;
import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.ObjectStream;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.eclipse.jgit.storage.file.WindowCacheConfig;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.util.IO;
@@ -489,6 +492,38 @@ public class PackInserterTest extends RepositoryTestCase {
}
}
+ @Test
+ public void createsObjectSizeIndex() throws Exception {
+ FileBasedConfig jGitConfig = mockSystemReader.getJGitConfig();
+ jGitConfig.setInt(
+ ConfigConstants.CONFIG_PACK_SECTION,
+ null,
+ ConfigConstants.CONFIG_KEY_MIN_BYTES_OBJ_SIZE_INDEX, 10);
+ jGitConfig.save();
+ byte[] oneBlob = Constants.encode("a blob with some content");
+ byte[] anotherBlob = Constants.encode("some more contents");
+ byte[] streamMeBlob = Constants.encode("some more content to write");
+
+ ObjectId oneBlobOid, anotherBlobOid, streamMeBlobOid;
+ try (PackInserter ins = newInserter()) {
+ oneBlobOid = ins.insert(OBJ_BLOB, oneBlob);
+ anotherBlobOid = ins.insert(OBJ_BLOB, anotherBlob);
+ streamMeBlobOid = ins.insert(OBJ_BLOB, streamMeBlob.length,
+ new ByteArrayInputStream(streamMeBlob));
+ ins.flush();
+ }
+
+ List<Pack> listPacks = listPacks(db);
+ assertEquals(1, listPacks.size());
+ Pack thePack = listPacks.get(0);
+ assertTrue(thePack.hasObjectSizeIndex());
+ assertEquals(oneBlob.length, thePack.getIndexedObjectSize(oneBlobOid));
+ assertEquals(anotherBlob.length,
+ thePack.getIndexedObjectSize(anotherBlobOid));
+ assertEquals(streamMeBlob.length,
+ thePack.getIndexedObjectSize(streamMeBlobOid));
+ }
+
private List<Pack> listPacks() throws Exception {
List<Pack> fromOpenDb = listPacks(db);
List<Pack> reopened;
@@ -549,7 +584,8 @@ public class PackInserterTest extends RepositoryTestCase {
}
private void assertPacksOnly() throws Exception {
- new BadFileCollector(f -> !f.endsWith(".pack") && !f.endsWith(".idx"))
+ new BadFileCollector(f -> !f.endsWith(".pack") && !f.endsWith(".idx")
+ && !f.endsWith(".objsize"))
.assertNoBadFiles(db.getObjectDatabase().getDirectory());
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackReverseIndexComputedTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackReverseIndexComputedTest.java
new file mode 100644
index 0000000000..ea5aaf5dd4
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackReverseIndexComputedTest.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2008, Imran M Yousuf <imyousuf@smartitengineering.com>
+ * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.storage.file;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import org.eclipse.jgit.errors.CorruptObjectException;
+import org.eclipse.jgit.errors.PackMismatchException;
+import org.eclipse.jgit.internal.storage.file.PackIndex.MutableEntry;
+import org.eclipse.jgit.junit.JGitTestUtil;
+import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.junit.Before;
+import org.junit.Test;
+
+public class PackReverseIndexComputedTest extends RepositoryTestCase {
+
+ private PackIndex idx;
+
+ private PackReverseIndex reverseIdx;
+
+ /**
+ * Set up tested class instance, test constructor by the way.
+ */
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ // index with both small (< 2^31) and big offsets
+ idx = PackIndex.open(JGitTestUtil.getTestResourceFile("pack-huge.idx"));
+ reverseIdx = PackReverseIndexFactory.computeFromIndex(idx);
+ }
+
+ /**
+ * Test findObject() for all index entries.
+ */
+ @Test
+ public void testFindObject() {
+ for (MutableEntry me : idx)
+ assertEquals(me.toObjectId(), reverseIdx.findObject(me.getOffset()));
+ }
+
+ /**
+ * Test findObject() with illegal argument.
+ */
+ @Test
+ public void testFindObjectWrongOffset() {
+ assertNull(reverseIdx.findObject(0));
+ }
+
+ /**
+ * Test findNextOffset() for all index entries.
+ *
+ * @throws CorruptObjectException
+ */
+ @Test
+ public void testFindNextOffset() throws CorruptObjectException {
+ long offset = findFirstOffset();
+ assertTrue(offset > 0);
+ for (int i = 0; i < idx.getObjectCount(); i++) {
+ long newOffset = reverseIdx.findNextOffset(offset, Long.MAX_VALUE);
+ assertTrue(newOffset > offset);
+ if (i == idx.getObjectCount() - 1)
+ assertEquals(newOffset, Long.MAX_VALUE);
+ else
+ assertEquals(newOffset, idx.findOffset(reverseIdx
+ .findObject(newOffset)));
+ offset = newOffset;
+ }
+ }
+
+ /**
+ * Test findNextOffset() with wrong illegal argument as offset.
+ */
+ @Test
+ public void testFindNextOffsetWrongOffset() {
+ try {
+ reverseIdx.findNextOffset(0, Long.MAX_VALUE);
+ fail("findNextOffset() should throw exception");
+ } catch (CorruptObjectException x) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testVerifyChecksum() throws PackMismatchException {
+ // ComputedReverseIndex doesn't have a file containing a checksum.
+ reverseIdx.verifyPackChecksum(null);
+ }
+
+ private long findFirstOffset() {
+ long min = Long.MAX_VALUE;
+ for (MutableEntry me : idx)
+ min = Math.min(min, me.getOffset());
+ return min;
+ }
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackReverseIndexTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackReverseIndexTest.java
index 292e3e758a..f8fb4c15e7 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackReverseIndexTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackReverseIndexTest.java
@@ -1,6 +1,5 @@
/*
- * Copyright (C) 2008, Imran M Yousuf <imyousuf@smartitengineering.com>
- * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> and others
+ * Copyright (C) 2022, Google LLC and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -8,95 +7,94 @@
*
* SPDX-License-Identifier: BSD-3-Clause
*/
-
package org.eclipse.jgit.internal.storage.file;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-import org.eclipse.jgit.errors.CorruptObjectException;
-import org.eclipse.jgit.internal.storage.file.PackIndex.MutableEntry;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+
+import org.eclipse.jgit.internal.storage.pack.PackExt;
import org.eclipse.jgit.junit.JGitTestUtil;
-import org.eclipse.jgit.junit.RepositoryTestCase;
-import org.junit.Before;
import org.junit.Test;
-public class PackReverseIndexTest extends RepositoryTestCase {
+public class PackReverseIndexTest {
- private PackIndex idx;
-
- private PackReverseIndex reverseIdx;
+ @Test
+ public void open_fallbackToComputed() throws IOException {
+ String noRevFilePrefix = "pack-3280af9c07ee18a87705ef50b0cc4cd20266cf12.";
+ PackReverseIndex computed = PackReverseIndexFactory.openOrCompute(
+ getResourceFileFor(noRevFilePrefix, PackExt.REVERSE_INDEX), 7,
+ () -> PackIndex.open(
+ getResourceFileFor(noRevFilePrefix, PackExt.INDEX)));
- /**
- * Set up tested class instance, test constructor by the way.
- */
- @Override
- @Before
- public void setUp() throws Exception {
- super.setUp();
- // index with both small (< 2^31) and big offsets
- idx = PackIndex.open(JGitTestUtil.getTestResourceFile(
- "pack-huge.idx"));
- reverseIdx = new PackReverseIndex(idx);
+ assertTrue(computed instanceof PackReverseIndexComputed);
}
- /**
- * Test findObject() for all index entries.
- */
@Test
- public void testFindObject() {
- for (MutableEntry me : idx)
- assertEquals(me.toObjectId(), reverseIdx.findObject(me.getOffset()));
+ public void open_readGoodFile() throws IOException {
+ String hasRevFilePrefix = "pack-cbdeda40019ae0e6e789088ea0f51f164f489d14.";
+ PackReverseIndex version1 = PackReverseIndexFactory.openOrCompute(
+ getResourceFileFor(hasRevFilePrefix, PackExt.REVERSE_INDEX), 6,
+ () -> PackIndex.open(
+ getResourceFileFor(hasRevFilePrefix, PackExt.INDEX)));
+
+ assertTrue(version1 instanceof PackReverseIndexV1);
}
- /**
- * Test findObject() with illegal argument.
- */
@Test
- public void testFindObjectWrongOffset() {
- assertNull(reverseIdx.findObject(0));
+ public void open_readCorruptFile() {
+ String hasRevFilePrefix = "pack-cbdeda40019ae0e6e789088ea0f51f164f489d14.";
+
+ assertThrows(IOException.class,
+ () -> PackReverseIndexFactory.openOrCompute(
+ getResourceFileFor(hasRevFilePrefix + "corrupt.",
+ PackExt.REVERSE_INDEX),
+ 6, () -> PackIndex.open(getResourceFileFor(
+ hasRevFilePrefix, PackExt.INDEX))));
}
- /**
- * Test findNextOffset() for all index entries.
- *
- * @throws CorruptObjectException
- */
@Test
- public void testFindNextOffset() throws CorruptObjectException {
- long offset = findFirstOffset();
- assertTrue(offset > 0);
- for (int i = 0; i < idx.getObjectCount(); i++) {
- long newOffset = reverseIdx.findNextOffset(offset, Long.MAX_VALUE);
- assertTrue(newOffset > offset);
- if (i == idx.getObjectCount() - 1)
- assertEquals(newOffset, Long.MAX_VALUE);
- else
- assertEquals(newOffset, idx.findOffset(reverseIdx
- .findObject(newOffset)));
- offset = newOffset;
- }
+ public void read_badMagic() {
+ byte[] badMagic = new byte[] { 'R', 'B', 'A', 'D', // magic
+ 0x00, 0x00, 0x00, 0x01, // file version
+ 0x00, 0x00, 0x00, 0x01, // oid version
+ // pack checksum
+ 'P', 'A', 'C', 'K', 'C', 'H', 'E', 'C', 'K', 'S', 'U', 'M', '3',
+ '4', '5', '6', '7', '8', '9', '0',
+ // checksum
+ 0x66, 0x01, (byte) 0xbc, (byte) 0xe8, 0x51, 0x4b, 0x2f,
+ (byte) 0xa1, (byte) 0xa9, (byte) 0xcd, (byte) 0xbe, (byte) 0xd6,
+ 0x4f, (byte) 0xa8, 0x7d, (byte) 0xab, 0x50, (byte) 0xa3,
+ (byte) 0xf7, (byte) 0xcc, };
+ ByteArrayInputStream in = new ByteArrayInputStream(badMagic);
+
+ assertThrows(IOException.class,
+ () -> PackReverseIndexFactory.readFromFile(in, 0, () -> null));
}
- /**
- * Test findNextOffset() with wrong illegal argument as offset.
- */
@Test
- public void testFindNextOffsetWrongOffset() {
- try {
- reverseIdx.findNextOffset(0, Long.MAX_VALUE);
- fail("findNextOffset() should throw exception");
- } catch (CorruptObjectException x) {
- // expected
- }
+ public void read_unsupportedVersion2() {
+ byte[] version2 = new byte[] { 'R', 'I', 'D', 'X', // magic
+ 0x00, 0x00, 0x00, 0x02, // file version
+ 0x00, 0x00, 0x00, 0x01, // oid version
+ // pack checksum
+ 'P', 'A', 'C', 'K', 'C', 'H', 'E', 'C', 'K', 'S', 'U', 'M', '3',
+ '4', '5', '6', '7', '8', '9', '0',
+ // checksum
+ 0x70, 0x17, 0x10, 0x51, (byte) 0xfe, (byte) 0xab, (byte) 0x9b,
+ 0x68, (byte) 0xed, 0x3a, 0x3f, 0x27, 0x1d, (byte) 0xce,
+ (byte) 0xff, 0x38, 0x09, (byte) 0x9b, 0x29, 0x58, };
+ ByteArrayInputStream in = new ByteArrayInputStream(version2);
+
+ assertThrows(IOException.class,
+ () -> PackReverseIndexFactory.readFromFile(in, 0, () -> null));
}
- private long findFirstOffset() {
- long min = Long.MAX_VALUE;
- for (MutableEntry me : idx)
- min = Math.min(min, me.getOffset());
- return min;
+ private File getResourceFileFor(String packFilePrefix, PackExt ext) {
+ return JGitTestUtil
+ .getTestResourceFile(packFilePrefix + ext.getExtension());
}
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackReverseIndexV1Test.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackReverseIndexV1Test.java
new file mode 100644
index 0000000000..38b28b501b
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackReverseIndexV1Test.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright (C) 2022, Google LLC and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.internal.storage.file;
+
+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_TREE;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+
+import org.eclipse.jgit.errors.CorruptObjectException;
+import org.eclipse.jgit.errors.PackMismatchException;
+import org.eclipse.jgit.junit.JGitTestUtil;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.transport.PackedObjectInfo;
+import org.junit.Before;
+import org.junit.Test;
+
+public class PackReverseIndexV1Test {
+ private static final byte[] FAKE_PACK_CHECKSUM = new byte[] { 'P', 'A', 'C',
+ 'K', 'C', 'H', 'E', 'C', 'K', 'S', 'U', 'M', '3', '4', '5', '6',
+ '7', '8', '9', '0', };
+
+ private static final byte[] NO_OBJECTS = new byte[] { 'R', 'I', 'D', 'X', // magic
+ 0x00, 0x00, 0x00, 0x01, // file version
+ 0x00, 0x00, 0x00, 0x01, // oid version
+ // pack checksum to copy into at byte 12
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ // checksum
+ (byte) 0xd1, 0x1d, 0x17, (byte) 0xd5, (byte) 0xa1, 0x5c,
+ (byte) 0x8f, 0x45, 0x7e, 0x06, (byte) 0x91, (byte) 0xf2, 0x7e, 0x20,
+ 0x35, 0x2c, (byte) 0xdc, 0x4c, 0x46, (byte) 0xe4, };
+
+ private static final byte[] SMALL_PACK_CHECKSUM = new byte[] { (byte) 0xbb,
+ 0x1d, 0x25, 0x3d, (byte) 0xd3, (byte) 0xf0, 0x08, 0x75, (byte) 0xc8,
+ 0x04, (byte) 0xd0, 0x6f, 0x73, (byte) 0xe9, 0x00, (byte) 0x82,
+ (byte) 0xdb, 0x09, (byte) 0xc8, 0x13, };
+
+ private static final byte[] SMALL_CONTENTS = new byte[] { 'R', 'I', 'D',
+ 'X', // magic
+ 0x00, 0x00, 0x00, 0x01, // file version
+ 0x00, 0x00, 0x00, 0x01, // oid version
+ 0x00, 0x00, 0x00, 0x04, // offset 12: "68" -> index @ 4
+ 0x00, 0x00, 0x00, 0x02, // offset 165: "5c" -> index @ 2
+ 0x00, 0x00, 0x00, 0x03, // offset 257: "62" -> index @ 3
+ 0x00, 0x00, 0x00, 0x01, // offset 450: "58" -> index @ 1
+ 0x00, 0x00, 0x00, 0x05, // offset 556: "c5" -> index @ 5
+ 0x00, 0x00, 0x00, 0x00, // offset 614: "2d" -> index @ 0
+ // pack checksum to copy into at byte 36
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ // checksum
+ (byte) 0xf0, 0x6d, 0x03, (byte) 0xd7, 0x6f, (byte) 0x9f,
+ (byte) 0xc1, 0x36, 0x26, (byte) 0xbc, (byte) 0xcb, 0x75, 0x36,
+ (byte) 0xa1, 0x26, 0x6a, 0x2b, (byte) 0x84, 0x16, (byte) 0x83, };
+
+ private PackReverseIndex emptyReverseIndex;
+
+ /**
+ * Reverse index for the pack-cbdeda40019ae0e6e789088ea0f51f164f489d14.idx
+ * with contents `SHA-1 type size size-in-packfile offset-in-packfile` as
+ * shown by `verify-pack`:
+ * 2d04ee74dba30078c2dcdb713ddb8be4bc084d76 blob 8 17 614
+ * 58728c938a9a8b9970cc09236caf94ada4689923 blob 140 106 450
+ * 5ce00008cf3fb8f194f52742020bd40d78f3f1b3 commit 81 92 165 1 68cb1f232964f3cd698afc1dafe583937203c587
+ * 62299a7ae290d685196e948a2fcb7d8c07f95c7d tree 198 193 257
+ * 68cb1f232964f3cd698afc1dafe583937203c587 commit 220 153 12
+ * c5ab27309491cf641eb11bb4b7a78641f280b482 tree 46 58 556 1 62299a7ae290d685196e948a2fcb7d8c07f95c7d
+ */
+ private PackReverseIndex smallReverseIndex;
+
+ private final PackedObjectInfo object614 = objectInfo(
+ "2d04ee74dba30078c2dcdb713ddb8be4bc084d76", OBJ_BLOB, 614);
+
+ private final PackedObjectInfo object450 = objectInfo(
+ "58728c938a9a8b9970cc09236caf94ada4689923", OBJ_BLOB, 450);
+
+ private final PackedObjectInfo object165 = objectInfo(
+ "5ce00008cf3fb8f194f52742020bd40d78f3f1b3", OBJ_COMMIT, 165);
+
+ private final PackedObjectInfo object257 = objectInfo(
+ "62299a7ae290d685196e948a2fcb7d8c07f95c7d", OBJ_TREE, 257);
+
+ private final PackedObjectInfo object12 = objectInfo(
+ "68cb1f232964f3cd698afc1dafe583937203c587", OBJ_COMMIT, 12);
+
+ private final PackedObjectInfo object556 = objectInfo(
+ "c5ab27309491cf641eb11bb4b7a78641f280b482", OBJ_TREE, 556);
+
+ // last object's offset + last object's length
+ private final long smallMaxOffset = 631;
+
+ @Before
+ public void setUp() throws Exception {
+ System.arraycopy(SMALL_PACK_CHECKSUM, 0, SMALL_CONTENTS, 36,
+ SMALL_PACK_CHECKSUM.length);
+ ByteArrayInputStream smallIn = new ByteArrayInputStream(SMALL_CONTENTS);
+ smallReverseIndex = PackReverseIndexFactory.readFromFile(smallIn, 6,
+ () -> PackIndex.open(JGitTestUtil.getTestResourceFile(
+ "pack-cbdeda40019ae0e6e789088ea0f51f164f489d14.idx")));
+
+ System.arraycopy(FAKE_PACK_CHECKSUM, 0, NO_OBJECTS, 12,
+ FAKE_PACK_CHECKSUM.length);
+ ByteArrayInputStream emptyIn = new ByteArrayInputStream(NO_OBJECTS);
+ emptyReverseIndex = PackReverseIndexFactory.readFromFile(emptyIn, 0,
+ () -> null);
+ }
+
+ @Test
+ public void read_unsupportedOidSHA256() {
+ byte[] version2 = new byte[] { 'R', 'I', 'D', 'X', // magic
+ 0x00, 0x00, 0x00, 0x01, // file version
+ 0x00, 0x00, 0x00, 0x02, // oid version
+ // pack checksum to copy into at byte 12
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ // checksum
+ 0x6e, 0x78, 0x75, 0x67, (byte) 0x84, (byte) 0x89, (byte) 0xde,
+ (byte) 0xe3, (byte) 0x86, 0x6a, 0x3b, (byte) 0x98, 0x51,
+ (byte) 0xd8, (byte) 0x8c, (byte) 0xec, 0x50, (byte) 0xe7,
+ (byte) 0xfb, 0x22, };
+ System.arraycopy(FAKE_PACK_CHECKSUM, 0, version2, 12,
+ FAKE_PACK_CHECKSUM.length);
+ ByteArrayInputStream in = new ByteArrayInputStream(version2);
+
+ assertThrows(IOException.class,
+ () -> PackReverseIndexFactory.readFromFile(in, 0, () -> null));
+ }
+
+ @Test
+ public void read_objectCountTooLarge() {
+ ByteArrayInputStream dummyInput = new ByteArrayInputStream(NO_OBJECTS);
+ long biggerThanInt = ((long) Integer.MAX_VALUE) + 1;
+
+ assertThrows(IllegalArgumentException.class,
+ () -> PackReverseIndexFactory.readFromFile(dummyInput,
+ biggerThanInt,
+ () -> null));
+ }
+
+ @Test
+ public void read_incorrectChecksum() {
+ byte[] badChecksum = new byte[] { 'R', 'I', 'D', 'X', // magic
+ 0x00, 0x00, 0x00, 0x01, // file version
+ 0x00, 0x00, 0x00, 0x01, // oid version
+ // pack checksum to copy into at byte 12
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ // checksum
+ (byte) 0xf2, 0x1a, 0x1a, (byte) 0xaa, 0x32, 0x2d, (byte) 0xb9,
+ (byte) 0xfd, 0x0f, (byte) 0xa5, 0x4c, (byte) 0xea, (byte) 0xcf,
+ (byte) 0xbb, (byte) 0x99, (byte) 0xde, (byte) 0xd3, 0x4e,
+ (byte) 0xb1, (byte) 0xee, // would be 0x74 if correct
+ };
+ System.arraycopy(FAKE_PACK_CHECKSUM, 0, badChecksum, 12,
+ FAKE_PACK_CHECKSUM.length);
+ ByteArrayInputStream in = new ByteArrayInputStream(badChecksum);
+ assertThrows(CorruptObjectException.class,
+ () -> PackReverseIndexFactory.readFromFile(in, 0, () -> null));
+ }
+
+ @Test
+ public void findObject_noObjects() {
+ assertNull(emptyReverseIndex.findObject(0));
+ }
+
+ @Test
+ public void findObject_multipleObjects() {
+ assertEquals(object614, smallReverseIndex.findObject(614));
+ assertEquals(object450, smallReverseIndex.findObject(450));
+ assertEquals(object165, smallReverseIndex.findObject(165));
+ assertEquals(object257, smallReverseIndex.findObject(257));
+ assertEquals(object12, smallReverseIndex.findObject(12));
+ assertEquals(object556, smallReverseIndex.findObject(556));
+ }
+
+ @Test
+ public void findObject_badOffset() {
+ assertNull(smallReverseIndex.findObject(0));
+ }
+
+ @Test
+ public void findNextOffset_noObjects() {
+ assertThrows(IOException.class,
+ () -> emptyReverseIndex.findNextOffset(0, Long.MAX_VALUE));
+ }
+
+ @Test
+ public void findNextOffset_multipleObjects() throws CorruptObjectException {
+ assertEquals(smallMaxOffset,
+ smallReverseIndex.findNextOffset(614, smallMaxOffset));
+ assertEquals(614,
+ smallReverseIndex.findNextOffset(556, smallMaxOffset));
+ assertEquals(556,
+ smallReverseIndex.findNextOffset(450, smallMaxOffset));
+ assertEquals(450,
+ smallReverseIndex.findNextOffset(257, smallMaxOffset));
+ assertEquals(257,
+ smallReverseIndex.findNextOffset(165, smallMaxOffset));
+ assertEquals(165, smallReverseIndex.findNextOffset(12, smallMaxOffset));
+ }
+
+ @Test
+ public void findNextOffset_badOffset() {
+ assertThrows(IOException.class,
+ () -> smallReverseIndex.findNextOffset(0, Long.MAX_VALUE));
+ }
+
+ @Test
+ public void findPosition_noObjects() {
+ assertEquals(-1, emptyReverseIndex.findPosition(0));
+ }
+
+ @Test
+ public void findPosition_multipleObjects() {
+ assertEquals(0, smallReverseIndex.findPosition(12));
+ assertEquals(1, smallReverseIndex.findPosition(165));
+ assertEquals(2, smallReverseIndex.findPosition(257));
+ assertEquals(3, smallReverseIndex.findPosition(450));
+ assertEquals(4, smallReverseIndex.findPosition(556));
+ assertEquals(5, smallReverseIndex.findPosition(614));
+ }
+
+ @Test
+ public void findPosition_badOffset() {
+ assertEquals(-1, smallReverseIndex.findPosition(10));
+ }
+
+ @Test
+ public void findObjectByPosition_noObjects() {
+ assertThrows(AssertionError.class,
+ () -> emptyReverseIndex.findObjectByPosition(0));
+ }
+
+ @Test
+ public void findObjectByPosition_multipleObjects() {
+ assertEquals(object12, smallReverseIndex.findObjectByPosition(0));
+ assertEquals(object165, smallReverseIndex.findObjectByPosition(1));
+ assertEquals(object257, smallReverseIndex.findObjectByPosition(2));
+ assertEquals(object450, smallReverseIndex.findObjectByPosition(3));
+ assertEquals(object556, smallReverseIndex.findObjectByPosition(4));
+ assertEquals(object614, smallReverseIndex.findObjectByPosition(5));
+ }
+
+ @Test
+ public void findObjectByPosition_badOffset() {
+ assertThrows(AssertionError.class,
+ () -> smallReverseIndex.findObjectByPosition(10));
+ }
+
+ @Test
+ public void verifyChecksum_match() throws IOException {
+ smallReverseIndex.verifyPackChecksum("smallPackFilePath");
+ }
+
+ @Test
+ public void verifyChecksum_mismatch() throws IOException {
+ ByteArrayInputStream in = new ByteArrayInputStream(NO_OBJECTS);
+ PackIndex mockForwardIndex = mock(PackIndex.class);
+ when(mockForwardIndex.getChecksum()).thenReturn(
+ new byte[] { 'D', 'I', 'F', 'F', 'P', 'A', 'C', 'K', 'C', 'H',
+ 'E', 'C', 'K', 'S', 'U', 'M', '7', '8', '9', '0', });
+ PackReverseIndex reverseIndex = PackReverseIndexFactory.readFromFile(in,
+ 0,
+ () -> mockForwardIndex);
+
+ assertThrows(PackMismatchException.class,
+ () -> reverseIndex.verifyPackChecksum("packFilePath"));
+ }
+
+ private static PackedObjectInfo objectInfo(String objectId, int type,
+ long offset) {
+ PackedObjectInfo objectInfo = new PackedObjectInfo(
+ ObjectId.fromString(objectId));
+ objectInfo.setType(type);
+ objectInfo.setOffset(offset);
+ return objectInfo;
+ }
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackReverseIndexV1WriteReadTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackReverseIndexV1WriteReadTest.java
new file mode 100644
index 0000000000..372a4c7cba
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackReverseIndexV1WriteReadTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2022, Google LLC and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.internal.storage.file;
+
+import static org.eclipse.jgit.lib.Constants.OBJECT_ID_STRING_LENGTH;
+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_TREE;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.transport.PackedObjectInfo;
+import org.junit.Test;
+
+public class PackReverseIndexV1WriteReadTest {
+
+ private static byte[] PACK_CHECKSUM = new byte[] { 'P', 'A', 'C', 'K', 'C',
+ 'H', 'E', 'C', 'K', 'S', 'U', 'M', '3', '4', '5', '6', '7', '8',
+ '9', '0', };
+
+ @Test
+ public void writeThenRead_noObjects() throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ PackReverseIndexWriter writer = PackReverseIndexWriter.createWriter(out,
+ 1);
+ List<PackedObjectInfo> objectsSortedByName = new ArrayList<>();
+
+ // write
+ writer.write(objectsSortedByName, PACK_CHECKSUM);
+
+ ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
+
+ // read
+ PackReverseIndex noObjectsReverseIndex = PackReverseIndexFactory
+ .readFromFile(in, 0, () -> null);
+
+ // use
+ assertThrows(AssertionError.class,
+ () -> noObjectsReverseIndex.findObjectByPosition(0));
+ }
+
+ @Test
+ public void writeThenRead_oneObject() throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ PackReverseIndexWriter writer = PackReverseIndexWriter.createWriter(out,
+ 1);
+ PackedObjectInfo a = objectInfo("a", OBJ_COMMIT, 0);
+ List<PackedObjectInfo> objectsSortedByName = List.of(a);
+
+ // write
+ writer.write(objectsSortedByName, PACK_CHECKSUM);
+
+ ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
+ PackIndex mockForwardIndex = mock(PackIndex.class);
+ when(mockForwardIndex.getObjectId(0)).thenReturn(a);
+
+ // read
+ PackReverseIndex oneObjectReverseIndex = PackReverseIndexFactory
+ .readFromFile(in, 1, () -> mockForwardIndex);
+
+ // use
+ assertEquals(a, oneObjectReverseIndex.findObjectByPosition(0));
+ }
+
+ @Test
+ public void writeThenRead_multipleObjectsLargeOffsets() throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ PackReverseIndexWriter writer = PackReverseIndexWriter.createWriter(out,
+ 1);
+ PackedObjectInfo a = objectInfo("a", OBJ_BLOB, 200000000);
+ PackedObjectInfo b = objectInfo("b", OBJ_COMMIT, 0);
+ PackedObjectInfo c = objectInfo("c", OBJ_COMMIT, 52000000000L);
+ PackedObjectInfo d = objectInfo("d", OBJ_TREE, 7);
+ PackedObjectInfo e = objectInfo("e", OBJ_COMMIT, 38000000000L);
+ List<PackedObjectInfo> objectsSortedByName = List.of(a, b, c, d, e);
+
+ writer.write(objectsSortedByName, PACK_CHECKSUM);
+
+ // write
+ writer.write(objectsSortedByName, PACK_CHECKSUM);
+
+ ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
+ PackIndex mockForwardIndex = mock(PackIndex.class);
+ when(mockForwardIndex.getObjectId(4)).thenReturn(e);
+
+ // read
+ PackReverseIndex multipleObjectsReverseIndex = PackReverseIndexFactory
+ .readFromFile(in, 5, () -> mockForwardIndex);
+
+ // use with minimal mocked forward index use
+ assertEquals(e, multipleObjectsReverseIndex.findObjectByPosition(3));
+ }
+
+ private static PackedObjectInfo objectInfo(String objectId, int type,
+ long offset) {
+ assert (objectId.length() == 1);
+ PackedObjectInfo objectInfo = new PackedObjectInfo(
+ ObjectId.fromString(objectId.repeat(OBJECT_ID_STRING_LENGTH)));
+ objectInfo.setType(type);
+ objectInfo.setOffset(offset);
+ return objectInfo;
+ }
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackTest.java
index a3596541fe..016a6afd70 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackTest.java
@@ -10,6 +10,7 @@
package org.eclipse.jgit.internal.storage.file;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_PACK_SECTION;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -29,6 +30,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.stream.Collectors;
import java.util.zip.Deflater;
import org.eclipse.jgit.errors.LargeObjectException;
@@ -39,6 +41,7 @@ import org.eclipse.jgit.junit.JGitTestUtil;
import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.junit.TestRng;
+import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.ObjectId;
@@ -47,6 +50,7 @@ import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.ObjectStream;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevBlob;
+import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.storage.file.WindowCacheConfig;
import org.eclipse.jgit.transport.PackParser;
import org.eclipse.jgit.transport.PackedObjectInfo;
@@ -261,7 +265,7 @@ public class PackTest extends LocalDiskRepositoryTestCase {
new PackIndexWriterV1(f).write(list, footer);
}
- Pack pack = new Pack(packName, null);
+ Pack pack = new Pack(repo.getConfig(), packName, null);
try {
pack.get(wc, b);
fail("expected LargeObjectException.ExceedsByteArrayLimit");
@@ -295,6 +299,29 @@ public class PackTest extends LocalDiskRepositoryTestCase {
}
}
+ @Test
+ public void testObjectSize() throws Exception {
+ byte[] data = getRng().nextBytes(300);
+ RevBlob aBlob = tr.blob(data);
+ RevCommit aCommit = tr.branch("master").commit().add("A", aBlob).create();
+ repo.getConfig().setInt(CONFIG_PACK_SECTION, null, ConfigConstants.CONFIG_KEY_MIN_BYTES_OBJ_SIZE_INDEX, 0);
+ tr.packAndPrune();
+
+ List<Pack> packs = repo.getObjectDatabase().getPacks().stream().collect(Collectors.toList());
+ assertEquals(1, packs.size());
+ // Indexed object
+ assertEquals(300, packs.get(0).getIndexedObjectSize(aBlob));
+ assertEquals(300, packs.get(0).getObjectSize(wc, aBlob));
+ // Non indexed object
+ assertEquals(-1, packs.get(0).getIndexedObjectSize(aCommit));
+ assertEquals(168, packs.get(0).getObjectSize(wc, aCommit));
+ // Object not in pack
+ assertEquals(-1, packs.get(0).getObjectSize(wc,
+ ObjectId.fromString("1111111111111111111111111111111111111111")));
+ assertEquals(-1, packs.get(0).getIndexedObjectSize(
+ ObjectId.fromString("1111111111111111111111111111111111111111")));
+ }
+
private static byte[] clone(int first, byte[] base) {
byte[] r = new byte[base.length];
System.arraycopy(base, 1, r, 1, r.length - 1);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryAfterOpenConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryAfterOpenConfigTest.java
index 42304e2253..3ea4a167cb 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryAfterOpenConfigTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryAfterOpenConfigTest.java
@@ -17,7 +17,6 @@ import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.util.SystemReader;
public class RefDirectoryAfterOpenConfigTest extends RefDirectoryTest {
- /** {@inheritDoc} */
@Override
public void refDirectorySetup() throws Exception {
StoredConfig userConfig = SystemReader.getInstance().getUserConfig();
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java
index 619e585a90..baa0182b87 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java
@@ -51,6 +51,7 @@ import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTag;
import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.StringUtils;
import org.junit.Before;
import org.junit.Test;
@@ -89,25 +90,26 @@ public class RefDirectoryTest extends LocalDiskRepositoryTestCase {
@Test
public void testCreate() throws IOException {
// setUp above created the directory. We just have to test it.
- File d = diskRepo.getDirectory();
+ File gitDir = diskRepo.getDirectory();
+ File commonDir = diskRepo.getCommonDirectory();
assertSame(diskRepo, refdir.getRepository());
- assertTrue(new File(d, "refs").isDirectory());
- assertTrue(new File(d, "logs").isDirectory());
- assertTrue(new File(d, "logs/refs").isDirectory());
- assertFalse(new File(d, "packed-refs").exists());
+ assertTrue(new File(commonDir, "refs").isDirectory());
+ assertTrue(new File(commonDir, "logs").isDirectory());
+ assertTrue(new File(commonDir, "logs/refs").isDirectory());
+ assertFalse(new File(commonDir, "packed-refs").exists());
- assertTrue(new File(d, "refs/heads").isDirectory());
- assertTrue(new File(d, "refs/tags").isDirectory());
- assertEquals(2, new File(d, "refs").list().length);
- assertEquals(0, new File(d, "refs/heads").list().length);
- assertEquals(0, new File(d, "refs/tags").list().length);
+ assertTrue(new File(commonDir, "refs/heads").isDirectory());
+ assertTrue(new File(commonDir, "refs/tags").isDirectory());
+ assertEquals(2, new File(commonDir, "refs").list().length);
+ assertEquals(0, new File(commonDir, "refs/heads").list().length);
+ assertEquals(0, new File(commonDir, "refs/tags").list().length);
- assertTrue(new File(d, "logs/refs/heads").isDirectory());
- assertFalse(new File(d, "logs/HEAD").exists());
- assertEquals(0, new File(d, "logs/refs/heads").list().length);
+ assertTrue(new File(commonDir, "logs/refs/heads").isDirectory());
+ assertFalse(new File(gitDir, "logs/HEAD").exists());
+ assertEquals(0, new File(commonDir, "logs/refs/heads").list().length);
- assertEquals("ref: refs/heads/master\n", read(new File(d, HEAD)));
+ assertEquals("ref: refs/heads/master\n", read(new File(gitDir, HEAD)));
}
@Test(expected = UnsupportedOperationException.class)
@@ -1349,6 +1351,18 @@ public class RefDirectoryTest extends LocalDiskRepositoryTestCase {
assertEquals(Storage.LOOSE, ref.getStorage());
}
+ @Test
+ public void testCommonRefPrefix() {
+ assertEquals("", StringUtils.commonPrefix());
+ assertEquals("HEAD", StringUtils.commonPrefix("HEAD"));
+ assertEquals("", StringUtils.commonPrefix("HEAD", ""));
+ assertEquals("", StringUtils.commonPrefix("HEAD", "refs/heads/"));
+ assertEquals("refs/heads/",
+ StringUtils.commonPrefix("refs/heads/master", "refs/heads/"));
+ assertEquals("refs/heads/",
+ StringUtils.commonPrefix("refs/heads/", "refs/heads/main"));
+ }
+
void writePackedRef(String name, AnyObjectId id) throws IOException {
writePackedRefs(id.name() + " " + name + "\n");
}
@@ -1369,7 +1383,7 @@ public class RefDirectoryTest extends LocalDiskRepositoryTestCase {
}
private void deleteLooseRef(String name) {
- File path = new File(diskRepo.getDirectory(), name);
+ File path = new File(diskRepo.getCommonDirectory(), name);
assertTrue("deleted " + name, path.delete());
}
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefUpdateTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefUpdateTest.java
index 28d5ca726a..acc36d76f4 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefUpdateTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefUpdateTest.java
@@ -40,6 +40,7 @@ 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.RefDatabase;
import org.eclipse.jgit.lib.RefRename;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.RefUpdate.Result;
@@ -111,16 +112,17 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase {
assertNotSame(newid, r.getObjectId());
assertSame(ObjectId.class, r.getObjectId().getClass());
assertEquals(newid, r.getObjectId());
- List<ReflogEntry> reverseEntries1 = db
+ List<ReflogEntry> reverseEntries1 = db.getRefDatabase()
.getReflogReader("refs/heads/abc").getReverseEntries();
ReflogEntry entry1 = reverseEntries1.get(0);
assertEquals(1, reverseEntries1.size());
assertEquals(ObjectId.zeroId(), entry1.getOldId());
assertEquals(r.getObjectId(), entry1.getNewId());
- assertEquals(new PersonIdent(db).toString(), entry1.getWho().toString());
+ assertEquals(new PersonIdent(db).toString(),
+ entry1.getWho().toString());
assertEquals("", entry1.getComment());
- List<ReflogEntry> reverseEntries2 = db.getReflogReader("HEAD")
- .getReverseEntries();
+ List<ReflogEntry> reverseEntries2 = db.getRefDatabase()
+ .getReflogReader("HEAD").getReverseEntries();
assertEquals(0, reverseEntries2.size());
}
@@ -136,8 +138,11 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase {
final RefUpdate ru2 = updateRef(newRef2);
Result update2 = ru2.update();
assertEquals(Result.LOCK_FAILURE, update2);
- assertEquals(1, db.getReflogReader("refs/heads/z").getReverseEntries().size());
- assertEquals(0, db.getReflogReader("HEAD").getReverseEntries().size());
+ RefDatabase refDb = db.getRefDatabase();
+ assertEquals(1, refDb.getReflogReader("refs/heads/z")
+ .getReverseEntries().size());
+ assertEquals(0,
+ refDb.getReflogReader("HEAD").getReverseEntries().size());
}
@Test
@@ -147,8 +152,10 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase {
final RefUpdate ru = updateRef(newRef);
Result update = ru.update();
assertEquals(Result.LOCK_FAILURE, update);
- assertNull(db.getReflogReader("refs/heads/master/x"));
- assertEquals(0, db.getReflogReader("HEAD").getReverseEntries().size());
+ RefDatabase refDb = db.getRefDatabase();
+ assertNull(refDb.getReflogReader("refs/heads/master/x"));
+ assertEquals(0,
+ refDb.getReflogReader("HEAD").getReverseEntries().size());
}
@Test
@@ -163,9 +170,12 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase {
final RefUpdate ru2 = updateRef(newRef2);
Result update2 = ru2.update();
assertEquals(Result.LOCK_FAILURE, update2);
- assertEquals(1, db.getReflogReader("refs/heads/z/a").getReverseEntries().size());
- assertNull(db.getReflogReader("refs/heads/z"));
- assertEquals(0, db.getReflogReader("HEAD").getReverseEntries().size());
+ RefDatabase refDb = db.getRefDatabase();
+ assertEquals(1, refDb.getReflogReader("refs/heads/z/a")
+ .getReverseEntries().size());
+ assertNull(refDb.getReflogReader("refs/heads/z"));
+ assertEquals(0,
+ refDb.getReflogReader("HEAD").getReverseEntries().size());
}
@Test
@@ -175,8 +185,10 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase {
final RefUpdate ru = updateRef(newRef);
Result update = ru.update();
assertEquals(Result.LOCK_FAILURE, update);
- assertNull(db.getReflogReader("refs/heads/prefix"));
- assertEquals(0, db.getReflogReader("HEAD").getReverseEntries().size());
+ RefDatabase refDb = db.getRefDatabase();
+ assertNull(refDb.getReflogReader("refs/heads/prefix"));
+ assertEquals(0,
+ refDb.getReflogReader("HEAD").getReverseEntries().size());
}
/**
@@ -197,8 +209,11 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase {
Result delete = updateRef2.delete();
assertEquals(Result.REJECTED_CURRENT_BRANCH, delete);
assertEquals(pid, db.resolve("refs/heads/master"));
- assertEquals(1,db.getReflogReader("refs/heads/master").getReverseEntries().size());
- assertEquals(0,db.getReflogReader("HEAD").getReverseEntries().size());
+ RefDatabase refDb = db.getRefDatabase();
+ assertEquals(1, refDb.getReflogReader("refs/heads/master")
+ .getReverseEntries().size());
+ assertEquals(0,
+ refDb.getReflogReader("HEAD").getReverseEntries().size());
}
@Test
@@ -209,7 +224,8 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase {
updateRef.setForceUpdate(true);
Result update = updateRef.update();
assertEquals(Result.FORCED, update);
- assertEquals(1,db.getReflogReader("refs/heads/master").getReverseEntries().size());
+ assertEquals(1, db.getRefDatabase().getReflogReader("refs/heads/master")
+ .getReverseEntries().size());
}
@Test
@@ -219,15 +235,18 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase {
ref.update(); // create loose ref
ref = updateRef(newRef); // refresh
delete(ref, Result.NO_CHANGE);
- assertNull(db.getReflogReader("refs/heads/abc"));
+ assertNull(db.getRefDatabase().getReflogReader("refs/heads/abc"));
}
@Test
public void testDeleteHead() throws IOException {
final RefUpdate ref = updateRef(Constants.HEAD);
delete(ref, Result.REJECTED_CURRENT_BRANCH, true, false);
- assertEquals(0, db.getReflogReader("refs/heads/master").getReverseEntries().size());
- assertEquals(0, db.getReflogReader("HEAD").getReverseEntries().size());
+ RefDatabase refDb = db.getRefDatabase();
+ assertEquals(0, refDb.getReflogReader("refs/heads/master")
+ .getReverseEntries().size());
+ assertEquals(0,
+ refDb.getReflogReader("HEAD").getReverseEntries().size());
}
@Test
@@ -423,7 +442,7 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase {
// the branch HEAD referred to is left untouched
assertEquals(pid, db.resolve("refs/heads/master"));
- ReflogReader reflogReader = db.getReflogReader("HEAD");
+ ReflogReader reflogReader = db.getRefDatabase().getReflogReader("HEAD");
ReflogEntry e = reflogReader.getReverseEntries().get(0);
assertEquals(pid, e.getOldId());
assertEquals(ppid, e.getNewId());
@@ -453,7 +472,7 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase {
// the branch HEAD referred to is left untouched
assertNull(db.resolve("refs/heads/unborn"));
- ReflogReader reflogReader = db.getReflogReader("HEAD");
+ ReflogReader reflogReader = db.getRefDatabase().getReflogReader("HEAD");
ReflogEntry e = reflogReader.getReverseEntries().get(0);
assertEquals(ObjectId.zeroId(), e.getOldId());
assertEquals(ppid, e.getNewId());
@@ -513,7 +532,6 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase {
@Test
public void testRefsCacheAfterUpdate() throws Exception {
// Do not use the default repo for this case.
- List<Ref> allRefs = db.getRefDatabase().getRefs();
ObjectId oldValue = db.resolve("HEAD");
ObjectId newValue = db.resolve("HEAD^");
// first make HEAD refer to loose ref
@@ -529,7 +547,7 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase {
update = updateRef.update();
assertEquals(Result.FAST_FORWARD, update);
- allRefs = db.getRefDatabase().getRefs();
+ List<Ref> allRefs = db.getRefDatabase().getRefs();
Ref master = getRef(allRefs, "refs/heads/master").get();
Ref head = getRef(allRefs, "HEAD").get();
assertEquals("refs/heads/master", master.getName());
@@ -550,7 +568,6 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase {
@Test
public void testRefsCacheAfterUpdateLooseOnly() throws Exception {
// Do not use the default repo for this case.
- List<Ref> allRefs = db.getRefDatabase().getRefs();
ObjectId oldValue = db.resolve("HEAD");
writeSymref(Constants.HEAD, "refs/heads/newref");
RefUpdate updateRef = db.updateRef(Constants.HEAD);
@@ -559,7 +576,7 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase {
Result update = updateRef.update();
assertEquals(Result.NEW, update);
- allRefs = db.getRefDatabase().getRefs();
+ List<Ref> allRefs = db.getRefDatabase().getRefs();
Ref head = getRef(allRefs, "HEAD").get();
Ref newref = getRef(allRefs, "refs/heads/newref").get();
assertEquals("refs/heads/newref", newref.getName());
@@ -693,9 +710,12 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase {
assertEquals(Result.RENAMED, result);
assertEquals(rb, db.resolve("refs/heads/new/name"));
assertNull(db.resolve("refs/heads/b"));
- assertEquals(1, db.getReflogReader("new/name").getReverseEntries().size());
- assertEquals("Branch: renamed b to new/name", db.getReflogReader("new/name")
- .getLastEntry().getComment());
+ RefDatabase refDb = db.getRefDatabase();
+ assertEquals(1, refDb.getReflogReader("refs/heads/new/name")
+ .getReverseEntries().size());
+ assertEquals("Branch: renamed b to new/name",
+ refDb.getReflogReader("refs/heads/new/name").getLastEntry()
+ .getComment());
assertFalse(new File(db.getDirectory(), "logs/refs/heads/b").exists());
assertEquals(oldHead, db.resolve(Constants.HEAD)); // unchanged
}
@@ -715,11 +735,15 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase {
assertEquals(Result.RENAMED, result);
assertEquals(rb, db.resolve("refs/heads/new/name"));
assertNull(db.resolve("refs/heads/b"));
- assertEquals(2, db.getReflogReader("new/name").getReverseEntries().size());
- assertEquals("Branch: renamed b to new/name", db.getReflogReader("new/name")
- .getLastEntry().getComment());
- assertEquals("Just a message", db.getReflogReader("new/name")
- .getReverseEntries().get(1).getComment());
+ RefDatabase refDb = db.getRefDatabase();
+ assertEquals(2, refDb.getReflogReader("refs/heads/new/name")
+ .getReverseEntries().size());
+ assertEquals("Branch: renamed b to new/name",
+ refDb.getReflogReader("refs/heads/new/name").getLastEntry()
+ .getComment());
+ assertEquals("Just a message",
+ refDb.getReflogReader("refs/heads/new/name").getReverseEntries()
+ .get(1).getComment());
assertFalse(new File(db.getDirectory(), "logs/refs/heads/b").exists());
assertEquals(oldHead, db.resolve(Constants.HEAD)); // unchanged
}
@@ -739,13 +763,20 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase {
assertEquals(Result.RENAMED, result);
assertEquals(rb, db.resolve("refs/heads/new/name"));
assertNull(db.resolve("refs/heads/b"));
- assertEquals("Branch: renamed b to new/name", db.getReflogReader(
- "new/name").getLastEntry().getComment());
+ RefDatabase refDb = db.getRefDatabase();
+ assertEquals("Branch: renamed b to new/name",
+ refDb.getReflogReader("refs/heads/new/name").getLastEntry()
+ .getComment());
assertFalse(new File(db.getDirectory(), "logs/refs/heads/b").exists());
assertEquals(rb, db.resolve(Constants.HEAD));
- assertEquals(2, db.getReflogReader("new/name").getReverseEntries().size());
- assertEquals("Branch: renamed b to new/name", db.getReflogReader("new/name").getReverseEntries().get(0).getComment());
- assertEquals("Just a message", db.getReflogReader("new/name").getReverseEntries().get(1).getComment());
+ assertEquals(2, refDb.getReflogReader("refs/heads/new/name")
+ .getReverseEntries().size());
+ assertEquals("Branch: renamed b to new/name",
+ refDb.getReflogReader("refs/heads/new/name").getReverseEntries()
+ .get(0).getComment());
+ assertEquals("Just a message",
+ refDb.getReflogReader("refs/heads/new/name").getReverseEntries()
+ .get(1).getComment());
}
@Test
@@ -768,11 +799,17 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase {
assertEquals(Result.RENAMED, result);
assertEquals(rb2, db.resolve("refs/heads/new/name"));
assertNull(db.resolve("refs/heads/b"));
- assertEquals("Branch: renamed b to new/name", db.getReflogReader(
- "new/name").getLastEntry().getComment());
- assertEquals(3, db.getReflogReader("refs/heads/new/name").getReverseEntries().size());
- assertEquals("Branch: renamed b to new/name", db.getReflogReader("refs/heads/new/name").getReverseEntries().get(0).getComment());
- assertEquals(0, db.getReflogReader("HEAD").getReverseEntries().size());
+ RefDatabase refDb = db.getRefDatabase();
+ assertEquals("Branch: renamed b to new/name",
+ refDb.getReflogReader("refs/heads/new/name").getLastEntry()
+ .getComment());
+ assertEquals(3, refDb.getReflogReader("refs/heads/new/name")
+ .getReverseEntries().size());
+ assertEquals("Branch: renamed b to new/name",
+ refDb.getReflogReader("refs/heads/new/name").getReverseEntries()
+ .get(0).getComment());
+ assertEquals(0,
+ refDb.getReflogReader("HEAD").getReverseEntries().size());
// make sure b's log file is gone too.
assertFalse(new File(db.getDirectory(), "logs/refs/heads/b").exists());
@@ -791,9 +828,10 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase {
ObjectId oldfromId = db.resolve(fromName);
ObjectId oldHeadId = db.resolve(Constants.HEAD);
writeReflog(db, oldfromId, "Just a message", fromName);
- List<ReflogEntry> oldFromLog = db
+ RefDatabase refDb = db.getRefDatabase();
+ List<ReflogEntry> oldFromLog = refDb
.getReflogReader(fromName).getReverseEntries();
- List<ReflogEntry> oldHeadLog = oldHeadId != null ? db
+ List<ReflogEntry> oldHeadLog = oldHeadId != null ? refDb
.getReflogReader(Constants.HEAD).getReverseEntries() : null;
assertTrue("internal check, we have a log", new File(db.getDirectory(),
@@ -820,10 +858,10 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase {
assertEquals(oldHeadId, db.resolve(Constants.HEAD));
assertEquals(oldfromId, db.resolve(fromName));
assertNull(db.resolve(toName));
- assertEquals(oldFromLog.toString(), db.getReflogReader(fromName)
+ assertEquals(oldFromLog.toString(), refDb.getReflogReader(fromName)
.getReverseEntries().toString());
if (oldHeadId != null && oldHeadLog != null)
- assertEquals(oldHeadLog.toString(), db.getReflogReader(
+ assertEquals(oldHeadLog.toString(), refDb.getReflogReader(
Constants.HEAD).getReverseEntries().toString());
} finally {
lockFile.unlock();
@@ -944,15 +982,18 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase {
assertEquals(Result.RENAMED, result);
assertNull(db.resolve("refs/heads/a"));
assertEquals(rb, db.resolve("refs/heads/a/b"));
- assertEquals(3, db.getReflogReader("a/b").getReverseEntries().size());
- assertEquals("Branch: renamed a to a/b", db.getReflogReader("a/b")
- .getReverseEntries().get(0).getComment());
- assertEquals("Just a message", db.getReflogReader("a/b")
+ RefDatabase refDb = db.getRefDatabase();
+ assertEquals(3, refDb.getReflogReader("refs/heads/a/b")
+ .getReverseEntries().size());
+ assertEquals("Branch: renamed a to a/b",
+ refDb.getReflogReader("refs/heads/a/b").getReverseEntries()
+ .get(0).getComment());
+ assertEquals("Just a message", refDb.getReflogReader("refs/heads/a/b")
.getReverseEntries().get(1).getComment());
- assertEquals("Setup", db.getReflogReader("a/b").getReverseEntries()
- .get(2).getComment());
+ assertEquals("Setup", refDb.getReflogReader("refs/heads/a/b")
+ .getReverseEntries().get(2).getComment());
// same thing was logged to HEAD
- assertEquals("Branch: renamed a to a/b", db.getReflogReader("HEAD")
+ assertEquals("Branch: renamed a to a/b", refDb.getReflogReader("HEAD")
.getReverseEntries().get(0).getComment());
}
@@ -980,15 +1021,20 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase {
assertNull(db.resolve("refs/heads/prefix/a"));
assertEquals(rb, db.resolve("refs/heads/prefix"));
- assertEquals(3, db.getReflogReader("prefix").getReverseEntries().size());
- assertEquals("Branch: renamed prefix/a to prefix", db.getReflogReader(
- "prefix").getReverseEntries().get(0).getComment());
- assertEquals("Just a message", db.getReflogReader("prefix")
- .getReverseEntries().get(1).getComment());
- assertEquals("Setup", db.getReflogReader("prefix").getReverseEntries()
- .get(2).getComment());
- assertEquals("Branch: renamed prefix/a to prefix", db.getReflogReader(
- "HEAD").getReverseEntries().get(0).getComment());
+ RefDatabase refDb = db.getRefDatabase();
+ assertEquals(3, refDb.getReflogReader("refs/heads/prefix")
+ .getReverseEntries().size());
+ assertEquals("Branch: renamed prefix/a to prefix",
+ refDb.getReflogReader("refs/heads/prefix").getReverseEntries()
+ .get(0).getComment());
+ assertEquals("Just a message",
+ refDb.getReflogReader("refs/heads/prefix").getReverseEntries()
+ .get(1).getComment());
+ assertEquals("Setup", refDb.getReflogReader("refs/heads/prefix")
+ .getReverseEntries().get(2).getComment());
+ assertEquals("Branch: renamed prefix/a to prefix",
+ refDb.getReflogReader("HEAD").getReverseEntries().get(0)
+ .getComment());
}
@Test
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ReflogReaderTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ReflogReaderTest.java
index dc0e749373..16645cbcd7 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ReflogReaderTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ReflogReaderTest.java
@@ -27,6 +27,7 @@ import org.eclipse.jgit.lib.CheckoutEntry;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.RefDatabase;
import org.eclipse.jgit.lib.ReflogEntry;
import org.eclipse.jgit.lib.ReflogReader;
import org.eclipse.jgit.test.resources.SampleDataRepositoryTestCase;
@@ -154,18 +155,22 @@ public class ReflogReaderTest extends SampleDataRepositoryTestCase {
setupReflog("logs/refs/heads/a", aLine);
setupReflog("logs/refs/heads/master", masterLine);
setupReflog("logs/HEAD", headLine);
- assertEquals("branch: change to master", db.getReflogReader("master")
- .getLastEntry().getComment());
- assertEquals("branch: change to a", db.getReflogReader("a")
- .getLastEntry().getComment());
- assertEquals("branch: change to HEAD", db.getReflogReader("HEAD")
- .getLastEntry().getComment());
+ RefDatabase refDb = db.getRefDatabase();
+ assertEquals("branch: change to master",
+ refDb.getReflogReader("refs/heads/master").getLastEntry()
+ .getComment());
+ assertEquals("branch: change to a",
+ refDb.getReflogReader("refs/heads/a").getLastEntry()
+ .getComment());
+ assertEquals("branch: change to HEAD",
+ refDb.getReflogReader("HEAD").getLastEntry().getComment());
}
@Test
public void testReadLineWithMissingComment() throws Exception {
setupReflog("logs/refs/heads/master", oneLineWithoutComment);
- final ReflogReader reader = db.getReflogReader("master");
+ final ReflogReader reader = db.getRefDatabase()
+ .getReflogReader("refs/heads/master");
ReflogEntry e = reader.getLastEntry();
assertEquals(ObjectId
.fromString("da85355dfc525c9f6f3927b876f379f46ccf826e"), e
@@ -183,15 +188,18 @@ public class ReflogReaderTest extends SampleDataRepositoryTestCase {
@Test
public void testNoLog() throws Exception {
- assertEquals(0, db.getReflogReader("master").getReverseEntries().size());
- assertNull(db.getReflogReader("master").getLastEntry());
+ RefDatabase refDb = db.getRefDatabase();
+ assertEquals(0,
+ refDb.getReflogReader("refs/heads/master").getReverseEntries()
+ .size());
+ assertNull(refDb.getReflogReader("refs/heads/master").getLastEntry());
}
@Test
public void testCheckout() throws Exception {
setupReflog("logs/HEAD", switchBranch);
- List<ReflogEntry> entries = db.getReflogReader(Constants.HEAD)
- .getReverseEntries();
+ List<ReflogEntry> entries = db.getRefDatabase()
+ .getReflogReader(Constants.HEAD).getReverseEntries();
assertEquals(1, entries.size());
ReflogEntry entry = entries.get(0);
CheckoutEntry checkout = entry.parseCheckout();
@@ -238,7 +246,7 @@ public class ReflogReaderTest extends SampleDataRepositoryTestCase {
private void setupReflog(String logName, byte[] data)
throws FileNotFoundException, IOException {
- File logfile = new File(db.getDirectory(), logName);
+ File logfile = new File(db.getCommonDirectory(), logName);
if (!logfile.getParentFile().mkdirs()
&& !logfile.getParentFile().isDirectory()) {
throw new IOException(
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ReflogWriterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ReflogWriterTest.java
index 8d0e99dea0..a8363336d9 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ReflogWriterTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ReflogWriterTest.java
@@ -16,6 +16,8 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
+import java.time.Instant;
+import java.time.ZoneOffset;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
@@ -32,7 +34,7 @@ public class ReflogWriterTest extends SampleDataRepositoryTestCase {
ReflogWriter writer =
new ReflogWriter((RefDirectory) db.getRefDatabase());
PersonIdent ident = new PersonIdent("John Doe", "john@doe.com",
- 1243028200000L, 120);
+ Instant.ofEpochMilli(1243028200000L), ZoneOffset.ofHours(2));
ObjectId oldId = ObjectId
.fromString("da85355dfc525c9f6f3927b876f379f46ccf826e");
ObjectId newId = ObjectId
@@ -48,7 +50,7 @@ public class ReflogWriterTest extends SampleDataRepositoryTestCase {
private void readReflog(byte[] buffer)
throws FileNotFoundException, IOException {
- File logfile = new File(db.getDirectory(), "logs/refs/heads/master");
+ File logfile = new File(db.getCommonDirectory(), "logs/refs/heads/master");
if (!logfile.getParentFile().mkdirs()
&& !logfile.getParentFile().isDirectory()) {
throw new IOException(
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/SnapshottingRefDirectoryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/SnapshottingRefDirectoryTest.java
index c3dafe4aa2..90a2aa601e 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/SnapshottingRefDirectoryTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/SnapshottingRefDirectoryTest.java
@@ -23,7 +23,6 @@ import static org.junit.Assert.assertEquals;
public class SnapshottingRefDirectoryTest extends RefDirectoryTest {
private RefDirectory originalRefDirectory;
- /** {@inheritDoc} */
@Before
@Override
public void setUp() throws Exception {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/StoredBitmapTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/StoredBitmapTest.java
index f5c7c67c5d..684ee52b4a 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/StoredBitmapTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/StoredBitmapTest.java
@@ -11,6 +11,8 @@
package org.eclipse.jgit.internal.storage.file;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
import org.eclipse.jgit.internal.storage.file.BasePackBitmapIndex.StoredBitmap;
import org.eclipse.jgit.lib.ObjectId;
@@ -44,6 +46,33 @@ public class StoredBitmapTest {
assertEquals(bitmapOf(50, 90), sb.getBitmap());
}
+ @Test
+ public void testGetSizeWithoutXor() {
+ EWAHCompressedBitmap base = bitmapOf(100);
+ StoredBitmap sb = newStoredBitmap(base);
+ assertEquals(base.sizeInBytes(), sb.getCurrentSizeInBytes());
+ sb.getBitmap();
+ assertEquals(base.sizeInBytes(), sb.getCurrentSizeInBytes());
+ }
+
+ @Test
+ public void testGetSizeWithOneXor() {
+ EWAHCompressedBitmap base = bitmapOf(100, 101);
+ EWAHCompressedBitmap xor = bitmapOf(100);
+ StoredBitmap sb = newStoredBitmap(base, xor);
+ assertEquals(xor.sizeInBytes(), sb.getCurrentSizeInBytes());
+ }
+
+ @Test
+ public void testIsBase() {
+ EWAHCompressedBitmap one = bitmapOf(100, 101);
+ EWAHCompressedBitmap two = bitmapOf(100);
+ StoredBitmap baseBitmap = newStoredBitmap(one);
+ StoredBitmap xoredBitmap = newStoredBitmap(one, two);
+ assertTrue(baseBitmap.isBase());
+ assertFalse(xoredBitmap.isBase());
+ }
+
private static final StoredBitmap newStoredBitmap(
EWAHCompressedBitmap... bitmaps) {
StoredBitmap sb = null;
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 49e8a7be66..e067beb317 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
@@ -28,6 +28,7 @@ import java.io.FileInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.time.Instant;
+import java.time.ZoneOffset;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -374,8 +375,10 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase {
public void test009_CreateCommitOldFormat() throws IOException {
final ObjectId treeId = insertTree(new TreeFormatter());
final CommitBuilder c = new CommitBuilder();
- c.setAuthor(new PersonIdent(author, 1154236443000L, -4 * 60));
- c.setCommitter(new PersonIdent(committer, 1154236443000L, -4 * 60));
+ c.setAuthor(new PersonIdent(author,
+ Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
+ c.setCommitter(new PersonIdent(committer,
+ Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
c.setMessage("A Commit\n");
c.setTreeId(treeId);
assertEquals(treeId, c.getTreeId());
@@ -411,7 +414,8 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase {
final TagBuilder t = new TagBuilder();
t.setObjectId(emptyId, Constants.OBJ_BLOB);
t.setTag("test020");
- t.setTagger(new PersonIdent(author, 1154236443000L, -4 * 60));
+ t.setTagger(new PersonIdent(author,
+ Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
t.setMessage("test020 tagged\n");
ObjectId actid = insertTag(t);
assertEquals("6759556b09fbb4fd8ae5e315134481cc25d46954", actid.name());
@@ -419,8 +423,9 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase {
RevTag mapTag = parseTag(actid);
assertEquals(Constants.OBJ_BLOB, mapTag.getObject().getType());
assertEquals("test020 tagged\n", mapTag.getFullMessage());
- assertEquals(new PersonIdent(author, 1154236443000L, -4 * 60), mapTag
- .getTaggerIdent());
+ assertEquals(new PersonIdent(author,
+ Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)),
+ mapTag.getTaggerIdent());
assertEquals("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", mapTag
.getObject().getId().name());
}
@@ -434,7 +439,8 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase {
final TagBuilder t = new TagBuilder();
t.setObjectId(almostEmptyTreeId, Constants.OBJ_TREE);
t.setTag("test021");
- t.setTagger(new PersonIdent(author, 1154236443000L, -4 * 60));
+ t.setTagger(new PersonIdent(author,
+ Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
t.setMessage("test021 tagged\n");
ObjectId actid = insertTag(t);
assertEquals("b0517bc8dbe2096b419d42424cd7030733f4abe5", actid.name());
@@ -442,8 +448,9 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase {
RevTag mapTag = parseTag(actid);
assertEquals(Constants.OBJ_TREE, mapTag.getObject().getType());
assertEquals("test021 tagged\n", mapTag.getFullMessage());
- assertEquals(new PersonIdent(author, 1154236443000L, -4 * 60), mapTag
- .getTaggerIdent());
+ assertEquals(new PersonIdent(author,
+ Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)),
+ mapTag.getTaggerIdent());
assertEquals("417c01c8795a35b8e835113a85a5c0c1c77f67fb", mapTag
.getObject().getId().name());
}
@@ -455,17 +462,18 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase {
almostEmptyTree.append("empty", FileMode.REGULAR_FILE, emptyId);
final ObjectId almostEmptyTreeId = insertTree(almostEmptyTree);
final CommitBuilder almostEmptyCommit = new CommitBuilder();
- almostEmptyCommit.setAuthor(new PersonIdent(author, 1154236443000L,
- -2 * 60)); // not exactly the same
- almostEmptyCommit.setCommitter(new PersonIdent(author, 1154236443000L,
- -2 * 60));
+ almostEmptyCommit.setAuthor(new PersonIdent(author,
+ Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-2)));
+ almostEmptyCommit.setCommitter(new PersonIdent(author,
+ Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-2)));
almostEmptyCommit.setMessage("test022\n");
almostEmptyCommit.setTreeId(almostEmptyTreeId);
ObjectId almostEmptyCommitId = insertCommit(almostEmptyCommit);
final TagBuilder t = new TagBuilder();
t.setObjectId(almostEmptyCommitId, Constants.OBJ_COMMIT);
t.setTag("test022");
- t.setTagger(new PersonIdent(author, 1154236443000L, -4 * 60));
+ t.setTagger(new PersonIdent(author,
+ Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
t.setMessage("test022 tagged\n");
ObjectId actid = insertTag(t);
assertEquals("0ce2ebdb36076ef0b38adbe077a07d43b43e3807", actid.name());
@@ -473,8 +481,9 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase {
RevTag mapTag = parseTag(actid);
assertEquals(Constants.OBJ_COMMIT, mapTag.getObject().getType());
assertEquals("test022 tagged\n", mapTag.getFullMessage());
- assertEquals(new PersonIdent(author, 1154236443000L, -4 * 60), mapTag
- .getTaggerIdent());
+ assertEquals(new PersonIdent(author,
+ Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)),
+ mapTag.getTaggerIdent());
assertEquals("b5d3b45a96b340441f5abb9080411705c51cc86c", mapTag
.getObject().getId().name());
}
@@ -488,9 +497,9 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase {
CommitBuilder commit = new CommitBuilder();
commit.setTreeId(almostEmptyTreeId);
commit.setAuthor(new PersonIdent("Joe H\u00e4cker", "joe@example.com",
- 4294967295000L, 60));
+ Instant.ofEpochMilli(4294967295000L), ZoneOffset.ofHours(1)));
commit.setCommitter(new PersonIdent("Joe Hacker", "joe2@example.com",
- 4294967295000L, 60));
+ Instant.ofEpochMilli(4294967295000L), ZoneOffset.ofHours(1)));
commit.setEncoding(UTF_8);
commit.setMessage("\u00dcbergeeks");
ObjectId cid = insertCommit(commit);
@@ -509,9 +518,9 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase {
CommitBuilder commit = new CommitBuilder();
commit.setTreeId(almostEmptyTreeId);
commit.setAuthor(new PersonIdent("Joe H\u00e4cker", "joe@example.com",
- 4294967295000L, 60));
+ Instant.ofEpochMilli(4294967295000L), ZoneOffset.ofHours(1)));
commit.setCommitter(new PersonIdent("Joe Hacker", "joe2@example.com",
- 4294967295000L, 60));
+ Instant.ofEpochMilli(4294967295000L), ZoneOffset.ofHours(1)));
commit.setEncoding(ISO_8859_1);
commit.setMessage("\u00dcbergeeks");
ObjectId cid = insertCommit(commit);
@@ -544,8 +553,10 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase {
.fromString("00b1f73724f493096d1ffa0b0f1f1482dbb8c936"), treeId);
final CommitBuilder c1 = new CommitBuilder();
- c1.setAuthor(new PersonIdent(author, 1154236443000L, -4 * 60));
- c1.setCommitter(new PersonIdent(committer, 1154236443000L, -4 * 60));
+ c1.setAuthor(new PersonIdent(author,
+ Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
+ c1.setCommitter(new PersonIdent(committer,
+ Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
c1.setMessage("A Commit\n");
c1.setTreeId(treeId);
assertEquals(treeId, c1.getTreeId());
@@ -555,8 +566,10 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase {
assertEquals(cmtid1, actid1);
final CommitBuilder c2 = new CommitBuilder();
- c2.setAuthor(new PersonIdent(author, 1154236443000L, -4 * 60));
- c2.setCommitter(new PersonIdent(committer, 1154236443000L, -4 * 60));
+ c2.setAuthor(new PersonIdent(author,
+ Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
+ c2.setCommitter(new PersonIdent(committer,
+ Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
c2.setMessage("A Commit 2\n");
c2.setTreeId(treeId);
assertEquals(treeId, c2.getTreeId());
@@ -577,8 +590,10 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase {
assertEquals(actid1, rm2.getParent(0));
final CommitBuilder c3 = new CommitBuilder();
- c3.setAuthor(new PersonIdent(author, 1154236443000L, -4 * 60));
- c3.setCommitter(new PersonIdent(committer, 1154236443000L, -4 * 60));
+ c3.setAuthor(new PersonIdent(author,
+ Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
+ c3.setCommitter(new PersonIdent(committer,
+ Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
c3.setMessage("A Commit 3\n");
c3.setTreeId(treeId);
assertEquals(treeId, c3.getTreeId());
@@ -600,8 +615,10 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase {
assertEquals(actid2, rm3.getParent(1));
final CommitBuilder c4 = new CommitBuilder();
- c4.setAuthor(new PersonIdent(author, 1154236443000L, -4 * 60));
- c4.setCommitter(new PersonIdent(committer, 1154236443000L, -4 * 60));
+ c4.setAuthor(new PersonIdent(author,
+ Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
+ c4.setCommitter(new PersonIdent(committer,
+ Instant.ofEpochMilli(1154236443000L), ZoneOffset.ofHours(-4)));
c4.setMessage("A Commit 4\n");
c4.setTreeId(treeId);
assertEquals(treeId, c3.getTreeId());
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/MultiPackIndexBuilderTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/MultiPackIndexBuilderTest.java
new file mode 100644
index 0000000000..e6fefc623d
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/MultiPackIndexBuilderTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2024, GerritForge Inc. and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.storage.midx;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import org.eclipse.jgit.internal.storage.midx.MultiPackIndexLoader.MultiPackIndexBuilder;
+import org.eclipse.jgit.internal.storage.midx.MultiPackIndexLoader.MultiPackIndexFormatException;
+import org.junit.Test;
+
+public class MultiPackIndexBuilderTest {
+
+ @Test
+ public void testRepeatedChunk() throws Exception {
+ byte[] buffer = new byte[2048];
+
+ MultiPackIndexBuilder builder1 = MultiPackIndexBuilder.builder();
+ builder1.addOidFanout(buffer);
+ Exception e1 = assertThrows(MultiPackIndexFormatException.class, () -> {
+ builder1.addOidFanout(buffer);
+ });
+ assertEquals("midx chunk id 0x4f494446 appears multiple times",
+ e1.getMessage());
+
+ MultiPackIndexBuilder builder2 = MultiPackIndexBuilder.builder();
+ builder2.addOidLookUp(buffer);
+ Exception e2 = assertThrows(MultiPackIndexFormatException.class, () -> {
+ builder2.addOidLookUp(buffer);
+ });
+ assertEquals("midx chunk id 0x4f49444c appears multiple times",
+ e2.getMessage());
+
+ MultiPackIndexBuilder builder3 = MultiPackIndexBuilder.builder();
+ builder3.addObjectOffsets(buffer);
+ Exception e3 = assertThrows(MultiPackIndexFormatException.class, () -> {
+ builder3.addObjectOffsets(buffer);
+ });
+ assertEquals("midx chunk id 0x4f4f4646 appears multiple times",
+ e3.getMessage());
+
+ MultiPackIndexBuilder builder4 = MultiPackIndexBuilder.builder();
+ builder4.addPackNames(buffer);
+ Exception e4 = assertThrows(MultiPackIndexFormatException.class, () -> {
+ builder4.addPackNames(buffer);
+ });
+ assertEquals("midx chunk id 0x504e414d appears multiple times",
+ e4.getMessage());
+
+ MultiPackIndexBuilder builder5 = MultiPackIndexBuilder.builder();
+ builder5.addBitmappedPacks(buffer);
+ Exception e5 = assertThrows(MultiPackIndexFormatException.class, () -> {
+ builder5.addBitmappedPacks(buffer);
+ });
+ assertEquals("midx chunk id 0x42544d50 appears multiple times",
+ e5.getMessage());
+
+ MultiPackIndexBuilder builder6 = MultiPackIndexBuilder.builder();
+ builder6.addObjectLargeOffsets(buffer);
+ Exception e6 = assertThrows(MultiPackIndexFormatException.class, () -> {
+ builder6.addObjectLargeOffsets(buffer);
+ });
+ assertEquals("midx chunk id 0x4c4f4646 appears multiple times",
+ e6.getMessage());
+
+ MultiPackIndexBuilder builder7 = MultiPackIndexBuilder.builder();
+ builder7.addReverseIndex(buffer);
+ Exception e7 = assertThrows(MultiPackIndexFormatException.class, () -> {
+ builder7.addReverseIndex(buffer);
+ });
+ assertEquals("midx chunk id 0x52494458 appears multiple times",
+ e7.getMessage());
+ }
+
+ @Test
+ public void testNeededChunk() {
+ byte[] buffer = new byte[2048];
+
+ Exception e1 = assertThrows(MultiPackIndexFormatException.class, () -> {
+ MultiPackIndexBuilder.builder().addOidLookUp(buffer).build();
+ });
+ assertEquals("midx 0x4f494446 chunk has not been loaded",
+ e1.getMessage());
+
+ Exception e2 = assertThrows(MultiPackIndexFormatException.class, () -> {
+ MultiPackIndexBuilder.builder().addOidFanout(buffer)
+ .addOidLookUp(buffer).build();
+ });
+ assertEquals("midx 0x504e414d chunk has not been loaded",
+ e2.getMessage());
+
+ Exception e3 = assertThrows(MultiPackIndexFormatException.class, () -> {
+ MultiPackIndexBuilder.builder().addOidFanout(buffer)
+ .addOidLookUp(buffer).addPackNames(buffer).build();
+ });
+ assertEquals("midx 0x4f4f4646 chunk has not been loaded",
+ e3.getMessage());
+ }
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/MultiPackIndexLoaderTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/MultiPackIndexLoaderTest.java
new file mode 100644
index 0000000000..494f1d1137
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/MultiPackIndexLoaderTest.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2024, GerritForge Inc. and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.internal.storage.midx;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jgit.internal.storage.file.PackIndex;
+import org.eclipse.jgit.junit.FakeIndexFactory;
+import org.eclipse.jgit.junit.JGitTestUtil;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.junit.Test;
+
+/**
+ * Test that the loader accepts valid files, discard broken files
+ * <p>
+ * Contents and lookups are covered in the MultiPackIndexTest
+ */
+public class MultiPackIndexLoaderTest {
+
+ @Test
+ public void load_validFile_basic_upstream() throws Exception {
+ MultiPackIndex midx = MultiPackIndexLoader
+ .open(JGitTestUtil.getTestResourceFile("multi-pack-index.v1"));
+ assertNotNull(midx);
+ }
+
+ @Test
+ public void load_validFile_basic_jgit() throws Exception {
+ PackIndex idxOne = FakeIndexFactory.indexOf(List.of(
+ new FakeIndexFactory.IndexObject(
+ "0000000000000000000000000000000000000001", 500),
+ new FakeIndexFactory.IndexObject(
+ "0000000000000000000000000000000000000005", 12),
+ new FakeIndexFactory.IndexObject(
+ "0000000000000000000000000000000000000010", 1500)));
+ PackIndex idxTwo = FakeIndexFactory.indexOf(List.of(
+ new FakeIndexFactory.IndexObject(
+ "0000000000000000000000000000000000000002", 501),
+ new FakeIndexFactory.IndexObject(
+ "0000000000000000000000000000000000000003", 13),
+ new FakeIndexFactory.IndexObject(
+ "0000000000000000000000000000000000000015", 1501)));
+ PackIndex idxThree = FakeIndexFactory.indexOf(List.of(
+ new FakeIndexFactory.IndexObject(
+ "0000000000000000000000000000000000000004", 502),
+ new FakeIndexFactory.IndexObject(
+ "0000000000000000000000000000000000000007", 14),
+ new FakeIndexFactory.IndexObject(
+ "0000000000000000000000000000000000000012", 1502)));
+
+ Map<String, PackIndex> packs = Map.of("p1", idxOne, "p2", idxTwo, "p3",
+ idxThree);
+ MultiPackIndexWriter writer = new MultiPackIndexWriter();
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ writer.write(NullProgressMonitor.INSTANCE, out, packs);
+
+ MultiPackIndex midx = MultiPackIndexLoader
+ .read(new ByteArrayInputStream(out.toByteArray()));
+ assertNotNull(midx);
+ }
+
+ @Test
+ public void load_emptyFile() {
+ assertThrows(IOException.class, () -> MultiPackIndexLoader
+ .read(new ByteArrayInputStream(new byte[0])));
+ }
+
+ @Test
+ public void load_rubbishFile() {
+ assertThrows(MultiPackIndexLoader.MultiPackIndexFormatException.class,
+ () -> MultiPackIndexLoader.read(new ByteArrayInputStream(
+ "More than 12 bytes of not-midx".getBytes(UTF_8))));
+ }
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/MultiPackIndexTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/MultiPackIndexTest.java
new file mode 100644
index 0000000000..ab452854b2
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/MultiPackIndexTest.java
@@ -0,0 +1,345 @@
+/*
+ * Copyright (C) 2024, GerritForge Inc. and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.internal.storage.midx;
+
+import static org.junit.Assert.assertArrayEquals;
+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 java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.jgit.internal.storage.file.PackIndex;
+import org.eclipse.jgit.junit.FakeIndexFactory;
+import org.eclipse.jgit.junit.JGitTestUtil;
+import org.eclipse.jgit.lib.AbbreviatedObjectId;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Test;
+
+public class MultiPackIndexTest {
+
+ @Test
+ public void basic_upstream() throws IOException {
+ int knownPackId = 22;
+ int knowOffset = 258;
+ String knownOid = "3f4ee50f784c1e9550f09a67d2ffc1bc76917bdc";
+ String knownPackName = "pack-e4b191e4343f2b7ff851026c2d8595a001077344.idx";
+ String[] packNames = {
+ "pack-15d67b35f2b6a66ff995e09cedb36b101e0e0262.idx",
+ "pack-1a979514a5965e71523187a17806e03af44344ed.idx",
+ "pack-1de6731c035633ba8f5b41dacbc680a5a36ddd90.idx",
+ "pack-1ee98948e4e362c56f3cdec7f5837d06e152854f.idx",
+ "pack-1f6fe52ac3d33f3091d8eb8497474554bfa80bc4.idx",
+ "pack-34b1aa6b437a9d968412454204c2676a88dc55fa.idx",
+ "pack-3b245f7b4aff32a52d0520608f662bbf403792b9.idx",
+ "pack-47901f7f8d1c440492035c4165796a330c7f79e0.idx",
+ "pack-4e7f889b79aea8905a0062ce1bd68e5ef3af6a55.idx",
+ "pack-71ea652e4aea2cbc609545b4fbc3eda6325d88a1.idx",
+ "pack-723b1238411a4257c18167e91fbabed313ba332f.idx",
+ "pack-7bd57092a7daa4dc31277e1ec86f3de8d968ae17.idx",
+ "pack-883d4f469c5ea0f6d373ee623a758aeaf17715fc.idx",
+ "pack-8eadd378a011ddaa5ec751f2a6d9789ef501120f.idx",
+ "pack-92221b6f79a211944ccc6740fc22c9553ea1ba22.idx",
+ "pack-b139d0cae5f54c70d057a8f4d2cf99f0ae0c326c.idx",
+ "pack-b4f5c96d1fa6b1fac17a2a43710693c5514a9224.idx",
+ "pack-bed4bc1521f965e55a5a8a58dffaaefc70ea4753.idx",
+ "pack-cdc6baa7d90707a3c0dac4c188f797f0f79b97bb.idx",
+ "pack-d6d58a58fa24b74c8c082f4f63c4d2ddfb824cc9.idx",
+ "pack-daec59ae07f1091f3b81bd8266481bb5db3c868a.idx",
+ "pack-e2197d60e09ad9091407eff4e06d39ec940851e1.idx",
+ "pack-e4b191e4343f2b7ff851026c2d8595a001077344.idx",
+ "pack-eedf783b5da4caa57be33b08990fe57f245a7413.idx",
+ "pack-efb23e968801b9050bc70f0115a8a0eec88fb879.idx",
+ "pack-f919c0660c207ddf6bb0569a3041d682d19fb4f7.idx" };
+ MultiPackIndex midx = MultiPackIndexLoader
+ .open(JGitTestUtil.getTestResourceFile("multi-pack-index.v1"));
+ assertNotNull(midx);
+ assertArrayEquals(packNames, midx.getPackNames());
+
+ MultiPackIndex.PackOffset oo = midx.find(ObjectId.fromString(knownOid));
+
+ assertEquals(knowOffset, oo.getOffset());
+ assertEquals(knownPackId, oo.getPackId());
+ assertEquals(knownPackName, midx.getPackNames()[oo.getPackId()]);
+ }
+
+ @Test
+ public void basicMidx() throws IOException {
+ PackIndex idxOne = FakeIndexFactory.indexOf(List.of(
+ new FakeIndexFactory.IndexObject(
+ "0000000000000000000000000000000000000001", 500),
+ new FakeIndexFactory.IndexObject(
+ "0000000000000000000000000000000000000005", 12),
+ new FakeIndexFactory.IndexObject(
+ "0000000000000000000000000000000000000010", 1500)));
+ PackIndex idxTwo = FakeIndexFactory.indexOf(List.of(
+ new FakeIndexFactory.IndexObject(
+ "0000000000000000000000000000000000000002", 501),
+ new FakeIndexFactory.IndexObject(
+ "0000000000000000000000000000000000000003", 13),
+ new FakeIndexFactory.IndexObject(
+ "0000000000000000000000000000000000000015", 1501)));
+ PackIndex idxThree = FakeIndexFactory.indexOf(List.of(
+ new FakeIndexFactory.IndexObject(
+ "0000000000000000000000000000000000000004", 502),
+ new FakeIndexFactory.IndexObject(
+ "0000000000000000000000000000000000000007", 14),
+ new FakeIndexFactory.IndexObject(
+ "0000000000000000000000000000000000000012", 1502)));
+
+ Map<String, PackIndex> packs = Map.of("p1", idxOne, "p2", idxTwo, "p3",
+ idxThree);
+ MultiPackIndexWriter writer = new MultiPackIndexWriter();
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ writer.write(NullProgressMonitor.INSTANCE, out, packs);
+
+ MultiPackIndex midx = MultiPackIndexLoader
+ .read(new ByteArrayInputStream(out.toByteArray()));
+ assertEquals(3, midx.getPackNames().length);
+ assertInIndex(midx, 0, "0000000000000000000000000000000000000001", 500);
+ assertInIndex(midx, 0, "0000000000000000000000000000000000000005", 12);
+ assertInIndex(midx, 0, "0000000000000000000000000000000000000010",
+ 1500);
+ assertInIndex(midx, 1, "0000000000000000000000000000000000000002", 501);
+ assertInIndex(midx, 1, "0000000000000000000000000000000000000003", 13);
+ assertInIndex(midx, 1, "0000000000000000000000000000000000000015",
+ 1501);
+ assertInIndex(midx, 2, "0000000000000000000000000000000000000004", 502);
+ assertInIndex(midx, 2, "0000000000000000000000000000000000000007", 14);
+ assertInIndex(midx, 2, "0000000000000000000000000000000000000012",
+ 1502);
+
+ assertNull(midx.find(ObjectId.zeroId()));
+ }
+
+ @Test
+ public void jgit_largeOffsetChunk() throws IOException {
+ PackIndex idxOne = FakeIndexFactory.indexOf(List.of(
+ new FakeIndexFactory.IndexObject(
+ "0000000000000000000000000000000000000001", (1L << 34)),
+ new FakeIndexFactory.IndexObject(
+ "0000000000000000000000000000000000000005", 12)));
+ PackIndex idxTwo = FakeIndexFactory.indexOf(List.of(
+ new FakeIndexFactory.IndexObject(
+ "0000000000000000000000000000000000000002", (1L << 35)),
+ new FakeIndexFactory.IndexObject(
+ "0000000000000000000000000000000000000003", 13)));
+ Map<String, PackIndex> packs = Map.of("p1", idxOne, "p2", idxTwo);
+ MultiPackIndexWriter writer = new MultiPackIndexWriter();
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ writer.write(NullProgressMonitor.INSTANCE, out, packs);
+
+ MultiPackIndex midx = MultiPackIndexLoader
+ .read(new ByteArrayInputStream(out.toByteArray()));
+ assertEquals(2, midx.getPackNames().length);
+ assertInIndex(midx, 0, "0000000000000000000000000000000000000001",
+ (1L << 34));
+ assertInIndex(midx, 0, "0000000000000000000000000000000000000005", 12);
+ assertInIndex(midx, 1, "0000000000000000000000000000000000000002",
+ (1L << 35));
+ }
+
+ @Test
+ public void jgit_largeOffset_noChunk() throws IOException {
+ // All offsets fit in 32 bits, no large offset chunk
+ // Most significant bit to 1 is still valid offset
+ PackIndex idxOne = FakeIndexFactory.indexOf(List.of(
+ new FakeIndexFactory.IndexObject(
+ "0000000000000000000000000000000000000001",
+ 0xff00_0000),
+ new FakeIndexFactory.IndexObject(
+ "0000000000000000000000000000000000000005", 12)));
+ PackIndex idxTwo = FakeIndexFactory.indexOf(List.of(
+ new FakeIndexFactory.IndexObject(
+ "0000000000000000000000000000000000000002", 501),
+ new FakeIndexFactory.IndexObject(
+ "0000000000000000000000000000000000000003", 13)));
+ Map<String, PackIndex> packs = Map.of("p1", idxOne, "p2", idxTwo);
+ MultiPackIndexWriter writer = new MultiPackIndexWriter();
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ writer.write(NullProgressMonitor.INSTANCE, out, packs);
+
+ MultiPackIndex midx = MultiPackIndexLoader
+ .read(new ByteArrayInputStream(out.toByteArray()));
+ assertEquals(2, midx.getPackNames().length);
+ assertInIndex(midx, 0, "0000000000000000000000000000000000000001",
+ 0xff00_0000L);
+ assertInIndex(midx, 0, "0000000000000000000000000000000000000005", 12);
+ }
+
+ @Test
+ public void jgit_resolve() throws IOException {
+ AbbreviatedObjectId abbrev = AbbreviatedObjectId
+ .fromString("32fe829a1c");
+
+ PackIndex idxOne = indexWith(
+ // Noise
+ "0000000000000000000000000000000000000001",
+ "3000000000000000000000000000000000000005",
+ // One before abbrev
+ "32fe829a1b000000000000000000000000000001",
+ // matches
+ "32fe829a1c000000000000000000000000000001",
+ "32fe829a1c000000000000000000000000000100",
+ // One after abbrev
+ "32fe829a1d000000000000000000000000000000");
+ PackIndex idxTwo = indexWith(
+ // Noise
+ "8888880000000000000000000000000000000002",
+ "bbbbbb0000000000000000000000000000000003",
+ // Match
+ "32fe829a1c000000000000000000000000000010");
+
+ Map<String, PackIndex> packs = Map.of("p1", idxOne, "p2", idxTwo);
+ MultiPackIndexWriter writer = new MultiPackIndexWriter();
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ writer.write(NullProgressMonitor.INSTANCE, out, packs);
+ MultiPackIndex midx = MultiPackIndexLoader
+ .read(new ByteArrayInputStream(out.toByteArray()));
+
+
+ Set<ObjectId> results = new HashSet<>();
+ midx.resolve(results, abbrev, 100);
+
+ assertEquals(3, results.size());
+ assertTrue(results.contains(ObjectId
+ .fromString("32fe829a1c000000000000000000000000000001")));
+ assertTrue(results.contains(ObjectId
+ .fromString("32fe829a1c000000000000000000000000000010")));
+ assertTrue(results.contains(ObjectId
+ .fromString("32fe829a1c000000000000000000000000000100")));
+
+ }
+
+ @Test
+ public void jgit_resolve_matchLimit() throws IOException {
+ AbbreviatedObjectId abbrev = AbbreviatedObjectId
+ .fromString("32fe829a1c");
+
+ PackIndex idxOne = indexWith(
+ // Noise
+ "0000000000000000000000000000000000000001",
+ "3000000000000000000000000000000000000005",
+ // One before abbrev
+ "32fe829a1b000000000000000000000000000001",
+ // matches
+ "32fe829a1c000000000000000000000000000001",
+ "32fe829a1c000000000000000000000000000100",
+ // One after abbrev
+ "32fe829a1d000000000000000000000000000000");
+ PackIndex idxTwo = indexWith(
+ // Noise
+ "8888880000000000000000000000000000000002",
+ "bbbbbb0000000000000000000000000000000003",
+ // Match
+ "32fe829a1c000000000000000000000000000010");
+
+ Map<String, PackIndex> packs = Map.of("p1", idxOne, "p2", idxTwo);
+ MultiPackIndexWriter writer = new MultiPackIndexWriter();
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ writer.write(NullProgressMonitor.INSTANCE, out, packs);
+ MultiPackIndex midx = MultiPackIndexLoader
+ .read(new ByteArrayInputStream(out.toByteArray()));
+
+
+ Set<ObjectId> results = new HashSet<>();
+ midx.resolve(results, abbrev, 2);
+
+ assertEquals(2, results.size());
+ assertTrue(results.contains(ObjectId
+ .fromString("32fe829a1c000000000000000000000000000001")));
+ assertTrue(results.contains(ObjectId
+ .fromString("32fe829a1c000000000000000000000000000010")));
+ }
+
+ @Test
+ public void jgit_resolve_noMatches() throws IOException {
+ AbbreviatedObjectId abbrev = AbbreviatedObjectId
+ .fromString("4400000000");
+
+ PackIndex idxOne = indexWith(
+ "0000000000000000000000000000000000000001",
+ "3000000000000000000000000000000000000005",
+ "32fe829a1b000000000000000000000000000001",
+ "32fe829a1c000000000000000000000000000001",
+ "32fe829a1c000000000000000000000000000100",
+ "32fe829a1d000000000000000000000000000000");
+ PackIndex idxTwo = indexWith(
+ // Noise
+ "8888880000000000000000000000000000000002",
+ "bbbbbb0000000000000000000000000000000003",
+ "32fe829a1c000000000000000000000000000010");
+
+ Map<String, PackIndex> packs = Map.of("p1", idxOne, "p2", idxTwo);
+ MultiPackIndexWriter writer = new MultiPackIndexWriter();
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ writer.write(NullProgressMonitor.INSTANCE, out, packs);
+ MultiPackIndex midx = MultiPackIndexLoader
+ .read(new ByteArrayInputStream(out.toByteArray()));
+
+
+ Set<ObjectId> results = new HashSet<>();
+ midx.resolve(results, abbrev, 200);
+
+ assertEquals(0, results.size());
+ }
+
+ @Test
+ public void jgit_resolve_empty() throws IOException {
+ AbbreviatedObjectId abbrev = AbbreviatedObjectId
+ .fromString("4400000000");
+
+ PackIndex idxOne = FakeIndexFactory.indexOf(List.of());
+ PackIndex idxTwo = FakeIndexFactory.indexOf(List.of());
+
+ Map<String, PackIndex> packs = Map.of("p1", idxOne, "p2", idxTwo);
+ MultiPackIndexWriter writer = new MultiPackIndexWriter();
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ writer.write(NullProgressMonitor.INSTANCE, out, packs);
+ MultiPackIndex midx = MultiPackIndexLoader
+ .read(new ByteArrayInputStream(out.toByteArray()));
+
+
+ Set<ObjectId> results = new HashSet<>();
+ midx.resolve(results, abbrev, 200);
+
+ assertEquals(0, results.size());
+ }
+
+ private static PackIndex indexWith(String... oids) {
+ List<FakeIndexFactory.IndexObject> idxObjs = new ArrayList<>(
+ oids.length);
+ int offset = 12;
+ for (String oid : oids) {
+ idxObjs.add(new FakeIndexFactory.IndexObject(oid, offset));
+ offset += 10;
+ }
+ return FakeIndexFactory.indexOf(idxObjs);
+ }
+
+ private static void assertInIndex(MultiPackIndex midx, int expectedPackId,
+ String oid, long expectedOffset) {
+ MultiPackIndex.PackOffset packOffset = midx
+ .find(ObjectId.fromString(oid));
+ assertNotNull(packOffset);
+ assertEquals("Wrong packId for " + oid, expectedPackId,
+ packOffset.getPackId());
+ assertEquals(expectedOffset, packOffset.getOffset());
+ }
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/MultiPackIndexWriterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/MultiPackIndexWriterTest.java
new file mode 100644
index 0000000000..8b57a2dcb4
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/MultiPackIndexWriterTest.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2025, Google LLC
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.internal.storage.midx;
+
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.CHUNK_LOOKUP_WIDTH;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_LARGEOFFSETS;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_OBJECTOFFSETS;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_OIDFANOUT;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_OIDLOOKUP;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_PACKNAMES;
+import static org.eclipse.jgit.internal.storage.midx.MultiPackIndexConstants.MIDX_CHUNKID_REVINDEX;
+import static org.junit.Assert.assertEquals;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jgit.internal.storage.file.PackIndex;
+import org.eclipse.jgit.junit.FakeIndexFactory;
+import org.eclipse.jgit.junit.FakeIndexFactory.IndexObject;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.util.NB;
+import org.junit.Test;
+
+public class MultiPackIndexWriterTest {
+
+ @Test
+ public void write_allSmallOffsets() throws IOException {
+ PackIndex index1 = indexOf(
+ object("0000000000000000000000000000000000000001", 500),
+ object("0000000000000000000000000000000000000003", 1500),
+ object("0000000000000000000000000000000000000005", 3000));
+ PackIndex index2 = indexOf(
+ object("0000000000000000000000000000000000000002", 500),
+ object("0000000000000000000000000000000000000004", 1500),
+ object("0000000000000000000000000000000000000006", 3000));
+
+ Map<String, PackIndex> data = Map.of("packname1", index1, "packname2",
+ index2);
+
+ MultiPackIndexWriter writer = new MultiPackIndexWriter();
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ writer.write(NullProgressMonitor.INSTANCE, out, data);
+ // header (12 bytes)
+ // + chunkHeader (6 * 12 bytes)
+ // + fanout table (256 * 4 bytes)
+ // + OIDs (6 * 20 bytes)
+ // + (pack, offset) pairs (6 * 8)
+ // + RIDX (6 * 4 bytes)
+ // + packfile names (2 * 10)
+ // + checksum (20)
+ assertEquals(1340, out.size());
+ List<Integer> chunkIds = readChunkIds(out);
+ assertEquals(5, chunkIds.size());
+ assertEquals(0, chunkIds.indexOf(MIDX_CHUNKID_OIDFANOUT));
+ assertEquals(1, chunkIds.indexOf(MIDX_CHUNKID_OIDLOOKUP));
+ assertEquals(2, chunkIds.indexOf(MIDX_CHUNKID_OBJECTOFFSETS));
+ assertEquals(3, chunkIds.indexOf(MIDX_CHUNKID_REVINDEX));
+ assertEquals(4, chunkIds.indexOf(MIDX_CHUNKID_PACKNAMES));
+ }
+
+ @Test
+ public void write_smallOffset_limit() throws IOException {
+ PackIndex index1 = indexOf(
+ object("0000000000000000000000000000000000000001", 500),
+ object("0000000000000000000000000000000000000003", 1500),
+ object("0000000000000000000000000000000000000005", (1L << 32) -1));
+ PackIndex index2 = indexOf(
+ object("0000000000000000000000000000000000000002", 500),
+ object("0000000000000000000000000000000000000004", 1500),
+ object("0000000000000000000000000000000000000006", 3000));
+ Map<String, PackIndex> data =
+ Map.of("packname1", index1, "packname2", index2);
+
+ MultiPackIndexWriter writer = new MultiPackIndexWriter();
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ writer.write(NullProgressMonitor.INSTANCE, out, data);
+ // header (12 bytes)
+ // + chunkHeader (6 * 12 bytes)
+ // + fanout table (256 * 4 bytes)
+ // + OIDs (6 * 20 bytes)
+ // + (pack, offset) pairs (6 * 8)
+ // + RIDX (6 * 4 bytes)
+ // + packfile names (2 * 10)
+ // + checksum (20)
+ assertEquals(1340, out.size());
+ List<Integer> chunkIds = readChunkIds(out);
+ assertEquals(5, chunkIds.size());
+ assertEquals(0, chunkIds.indexOf(MIDX_CHUNKID_OIDFANOUT));
+ assertEquals(1, chunkIds.indexOf(MIDX_CHUNKID_OIDLOOKUP));
+ assertEquals(2, chunkIds.indexOf(MIDX_CHUNKID_OBJECTOFFSETS));
+ assertEquals(3, chunkIds.indexOf(MIDX_CHUNKID_REVINDEX));
+ assertEquals(4, chunkIds.indexOf(MIDX_CHUNKID_PACKNAMES));
+ }
+
+ @Test
+ public void write_largeOffset() throws IOException {
+ PackIndex index1 = indexOf(
+ object("0000000000000000000000000000000000000001", 500),
+ object("0000000000000000000000000000000000000003", 1500),
+ object("0000000000000000000000000000000000000005", 1L << 32));
+ PackIndex index2 = indexOf(
+ object("0000000000000000000000000000000000000002", 500),
+ object("0000000000000000000000000000000000000004", 1500),
+ object("0000000000000000000000000000000000000006", 3000));
+ Map<String, PackIndex> data =
+ Map.of("packname1", index1, "packname2", index2);
+
+ MultiPackIndexWriter writer = new MultiPackIndexWriter();
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ writer.write(NullProgressMonitor.INSTANCE, out, data);
+ // header (12 bytes)
+ // + chunkHeader (7 * 12 bytes)
+ // + fanout table (256 * 4 bytes)
+ // + OIDs (6 * 20 bytes)
+ // + (pack, offset) pairs (6 * 8)
+ // + (large-offset) (1 * 8)
+ // + RIDX (6 * 4 bytes)
+ // + packfile names (2 * 10)
+ // + checksum (20)
+ assertEquals(1360, out.size());
+ List<Integer> chunkIds = readChunkIds(out);
+ assertEquals(6, chunkIds.size());
+ assertEquals(0, chunkIds.indexOf(MIDX_CHUNKID_OIDFANOUT));
+ assertEquals(1, chunkIds.indexOf(MIDX_CHUNKID_OIDLOOKUP));
+ assertEquals(2, chunkIds.indexOf(MIDX_CHUNKID_OBJECTOFFSETS));
+ assertEquals(3, chunkIds.indexOf(MIDX_CHUNKID_LARGEOFFSETS));
+ assertEquals(4, chunkIds.indexOf(MIDX_CHUNKID_REVINDEX));
+ assertEquals(5, chunkIds.indexOf(MIDX_CHUNKID_PACKNAMES));
+ }
+
+ @Test
+ public void jgit_emptyMidx() throws IOException {
+ PackIndex idxOne = FakeIndexFactory.indexOf(List.of());
+ PackIndex idxTwo = FakeIndexFactory.indexOf(List.of());
+ Map<String, PackIndex> packs = Map.of("p1", idxOne, "p2", idxTwo);
+ MultiPackIndexWriter writer = new MultiPackIndexWriter();
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ writer.write(NullProgressMonitor.INSTANCE, out, packs);
+ List<Integer> chunkIds = readChunkIds(out);
+ assertEquals(1134, out.size());
+ assertEquals(5, chunkIds.size());
+ assertEquals(0, chunkIds.indexOf(MIDX_CHUNKID_OIDFANOUT));
+ assertEquals(1, chunkIds.indexOf(MIDX_CHUNKID_OIDLOOKUP));
+ assertEquals(2, chunkIds.indexOf(MIDX_CHUNKID_OBJECTOFFSETS));
+ assertEquals(3, chunkIds.indexOf(MIDX_CHUNKID_REVINDEX));
+ assertEquals(4, chunkIds.indexOf(MIDX_CHUNKID_PACKNAMES));
+ }
+
+ private List<Integer> readChunkIds(ByteArrayOutputStream out) {
+ List<Integer> chunkIds = new ArrayList<>();
+ byte[] raw = out.toByteArray();
+ int numChunks = raw[6];
+ int position = 12;
+ for (int i = 0; i < numChunks; i++) {
+ chunkIds.add(NB.decodeInt32(raw, position));
+ position += CHUNK_LOOKUP_WIDTH;
+ }
+ return chunkIds;
+ }
+
+ private static PackIndex indexOf(IndexObject... objs) {
+ return FakeIndexFactory.indexOf(Arrays.asList(objs));
+ }
+
+ private static IndexObject object(String name, long offset) {
+ return new IndexObject(name, offset);
+ }
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/PackIndexMergerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/PackIndexMergerTest.java
new file mode 100644
index 0000000000..8218cbc20d
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/PackIndexMergerTest.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2025, Google LLC
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.internal.storage.midx;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.eclipse.jgit.internal.storage.file.PackIndex;
+import org.eclipse.jgit.junit.FakeIndexFactory;
+import org.eclipse.jgit.junit.FakeIndexFactory.IndexObject;
+import org.junit.Test;
+
+public class PackIndexMergerTest {
+
+ @Test
+ public void rawIterator_noDuplicates() {
+ PackIndex idxOne = indexOf(
+ oidOffset("0000000000000000000000000000000000000001", 500),
+ oidOffset("0000000000000000000000000000000000000005", 12),
+ oidOffset("0000000000000000000000000000000000000010", 1500));
+ PackIndex idxTwo = indexOf(
+ oidOffset("0000000000000000000000000000000000000002", 501),
+ oidOffset("0000000000000000000000000000000000000003", 13),
+ oidOffset("0000000000000000000000000000000000000015", 1501));
+ PackIndex idxThree = indexOf(
+ oidOffset("0000000000000000000000000000000000000004", 502),
+ oidOffset("0000000000000000000000000000000000000007", 14),
+ oidOffset("0000000000000000000000000000000000000012", 1502));
+ PackIndexMerger merger = new PackIndexMerger(
+ Map.of("p1", idxOne, "p2", idxTwo, "p3", idxThree));
+ assertEquals(9, merger.getUniqueObjectCount());
+ assertEquals(3, merger.getPackCount());
+ assertFalse(merger.needsLargeOffsetsChunk());
+ Iterator<PackIndexMerger.MidxMutableEntry> it = merger.rawIterator();
+ assertNextEntry(it, "0000000000000000000000000000000000000001", 0, 500);
+ assertNextEntry(it, "0000000000000000000000000000000000000002", 1, 501);
+ assertNextEntry(it, "0000000000000000000000000000000000000003", 1, 13);
+ assertNextEntry(it, "0000000000000000000000000000000000000004", 2, 502);
+ assertNextEntry(it, "0000000000000000000000000000000000000005", 0, 12);
+ assertNextEntry(it, "0000000000000000000000000000000000000007", 2, 14);
+ assertNextEntry(it, "0000000000000000000000000000000000000010", 0,
+ 1500);
+ assertNextEntry(it, "0000000000000000000000000000000000000012", 2,
+ 1502);
+ assertNextEntry(it, "0000000000000000000000000000000000000015", 1,
+ 1501);
+ assertFalse(it.hasNext());
+ }
+
+ @Test
+ public void rawIterator_allDuplicates() {
+ PackIndex idxOne = indexOf(
+ oidOffset("0000000000000000000000000000000000000001", 500),
+ oidOffset("0000000000000000000000000000000000000005", 12),
+ oidOffset("0000000000000000000000000000000000000010", 1500));
+ PackIndexMerger merger = new PackIndexMerger(
+ Map.of("p1", idxOne, "p2", idxOne, "p3", idxOne));
+ assertEquals(3, merger.getUniqueObjectCount());
+ assertEquals(3, merger.getPackCount());
+ assertFalse(merger.needsLargeOffsetsChunk());
+ Iterator<PackIndexMerger.MidxMutableEntry> it = merger.rawIterator();
+ assertNextEntry(it, "0000000000000000000000000000000000000001", 0, 500);
+ assertNextEntry(it, "0000000000000000000000000000000000000001", 1, 500);
+ assertNextEntry(it, "0000000000000000000000000000000000000001", 2, 500);
+ assertNextEntry(it, "0000000000000000000000000000000000000005", 0, 12);
+ assertNextEntry(it, "0000000000000000000000000000000000000005", 1, 12);
+ assertNextEntry(it, "0000000000000000000000000000000000000005", 2, 12);
+ assertNextEntry(it, "0000000000000000000000000000000000000010", 0,
+ 1500);
+ assertNextEntry(it, "0000000000000000000000000000000000000010", 1,
+ 1500);
+ assertNextEntry(it, "0000000000000000000000000000000000000010", 2,
+ 1500);
+ assertFalse(it.hasNext());
+ }
+
+ @Test
+ public void bySha1Iterator_noDuplicates() {
+ PackIndex idxOne = indexOf(
+ oidOffset("0000000000000000000000000000000000000001", 500),
+ oidOffset("0000000000000000000000000000000000000005", 12),
+ oidOffset("0000000000000000000000000000000000000010", 1500));
+ PackIndex idxTwo = indexOf(
+ oidOffset("0000000000000000000000000000000000000002", 501),
+ oidOffset("0000000000000000000000000000000000000003", 13),
+ oidOffset("0000000000000000000000000000000000000015", 1501));
+ PackIndex idxThree = indexOf(
+ oidOffset("0000000000000000000000000000000000000004", 502),
+ oidOffset("0000000000000000000000000000000000000007", 14),
+ oidOffset("0000000000000000000000000000000000000012", 1502));
+ PackIndexMerger merger = new PackIndexMerger(
+ Map.of("p1", idxOne, "p2", idxTwo, "p3", idxThree));
+ assertEquals(9, merger.getUniqueObjectCount());
+ assertEquals(3, merger.getPackCount());
+ assertFalse(merger.needsLargeOffsetsChunk());
+ Iterator<PackIndexMerger.MidxMutableEntry> it = merger.bySha1Iterator();
+ assertNextEntry(it, "0000000000000000000000000000000000000001", 0, 500);
+ assertNextEntry(it, "0000000000000000000000000000000000000002", 1, 501);
+ assertNextEntry(it, "0000000000000000000000000000000000000003", 1, 13);
+ assertNextEntry(it, "0000000000000000000000000000000000000004", 2, 502);
+ assertNextEntry(it, "0000000000000000000000000000000000000005", 0, 12);
+ assertNextEntry(it, "0000000000000000000000000000000000000007", 2, 14);
+ assertNextEntry(it, "0000000000000000000000000000000000000010", 0,
+ 1500);
+ assertNextEntry(it, "0000000000000000000000000000000000000012", 2,
+ 1502);
+ assertNextEntry(it, "0000000000000000000000000000000000000015", 1,
+ 1501);
+ assertFalse(it.hasNext());
+ }
+
+ @Test
+ public void bySha1Iterator_allDuplicates() {
+ PackIndex idxOne = indexOf(
+ oidOffset("0000000000000000000000000000000000000001", 500),
+ oidOffset("0000000000000000000000000000000000000005", 12),
+ oidOffset("0000000000000000000000000000000000000010", 1500));
+ PackIndexMerger merger = new PackIndexMerger(
+ Map.of("p1", idxOne, "p2", idxOne, "p3", idxOne));
+ assertEquals(3, merger.getUniqueObjectCount());
+ assertEquals(3, merger.getPackCount());
+ assertFalse(merger.needsLargeOffsetsChunk());
+ Iterator<PackIndexMerger.MidxMutableEntry> it = merger.bySha1Iterator();
+ assertNextEntry(it, "0000000000000000000000000000000000000001", 0, 500);
+ assertNextEntry(it, "0000000000000000000000000000000000000005", 0, 12);
+ assertNextEntry(it, "0000000000000000000000000000000000000010", 0,
+ 1500);
+ assertFalse(it.hasNext());
+ }
+
+ @Test
+ public void bySha1Iterator_differentIndexSizes() {
+ PackIndex idxOne = indexOf(
+ oidOffset("0000000000000000000000000000000000000010", 1500));
+ PackIndex idxTwo = indexOf(
+ oidOffset("0000000000000000000000000000000000000002", 500),
+ oidOffset("0000000000000000000000000000000000000003", 12));
+ PackIndex idxThree = indexOf(
+ oidOffset("0000000000000000000000000000000000000004", 500),
+ oidOffset("0000000000000000000000000000000000000007", 12),
+ oidOffset("0000000000000000000000000000000000000012", 1500));
+ PackIndexMerger merger = new PackIndexMerger(
+ Map.of("p1", idxOne, "p2", idxTwo, "p3", idxThree));
+ assertEquals(6, merger.getUniqueObjectCount());
+ assertEquals(3, merger.getPackCount());
+ assertFalse(merger.needsLargeOffsetsChunk());
+ Iterator<PackIndexMerger.MidxMutableEntry> it = merger.bySha1Iterator();
+ assertNextEntry(it, "0000000000000000000000000000000000000002", 1, 500);
+ assertNextEntry(it, "0000000000000000000000000000000000000003", 1, 12);
+ assertNextEntry(it, "0000000000000000000000000000000000000004", 2, 500);
+ assertNextEntry(it, "0000000000000000000000000000000000000007", 2, 12);
+ assertNextEntry(it, "0000000000000000000000000000000000000010", 0,
+ 1500);
+ assertNextEntry(it, "0000000000000000000000000000000000000012", 2,
+ 1500);
+ assertFalse(it.hasNext());
+ }
+
+ @Test
+ public void merger_noIndexes() {
+ PackIndexMerger merger = new PackIndexMerger(Map.of());
+ assertEquals(0, merger.getUniqueObjectCount());
+ assertFalse(merger.needsLargeOffsetsChunk());
+ assertTrue(merger.getPackNames().isEmpty());
+ assertEquals(0, merger.getPackCount());
+ assertFalse(merger.bySha1Iterator().hasNext());
+ }
+
+ @Test
+ public void merger_emptyIndexes() {
+ PackIndexMerger merger = new PackIndexMerger(
+ Map.of("p1", indexOf(), "p2", indexOf()));
+ assertEquals(0, merger.getUniqueObjectCount());
+ assertFalse(merger.needsLargeOffsetsChunk());
+ assertEquals(2, merger.getPackNames().size());
+ assertEquals(2, merger.getPackCount());
+ assertFalse(merger.bySha1Iterator().hasNext());
+ }
+
+ @Test
+ public void bySha1Iterator_largeOffsets_needsChunk() {
+ PackIndex idx1 = indexOf(
+ oidOffset("0000000000000000000000000000000000000002", 1L << 32),
+ oidOffset("0000000000000000000000000000000000000004", 12));
+ PackIndex idx2 = indexOf(oidOffset(
+ "0000000000000000000000000000000000000003", (1L << 31) + 10));
+ PackIndexMerger merger = new PackIndexMerger(
+ Map.of("p1", idx1, "p2", idx2));
+ assertTrue(merger.needsLargeOffsetsChunk());
+ assertEquals(2, merger.getOffsetsOver31BitsCount());
+ assertEquals(3, merger.getUniqueObjectCount());
+ }
+
+ @Test
+ public void bySha1Iterator_largeOffsets_noChunk() {
+ // If no value is over 2^32-1, then we don't need large offset
+ PackIndex idx1 = indexOf(
+ oidOffset("0000000000000000000000000000000000000002",
+ (1L << 31) + 15),
+ oidOffset("0000000000000000000000000000000000000004", 12));
+ PackIndex idx2 = indexOf(oidOffset(
+ "0000000000000000000000000000000000000003", (1L << 31) + 10));
+ PackIndexMerger merger = new PackIndexMerger(
+ Map.of("p1", idx1, "p2", idx2));
+ assertFalse(merger.needsLargeOffsetsChunk());
+ assertEquals(2, merger.getOffsetsOver31BitsCount());
+ assertEquals(3, merger.getUniqueObjectCount());
+ }
+
+ private static void assertNextEntry(
+ Iterator<PackIndexMerger.MidxMutableEntry> it, String oid,
+ int packId, long offset) {
+ assertTrue(it.hasNext());
+ PackIndexMerger.MidxMutableEntry e = it.next();
+ assertEquals(oid, e.getObjectId().name());
+ assertEquals(packId, e.getPackId());
+ assertEquals(offset, e.getOffset());
+ }
+
+ private static IndexObject oidOffset(String oid, long offset) {
+ return new IndexObject(oid, offset);
+ }
+
+ private static PackIndex indexOf(IndexObject... objs) {
+ return FakeIndexFactory.indexOf(Arrays.asList(objs));
+ }
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/PackIndexPeekIteratorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/PackIndexPeekIteratorTest.java
new file mode 100644
index 0000000000..0b3ccacfc1
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/midx/PackIndexPeekIteratorTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2025, Google LLC
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.internal.storage.midx;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import java.util.Arrays;
+
+import org.eclipse.jgit.internal.storage.file.PackIndex;
+import org.eclipse.jgit.junit.FakeIndexFactory;
+import org.junit.Test;
+
+public class PackIndexPeekIteratorTest {
+ @Test
+ public void next() {
+ PackIndex index1 = indexOf(
+ object("0000000000000000000000000000000000000001", 500),
+ object("0000000000000000000000000000000000000003", 1500),
+ object("0000000000000000000000000000000000000005", 3000));
+ PackIndexMerger.PackIndexPeekIterator it = new PackIndexMerger.PackIndexPeekIterator(0, index1);
+ assertEquals("0000000000000000000000000000000000000001", it.next().name());
+ assertEquals("0000000000000000000000000000000000000003", it.next().name());
+ assertEquals("0000000000000000000000000000000000000005", it.next().name());
+ assertNull(it.next());
+ }
+
+ @Test
+ public void peek_doesNotAdvance() {
+ PackIndex index1 = indexOf(
+ object("0000000000000000000000000000000000000001", 500),
+ object("0000000000000000000000000000000000000003", 1500),
+ object("0000000000000000000000000000000000000005", 3000));
+ PackIndexMerger.PackIndexPeekIterator it = new PackIndexMerger.PackIndexPeekIterator(0, index1);
+ it.next();
+ assertEquals("0000000000000000000000000000000000000001", it.peek().name());
+ assertEquals("0000000000000000000000000000000000000001", it.peek().name());
+ it.next();
+ assertEquals("0000000000000000000000000000000000000003", it.peek().name());
+ assertEquals("0000000000000000000000000000000000000003", it.peek().name());
+ it.next();
+ assertEquals("0000000000000000000000000000000000000005", it.peek().name());
+ assertEquals("0000000000000000000000000000000000000005", it.peek().name());
+ it.next();
+ assertNull(it.peek());
+ assertNull(it.peek());
+ }
+
+ @Test
+ public void empty() {
+ PackIndex index1 = indexOf();
+ PackIndexMerger.PackIndexPeekIterator it = new PackIndexMerger.PackIndexPeekIterator(0, index1);
+ assertNull(it.next());
+ assertNull(it.peek());
+ }
+
+ private static PackIndex indexOf(FakeIndexFactory.IndexObject... objs) {
+ return FakeIndexFactory.indexOf(Arrays.asList(objs));
+ }
+
+ private static FakeIndexFactory.IndexObject object(String name, long offset) {
+ return new FakeIndexFactory.IndexObject(name, offset);
+ }
+} \ No newline at end of file
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/pack/GcCommitSelectionTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/pack/GcCommitSelectionTest.java
index 0c09ad1510..ecf9a15b03 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/pack/GcCommitSelectionTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/pack/GcCommitSelectionTest.java
@@ -110,7 +110,7 @@ public class GcCommitSelectionTest extends GcTestCase {
tr.branch(mainBranch).update(commit1);
gc.setExpireAgeMillis(0);
- gc.gc();
+ gc.gc().get();
// Create only 2 bitmaps, for commit0 and commit1, excluding commit2
assertEquals(2, gc.getStatistics().numberOfBitmaps);
@@ -227,7 +227,7 @@ public class GcCommitSelectionTest extends GcTestCase {
PackConfig packConfig = new PackConfig();
packConfig.setBitmapExcludedRefsPrefixes(new String[] { "refs/heads/other" });
gc.setPackConfig(packConfig);
- gc.gc();
+ gc.gc().get();
assertEquals(1,
gc.getStatistics().numberOfBitmaps);
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/ReftableCompactorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/ReftableCompactorTest.java
index 6fc7f25475..62dbda47fd 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/ReftableCompactorTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/ReftableCompactorTest.java
@@ -19,6 +19,8 @@ import static org.junit.Assert.assertTrue;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.time.Instant;
+import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -27,6 +29,7 @@ import org.eclipse.jgit.internal.storage.io.BlockSource;
import org.eclipse.jgit.internal.storage.reftable.ReftableWriter.Stats;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.junit.Test;
@@ -279,6 +282,95 @@ public class ReftableCompactorTest {
}
}
+ @Test
+ public void reflog_all() throws IOException {
+ byte[] inTab;
+ try (ByteArrayOutputStream inBuf = new ByteArrayOutputStream()) {
+ ReftableWriter writer = new ReftableWriter(inBuf)
+ .setMinUpdateIndex(0).setMaxUpdateIndex(2).begin();
+ writer.writeLog(MASTER, 2, person(Instant.ofEpochSecond(500)),
+ id(3), id(4), null);
+ writer.writeLog(MASTER, 1, person(Instant.ofEpochSecond(300)),
+ id(2), id(3), null);
+ writer.writeLog(MASTER, 0, person(Instant.ofEpochSecond(100)),
+ id(1), id(2), null);
+ writer.finish();
+ inTab = inBuf.toByteArray();
+ }
+
+ ReftableCompactor compactor;
+ try (ByteArrayOutputStream outBuf = new ByteArrayOutputStream()) {
+ compactor = new ReftableCompactor(outBuf);
+ // No setReflogExpire time is set
+ List<ReftableReader> readers = new ArrayList<>();
+ readers.add(read(inTab));
+ compactor.addAll(readers);
+ compactor.compact();
+ }
+ Stats stats = compactor.getStats();
+ assertEquals(3, stats.logCount());
+ }
+
+ @Test
+ public void reflog_setExpireOlderThan() throws IOException {
+ byte[] inTab;
+ try (ByteArrayOutputStream inBuf = new ByteArrayOutputStream()) {
+ ReftableWriter writer = new ReftableWriter(inBuf)
+ .setMinUpdateIndex(0).setMaxUpdateIndex(2).begin();
+ writer.writeLog(MASTER, 2, person(Instant.ofEpochSecond(500)),
+ id(3), id(4), null);
+ writer.writeLog(MASTER, 1, person(Instant.ofEpochSecond(300)),
+ id(2), id(3), null);
+ writer.writeLog(MASTER, 0, person(Instant.ofEpochSecond(100)),
+ id(1), id(2), null);
+ writer.finish();
+ inTab = inBuf.toByteArray();
+ }
+
+ ReftableCompactor compactor;
+ try (ByteArrayOutputStream outBuf = new ByteArrayOutputStream()) {
+ compactor = new ReftableCompactor(outBuf);
+ compactor.setReflogExpireOlderThan(Instant.ofEpochSecond(300));
+ List<ReftableReader> readers = new ArrayList<>();
+ readers.add(read(inTab));
+ compactor.addAll(readers);
+ compactor.compact();
+ }
+
+ Stats stats = compactor.getStats();
+ assertEquals(2, stats.logCount());
+ }
+
+ @Test
+ public void reflog_disable() throws IOException {
+ byte[] inTab;
+ try (ByteArrayOutputStream inBuf = new ByteArrayOutputStream()) {
+ ReftableWriter writer = new ReftableWriter(inBuf)
+ .setMinUpdateIndex(0).setMaxUpdateIndex(2).begin();
+ writer.writeLog(MASTER, 2, person(Instant.ofEpochSecond(500)),
+ id(3), id(4), null);
+ writer.writeLog(MASTER, 1, person(Instant.ofEpochSecond(300)),
+ id(2), id(3), null);
+ writer.writeLog(MASTER, 0, person(Instant.ofEpochSecond(100)),
+ id(1), id(2), null);
+ writer.finish();
+ inTab = inBuf.toByteArray();
+ }
+
+ ReftableCompactor compactor;
+ try (ByteArrayOutputStream outBuf = new ByteArrayOutputStream()) {
+ compactor = new ReftableCompactor(outBuf);
+ compactor.setReflogExpireOlderThan(Instant.MAX);
+ List<ReftableReader> readers = new ArrayList<>();
+ readers.add(read(inTab));
+ compactor.addAll(readers);
+ compactor.compact();
+ }
+
+ Stats stats = compactor.getStats();
+ assertEquals(0, stats.logCount());
+ }
+
private static Ref ref(String name, int id) {
return new ObjectIdRef.PeeledNonTag(PACKED, name, id(id));
}
@@ -296,6 +388,10 @@ public class ReftableCompactorTest {
return ObjectId.fromRaw(buf);
}
+ private static PersonIdent person(Instant when) {
+ return new PersonIdent("a. u. thor", "author@jgit.com", when, ZoneId.systemDefault());
+ }
+
private static ReftableReader read(byte[] table) {
return new ReftableReader(BlockSource.from(table));
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/ReftableTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/ReftableTest.java
index ea0d92acfd..a54002bc74 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/ReftableTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/ReftableTest.java
@@ -29,6 +29,8 @@ import static org.junit.Assert.fail;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.time.Instant;
+import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -175,7 +177,8 @@ public class ReftableTest {
@Test
public void hasObjLogs() throws IOException {
- PersonIdent who = new PersonIdent("Log", "Ger", 1500079709, -8 * 60);
+ PersonIdent who = new PersonIdent("Log", "Ger",
+ Instant.ofEpochMilli(1500079709), ZoneOffset.ofHours(-8));
String msg = "test";
ReftableConfig cfg = new ReftableConfig();
cfg.setIndexObjects(false);
@@ -617,7 +620,8 @@ public class ReftableTest {
.setMinUpdateIndex(1)
.setMaxUpdateIndex(2)
.begin();
- PersonIdent who = new PersonIdent("Log", "Ger", 1500079709, -8 * 60);
+ PersonIdent who = new PersonIdent("Log", "Ger",
+ Instant.ofEpochMilli(1500079709), ZoneOffset.ofHours(-8));
String msg = "test";
writer.writeLog(MASTER, 1, who, ObjectId.zeroId(), id(1), msg);
@@ -633,7 +637,8 @@ public class ReftableTest {
.setMinUpdateIndex(1)
.setMaxUpdateIndex(1)
.begin();
- PersonIdent who = new PersonIdent("Log", "Ger", 1500079709, -8 * 60);
+ PersonIdent who = new PersonIdent("Log", "Ger",
+ Instant.ofEpochMilli(1500079709), ZoneOffset.ofHours(-8));
String msg = "test";
writer.writeLog(NEXT, 1, who, ObjectId.zeroId(), id(1), msg);
@@ -647,7 +652,8 @@ public class ReftableTest {
public void withReflog() throws IOException {
Ref master = ref(MASTER, 1);
Ref next = ref(NEXT, 2);
- PersonIdent who = new PersonIdent("Log", "Ger", 1500079709, -8 * 60);
+ PersonIdent who = new PersonIdent("Log", "Ger",
+ Instant.ofEpochMilli(1500079709), ZoneOffset.ofHours(-8));
String msg = "test";
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
@@ -712,11 +718,14 @@ public class ReftableTest {
writer.writeRef(master);
writer.writeRef(next);
- PersonIdent who1 = new PersonIdent("Log", "Ger", 1500079709, -8 * 60);
+ PersonIdent who1 = new PersonIdent("Log", "Ger",
+ Instant.ofEpochMilli(1500079709), ZoneOffset.ofHours(-8));
writer.writeLog(MASTER, 3, who1, ObjectId.zeroId(), id(1), "1");
- PersonIdent who2 = new PersonIdent("Log", "Ger", 1500079710, -8 * 60);
+ PersonIdent who2 = new PersonIdent("Log", "Ger",
+ Instant.ofEpochMilli(1500079709), ZoneOffset.ofHours(-8));
writer.writeLog(MASTER, 2, who2, id(1), id(2), "2");
- PersonIdent who3 = new PersonIdent("Log", "Ger", 1500079711, -8 * 60);
+ PersonIdent who3 = new PersonIdent("Log", "Ger",
+ Instant.ofEpochMilli(1500079709), ZoneOffset.ofHours(-8));
writer.writeLog(MASTER, 1, who3, id(2), id(3), "3");
writer.finish();
@@ -753,7 +762,8 @@ public class ReftableTest {
.setMaxUpdateIndex(1)
.setConfig(cfg)
.begin();
- PersonIdent who = new PersonIdent("Log", "Ger", 1500079709, -8 * 60);
+ PersonIdent who = new PersonIdent("Log", "Ger",
+ Instant.ofEpochMilli(1500079709), ZoneOffset.ofHours(-8));
// Fill out the 1st ref block.
List<String> names = new ArrayList<>();
@@ -782,7 +792,8 @@ public class ReftableTest {
@Test
public void reflogSeek() throws IOException {
- PersonIdent who = new PersonIdent("Log", "Ger", 1500079709, -8 * 60);
+ PersonIdent who = new PersonIdent("Log", "Ger",
+ Instant.ofEpochSecond(1500079709), ZoneOffset.ofHours(-8));
String msg = "test";
String msgNext = "test next";
@@ -827,7 +838,8 @@ public class ReftableTest {
@Test
public void reflogSeekPrefix() throws IOException {
- PersonIdent who = new PersonIdent("Log", "Ger", 1500079709, -8 * 60);
+ PersonIdent who = new PersonIdent("Log", "Ger",
+ Instant.ofEpochMilli(1500079709), ZoneOffset.ofHours(-8));
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
ReftableWriter writer = new ReftableWriter(buffer)
@@ -850,7 +862,8 @@ public class ReftableTest {
@Test
public void onlyReflog() throws IOException {
- PersonIdent who = new PersonIdent("Log", "Ger", 1500079709, -8 * 60);
+ PersonIdent who = new PersonIdent("Log", "Ger",
+ Instant.ofEpochMilli(1500079709), ZoneOffset.ofHours(-8));
String msg = "test";
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
@@ -916,7 +929,8 @@ public class ReftableTest {
writer.writeRef(ref);
}
- PersonIdent who = new PersonIdent("Log", "Ger", 1500079709, -8 * 60);
+ PersonIdent who = new PersonIdent("Log", "Ger",
+ Instant.ofEpochMilli(1500079709), ZoneOffset.ofHours(-8));
for (Ref ref : refs) {
writer.writeLog(ref.getName(), 1, who,
ObjectId.zeroId(), ref.getObjectId(),
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/junit/TestRepositoryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/junit/TestRepositoryTest.java
index 450b753d94..1581d49797 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/junit/TestRepositoryTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/junit/TestRepositoryTest.java
@@ -11,6 +11,7 @@
package org.eclipse.jgit.junit;
import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.time.Instant.EPOCH;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
@@ -18,7 +19,6 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
-import java.util.Date;
import java.util.regex.Pattern;
import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
@@ -199,8 +199,8 @@ public class TestRepositoryTest {
assertEquals(orig.getAuthorIdent(), amended.getAuthorIdent());
// Committer name/email is the same, but time was incremented.
- assertEquals(new PersonIdent(orig.getCommitterIdent(), new Date(0)),
- new PersonIdent(amended.getCommitterIdent(), new Date(0)));
+ assertEquals(new PersonIdent(orig.getCommitterIdent(), EPOCH),
+ new PersonIdent(amended.getCommitterIdent(), EPOCH));
assertTrue(orig.getCommitTime() < amended.getCommitTime());
assertEquals("foo contents", blobAsString(amended, "foo"));
@@ -275,9 +275,9 @@ public class TestRepositoryTest {
RevCommit toPick = tr.commit()
.parent(tr.commit().create()) // Can't cherry-pick root.
.author(new PersonIdent("Cherrypick Author", "cpa@example.com",
- tr.getDate(), tr.getTimeZone()))
+ tr.getInstant(), tr.getTimeZoneId()))
.author(new PersonIdent("Cherrypick Committer", "cpc@example.com",
- tr.getDate(), tr.getTimeZone()))
+ tr.getInstant(), tr.getTimeZoneId()))
.message("message to cherry-pick")
.add("bar", "bar contents\n")
.create();
@@ -294,8 +294,8 @@ public class TestRepositoryTest {
assertEquals(toPick.getAuthorIdent(), result.getAuthorIdent());
// Committer name/email matches default, and time was incremented.
- assertEquals(new PersonIdent(head.getCommitterIdent(), new Date(0)),
- new PersonIdent(result.getCommitterIdent(), new Date(0)));
+ assertEquals(new PersonIdent(head.getCommitterIdent(), EPOCH),
+ new PersonIdent(result.getCommitterIdent(), EPOCH));
assertTrue(toPick.getCommitTime() < result.getCommitTime());
assertEquals("message to cherry-pick", result.getFullMessage());
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/BitmapIndexTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/BitmapIndexTest.java
new file mode 100644
index 0000000000..ee4fa8bcc7
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/BitmapIndexTest.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2024, Google Inc. and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.lib;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.internal.storage.file.GC;
+import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.junit.Before;
+import org.junit.Test;
+
+public class BitmapIndexTest extends LocalDiskRepositoryTestCase {
+
+ private static final String MAIN = "refs/heads/main";
+
+ TestRepository<FileRepository> repo;
+
+ RevCommit tipWithBitmap;
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ FileRepository db = createWorkRepository();
+ repo = new TestRepository<>(db);
+
+ RevCommit base = repo.commit().create();
+ RevCommit one = repo.commit().parent(base).create();
+ tipWithBitmap = repo.commit().parent(one).create();
+ repo.update(MAIN, tipWithBitmap);
+
+ GC gc = new GC(repo.getRepository());
+ gc.setAuto(false);
+ gc.gc().get();
+
+ assertNotNull(repo.getRevWalk().getObjectReader().getBitmapIndex());
+ }
+
+
+ @Test
+ public void listener_getBitmap_counted() throws Exception {
+ try (RevWalk rw = repo.getRevWalk();
+ ObjectReader or = rw.getObjectReader()) {
+ BitmapLookupCounter counter = new BitmapLookupCounter();
+ BitmapIndex bitmapIndex = or.getBitmapIndex();
+ bitmapIndex.addBitmapLookupListener(counter);
+
+ bitmapIndex.getBitmap(tipWithBitmap);
+ bitmapIndex.getBitmap(tipWithBitmap);
+ bitmapIndex.getBitmap(ObjectId.zeroId());
+
+ assertEquals(2, counter.bitmapFound);
+ assertEquals(1, counter.bitmapNotFound);
+ }
+ }
+
+ private static class BitmapLookupCounter
+ implements BitmapIndex.BitmapLookupListener {
+ int bitmapFound = 0;
+
+ int bitmapNotFound = 0;
+
+ @Override
+ public void onBitmapFound(AnyObjectId oid) {
+ bitmapFound += 1;
+ }
+
+ @Override
+ public void onBitmapNotFound(AnyObjectId oid) {
+ bitmapNotFound += 1;
+ }
+ }
+} \ No newline at end of file
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitTemplateConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitTemplateConfigTest.java
index 42bafb60ca..3ccd0ef021 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitTemplateConfigTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitTemplateConfigTest.java
@@ -17,7 +17,9 @@ import java.io.IOException;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.junit.JGitTestUtil;
+import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
+import org.eclipse.jgit.util.FS;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -27,7 +29,7 @@ import org.junit.rules.TemporaryFolder;
* test using bazel which doesn't allow tests to create files in the home
* directory
*/
-public class CommitTemplateConfigTest {
+public class CommitTemplateConfigTest extends LocalDiskRepositoryTestCase {
@Rule
public TemporaryFolder tmp = new TemporaryFolder();
@@ -42,9 +44,11 @@ public class CommitTemplateConfigTest {
String templateContent = "content of the template";
JGitTestUtil.write(tempFile, templateContent);
// proper evaluation of the ~/ directory
- String homeDir = System.getProperty("user.home");
+ File homeDir = FS.DETECTED.userHome();
File tempFileInHomeDirectory = File.createTempFile("fileInHomeFolder",
- ".tmp", new File(homeDir));
+ ".tmp", homeDir);
+ // The home directory should be a mocked temporary directory, but
+ // still...
tempFileInHomeDirectory.deleteOnExit();
JGitTestUtil.write(tempFileInHomeDirectory, templateContent);
String expectedTemplatePath = "~/" + tempFileInHomeDirectory.getName();
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java
index 8f9d105319..06fee8ea71 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java
@@ -42,7 +42,6 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
-import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@@ -124,16 +123,16 @@ public class ConfigTest {
@Test
public void test005_PutGetStringList() {
Config c = new Config();
- final LinkedList<String> values = new LinkedList<>();
+ List<String> values = new ArrayList<>();
values.add("value1");
values.add("value2");
c.setStringList("my", null, "somename", values);
- final Object[] expArr = values.toArray();
- final String[] actArr = c.getStringList("my", null, "somename");
+ Object[] expArr = values.toArray();
+ String[] actArr = c.getStringList("my", null, "somename");
assertArrayEquals(expArr, actArr);
- final String expText = "[my]\n\tsomename = value1\n\tsomename = value2\n";
+ String expText = "[my]\n\tsomename = value1\n\tsomename = value2\n";
assertEquals(expText, c.toText());
}
@@ -507,6 +506,35 @@ public class ConfigTest {
}
@Test
+ public void testRemoveBranchSection() throws ConfigInvalidException {
+ Config c = parse("" //
+ + "[branch \"keep\"]\n"
+ + " merge = master.branch.to.keep.in.the.file\n"
+ + "\n"
+ + "[branch \"remove\"]\n"
+ + " merge = this.will.get.deleted\n"
+ + " remote = origin-for-some-long-gone-place\n"
+ + "\n"
+ + "\n"
+ + "[core-section-not-to-remove-in-test]\n"
+ + " packedGitLimit = 14\n"
+ + "\n"
+ + "[other]\n"
+ + " foo = bar\n");
+ assertFalse(c.removeSection("branch", "does.not.exist"));
+ assertTrue(c.removeSection("branch", "remove"));
+ assertEquals("" //
+ + "[branch \"keep\"]\n"
+ + " merge = master.branch.to.keep.in.the.file\n"
+ + "\n"
+ + "[core-section-not-to-remove-in-test]\n"
+ + " packedGitLimit = 14\n"
+ + "\n"
+ + "[other]\n"
+ + " foo = bar\n", c.toText());
+ }
+
+ @Test
public void testUnsetBranchSection() throws ConfigInvalidException {
Config c = parse("" //
+ "[branch \"keep\"]\n"
@@ -516,8 +544,12 @@ public class ConfigTest {
+ " merge = this.will.get.deleted\n"
+ " remote = origin-for-some-long-gone-place\n"
+ "\n"
+ + "\n"
+ "[core-section-not-to-remove-in-test]\n"
- + " packedGitLimit = 14\n");
+ + " packedGitLimit = 14\n"
+ + "\n"
+ + "[other]\n"
+ + " foo = bar\n");
c.unsetSection("branch", "does.not.exist");
c.unsetSection("branch", "remove");
assertEquals("" //
@@ -525,7 +557,10 @@ public class ConfigTest {
+ " merge = master.branch.to.keep.in.the.file\n"
+ "\n"
+ "[core-section-not-to-remove-in-test]\n"
- + " packedGitLimit = 14\n", c.toText());
+ + " packedGitLimit = 14\n"
+ + "\n"
+ + "[other]\n"
+ + " foo = bar\n", c.toText());
}
@Test
@@ -1482,7 +1517,9 @@ public class ConfigTest {
File workTree = tmp.newFolder("dummy-worktree");
File tempFile = tmp.newFile("testCommitTemplate-");
- Repository repo = FileRepositoryBuilder.create(workTree);
+ Repository repo = FileRepositoryBuilder
+ .create(new File(workTree, ".git"));
+ repo.create();
String templateContent = "content of the template";
JGitTestUtil.write(tempFile, templateContent);
String expectedTemplatePath = tempFile.getPath();
@@ -1530,14 +1567,15 @@ public class ConfigTest {
@Test
public void testCommitTemplateEncoding()
throws ConfigInvalidException, IOException {
- Config config = new Config(null);
File workTree = tmp.newFolder("dummy-worktree");
- Repository repo = FileRepositoryBuilder.create(workTree);
+ Repository repo = FileRepositoryBuilder
+ .create(new File(workTree, ".git"));
+ repo.create();
File tempFile = tmp.newFile("testCommitTemplate-");
String templateContent = "content of the template";
JGitTestUtil.write(tempFile, templateContent);
String expectedTemplatePath = tempFile.getPath();
- config = parse("[i18n]\n\tcommitEncoding = utf-8\n"
+ Config config = parse("[i18n]\n\tcommitEncoding = utf-8\n"
+ "[commit]\n\ttemplate = "
+ Config.escapeValue(expectedTemplatePath) + "\n");
assertEquals(templateContent,
@@ -1551,13 +1589,14 @@ public class ConfigTest {
@Test(expected = ConfigInvalidException.class)
public void testCommitTemplateWithInvalidEncoding()
throws ConfigInvalidException, IOException {
- Config config = new Config(null);
File workTree = tmp.newFolder("dummy-worktree");
File tempFile = tmp.newFile("testCommitTemplate-");
- Repository repo = FileRepositoryBuilder.create(workTree);
+ Repository repo = FileRepositoryBuilder
+ .create(new File(workTree, ".git"));
+ repo.create();
String templateContent = "content of the template";
JGitTestUtil.write(tempFile, templateContent);
- config = parse("[i18n]\n\tcommitEncoding = invalidEcoding\n"
+ Config config = parse("[i18n]\n\tcommitEncoding = invalidEcoding\n"
+ "[commit]\n\ttemplate = "
+ Config.escapeValue(tempFile.getPath()) + "\n");
config.get(CommitConfig.KEY).getCommitTemplateContent(repo);
@@ -1566,15 +1605,17 @@ public class ConfigTest {
@Test(expected = FileNotFoundException.class)
public void testCommitTemplateWithInvalidPath()
throws ConfigInvalidException, IOException {
- Config config = new Config(null);
File workTree = tmp.newFolder("dummy-worktree");
File tempFile = tmp.newFile("testCommitTemplate-");
- Repository repo = FileRepositoryBuilder.create(workTree);
+ Repository repo = FileRepositoryBuilder
+ .create(new File(workTree, ".git"));
+ repo.create();
String templateContent = "content of the template";
JGitTestUtil.write(tempFile, templateContent);
// commit message encoding
String expectedTemplatePath = "~/nonExistingTemplate";
- config = parse("[commit]\n\ttemplate = " + expectedTemplatePath + "\n");
+ Config config = parse(
+ "[commit]\n\ttemplate = " + expectedTemplatePath + "\n");
String templatePath = config.get(CommitConfig.KEY)
.getCommitTemplatePath();
assertEquals(expectedTemplatePath, templatePath);
@@ -1595,6 +1636,47 @@ public class ConfigTest {
assertFalse(config.get(CoreConfig.KEY).enableCommitGraph());
}
+ @Test
+ public void testGetNoDefaultBoolean() {
+ Config config = new Config();
+ assertNull(config.getBoolean("foo", "bar"));
+ assertNull(config.getBoolean("foo", "bar", "baz"));
+ }
+
+ @Test
+ public void testGetNoDefaultEnum() {
+ Config config = new Config();
+ assertNull(config.getEnum(new TestEnum[] { TestEnum.ONE_TWO }, "foo",
+ "bar", "baz"));
+ }
+
+ @Test
+ public void testGetNoDefaultInt() {
+ Config config = new Config();
+ assertNull(config.getInt("foo", "bar"));
+ assertNull(config.getInt("foo", "bar", "baz"));
+ }
+ @Test
+ public void testGetNoDefaultIntInRange() {
+ Config config = new Config();
+ assertNull(config.getIntInRange("foo", "bar", 1, 5));
+ assertNull(config.getIntInRange("foo", "bar", "baz", 1, 5));
+ }
+
+ @Test
+ public void testGetNoDefaultLong() {
+ Config config = new Config();
+ assertNull(config.getLong("foo", "bar"));
+ assertNull(config.getLong("foo", "bar", "baz"));
+ }
+
+ @Test
+ public void testGetNoDefaultTimeUnit() {
+ Config config = new Config();
+ assertNull(config.getTimeUnit("foo", "bar", "baz",
+ TimeUnit.SECONDS));
+ }
+
private static void assertValueRoundTrip(String value)
throws ConfigInvalidException {
assertValueRoundTrip(value, value);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/GpgConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/GpgConfigTest.java
index 32f6766d47..5c2b190777 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/GpgConfigTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/GpgConfigTest.java
@@ -96,6 +96,16 @@ public class GpgConfigTest {
}
@Test
+ public void testGetKeyFormat_ssh() throws Exception {
+ Config c = parse("" //
+ + "[gpg]\n" //
+ + " format = ssh\n" //
+ );
+
+ assertEquals(GpgConfig.GpgFormat.SSH, new GpgConfig(c).getKeyFormat());
+ }
+
+ @Test
public void testGetSigningKey() throws Exception {
Config c = parse("" //
+ "[user]\n" //
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 2b7b6ca76c..cd98606e53 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
@@ -2,7 +2,7 @@
* Copyright (C) 2007, Dave Watson <dwatson@mimvista.com>
* Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
* Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
- * Copyright (C) 2013, Robin Stocker <robin@nibor.org> and others
+ * Copyright (C) 2013, 2025 Robin Stocker <robin@nibor.org> and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -539,7 +539,7 @@ public class IndexDiffTest extends RepositoryTestCase {
assertTrue(diff.getAssumeUnchanged().contains("file3"));
assertTrue(diff.getModified().contains("file"));
- git.add().addFilepattern(".").call();
+ git.add().addFilepattern(".").setAll(false).call();
iterator = new FileTreeIterator(db);
diff = new IndexDiff(db, Constants.HEAD, iterator);
@@ -551,6 +551,18 @@ public class IndexDiffTest extends RepositoryTestCase {
assertTrue(diff.getAssumeUnchanged().contains("file3"));
assertTrue(diff.getChanged().contains("file"));
assertEquals(Collections.EMPTY_SET, diff.getUntrackedFolders());
+
+ git.add().addFilepattern(".").call();
+
+ iterator = new FileTreeIterator(db);
+ diff = new IndexDiff(db, Constants.HEAD, iterator);
+ diff.diff();
+ assertEquals(1, diff.getAssumeUnchanged().size());
+ assertEquals(0, diff.getModified().size());
+ assertEquals(1, diff.getChanged().size());
+ assertTrue(diff.getAssumeUnchanged().contains("file2"));
+ assertTrue(diff.getChanged().contains("file"));
+ assertEquals(Collections.EMPTY_SET, diff.getUntrackedFolders());
}
}
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 5c44c9c44d..3ec4b6a073 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
@@ -11,7 +11,6 @@
package org.eclipse.jgit.lib;
-import static java.lang.Integer.valueOf;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.eclipse.jgit.junit.JGitTestUtil.concat;
import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH;
@@ -92,7 +91,7 @@ public class ObjectCheckerTest {
public void testInvalidType() {
String msg = MessageFormat.format(
JGitText.get().corruptObjectInvalidType2,
- valueOf(OBJ_BAD));
+ Integer.valueOf(OBJ_BAD));
assertCorrupt(msg, OBJ_BAD, new byte[0]);
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectIdTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectIdTest.java
index 21032c341f..d6f0b038d2 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectIdTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectIdTest.java
@@ -16,6 +16,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import java.nio.ByteBuffer;
import java.util.Locale;
import org.eclipse.jgit.errors.InvalidObjectIdException;
@@ -153,4 +154,16 @@ public class ObjectIdTest {
assertEquals(ObjectId.fromRaw(exp).name(), id.name());
}
}
+
+ @Test
+ public void test_toFromByteBuffer_raw() {
+ ObjectId oid = ObjectId
+ .fromString("ff00eedd003713bb1bb26b808ec9312548e73946");
+ ByteBuffer anObject = ByteBuffer.allocate(Constants.OBJECT_ID_LENGTH);
+ oid.copyRawTo(anObject);
+ anObject.flip();
+
+ ObjectId actual = ObjectId.fromRaw(anObject);
+ assertEquals(oid.name(), actual.name());
+ }
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/PersonIdentTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/PersonIdentTest.java
index 97da1757e0..943a68b82c 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/PersonIdentTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/PersonIdentTest.java
@@ -55,7 +55,8 @@ public class PersonIdentTest {
p.getWhenAsInstant());
assertEquals("A U Thor <author@example.com> 1142878501 -0500",
p.toExternalString());
- assertEquals(ZoneId.of("GMT-05:00"), p.getZoneId());
+ assertEquals(ZoneId.of("GMT-05:00").getRules().getOffset(
+ Instant.ofEpochMilli(1142878501000L)), p.getZoneOffset());
}
@Test
@@ -69,7 +70,8 @@ public class PersonIdentTest {
p.getWhenAsInstant());
assertEquals("A U Thor <author@example.com> 1142878501 +0530",
p.toExternalString());
- assertEquals(ZoneId.of("GMT+05:30"), p.getZoneId());
+ assertEquals(ZoneId.of("GMT+05:30").getRules().getOffset(
+ Instant.ofEpochMilli(1142878501000L)), p.getZoneOffset());
}
@SuppressWarnings("unused")
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RacyGitTests.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RacyGitTests.java
index b1d80c5c30..f25e5d10ff 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RacyGitTests.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RacyGitTests.java
@@ -30,6 +30,22 @@ import org.junit.Test;
public class RacyGitTests extends RepositoryTestCase {
+ /**
+ * This test is inherently flaky in nature since using clocks in a computer
+ * to determine file modifications in a filesystem from Java is difficult
+ * and depends on many factors and we can't test all combinations
+ *
+ * If this test fails on your computer, don't worry but let us know if you
+ * are willing to provide details which may help to further improve handling
+ * of the racy git problem in JGit.
+ *
+ * Despite not being completely reproducible this test is still useful to
+ * detect regressions when running this test repeatedly on the same
+ * OS/filesystem/Java version (which we do on the CI used to build JGit).
+ *
+ * @see "https://git-scm.com/docs/racy-git"
+ * @see "https://www.youtube.com/watch?v=m44cAozuLNI"
+ */
@Test
public void testRacyGitDetection() throws Exception {
// Reset to force creation of index file
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RefDatabaseConflictingNamesTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RefDatabaseConflictingNamesTest.java
index b02f245865..85f9612b6a 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RefDatabaseConflictingNamesTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RefDatabaseConflictingNamesTest.java
@@ -71,6 +71,11 @@ public class RefDatabaseConflictingNamesTest {
}
@Override
+ public ReflogReader getReflogReader(Ref ref) throws IOException {
+ return null;
+ }
+
+ @Override
public void create() throws IOException {
// Not needed
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ReflogConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ReflogConfigTest.java
index 854180e3ea..a93937eeea 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ReflogConfigTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ReflogConfigTest.java
@@ -16,6 +16,9 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.ZoneOffset;
import org.eclipse.jgit.junit.RepositoryTestCase;
import org.eclipse.jgit.storage.file.FileBasedConfig;
@@ -24,22 +27,23 @@ import org.junit.Test;
public class ReflogConfigTest extends RepositoryTestCase {
@Test
public void testlogAllRefUpdates() throws Exception {
- long commitTime = 1154236443000L;
- int tz = -4 * 60;
+ Instant commitTime = Instant.ofEpochSecond(1154236443L);
+ ZoneOffset tz = ZoneOffset.ofHours(-4);
// check that there are no entries in the reflog and turn off writing
// reflogs
- assertTrue(db.getReflogReader(Constants.HEAD).getReverseEntries()
+ RefDatabase refDb = db.getRefDatabase();
+ assertTrue(refDb.getReflogReader(Constants.HEAD).getReverseEntries()
.isEmpty());
- final FileBasedConfig cfg = db.getConfig();
+ FileBasedConfig cfg = db.getConfig();
cfg.setBoolean("core", null, "logallrefupdates", false);
cfg.save();
// do one commit and check that reflog size is 0: no reflogs should be
// written
commit("A Commit\n", commitTime, tz);
- commitTime += 60 * 1000;
- assertTrue("Reflog for HEAD still contain no entry", db
+ commitTime = commitTime.plus(Duration.ofMinutes(1));
+ assertTrue("Reflog for HEAD still contain no entry", refDb
.getReflogReader(Constants.HEAD).getReverseEntries().isEmpty());
// set the logAllRefUpdates parameter to true and check it
@@ -52,10 +56,10 @@ public class ReflogConfigTest extends RepositoryTestCase {
// do one commit and check that reflog size is increased to 1
commit("A Commit\n", commitTime, tz);
- commitTime += 60 * 1000;
- assertTrue(
- "Reflog for HEAD should contain one entry",
- db.getReflogReader(Constants.HEAD).getReverseEntries().size() == 1);
+ commitTime = commitTime.plus(Duration.ofMinutes(1));
+ assertTrue("Reflog for HEAD should contain one entry",
+ refDb.getReflogReader(Constants.HEAD).getReverseEntries()
+ .size() == 1);
// set the logAllRefUpdates parameter to false and check it
cfg.setBoolean("core", null, "logallrefupdates", false);
@@ -67,10 +71,10 @@ public class ReflogConfigTest extends RepositoryTestCase {
// do one commit and check that reflog size is 2
commit("A Commit\n", commitTime, tz);
- commitTime += 60 * 1000;
- assertTrue(
- "Reflog for HEAD should contain two entries",
- db.getReflogReader(Constants.HEAD).getReverseEntries().size() == 2);
+ commitTime = commitTime.plus(Duration.ofMinutes(1));
+ assertTrue("Reflog for HEAD should contain two entries",
+ refDb.getReflogReader(Constants.HEAD).getReverseEntries()
+ .size() == 2);
// set the logAllRefUpdates parameter to false and check it
cfg.setEnum("core", null, "logallrefupdates",
@@ -84,13 +88,13 @@ public class ReflogConfigTest extends RepositoryTestCase {
// do one commit and check that reflog size is 3
commit("A Commit\n", commitTime, tz);
assertTrue("Reflog for HEAD should contain three entries",
- db.getReflogReader(Constants.HEAD).getReverseEntries()
+ refDb.getReflogReader(Constants.HEAD).getReverseEntries()
.size() == 3);
}
- private void commit(String commitMsg, long commitTime, int tz)
+ private void commit(String commitMsg, Instant commitTime, ZoneOffset tz)
throws IOException {
- final CommitBuilder commit = new CommitBuilder();
+ CommitBuilder commit = new CommitBuilder();
commit.setAuthor(new PersonIdent(author, commitTime, tz));
commit.setCommitter(new PersonIdent(committer, commitTime, tz));
commit.setMessage(commitMsg);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ThreadSafeProgressMonitorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ThreadSafeProgressMonitorTest.java
index e21ff580bd..a5a6ce5d76 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ThreadSafeProgressMonitorTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ThreadSafeProgressMonitorTest.java
@@ -125,7 +125,7 @@ public class ThreadSafeProgressMonitorTest {
try {
assertTrue("latch released", cdl.await(1000, TimeUnit.MILLISECONDS));
} catch (InterruptedException ie) {
- fail("Did not expect to be interrupted");
+ throw new AssertionError("Did not expect to be interrupted", ie);
}
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/CherryPickTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/CherryPickTest.java
index ae811f830f..8865ba9ebd 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/CherryPickTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/CherryPickTest.java
@@ -15,6 +15,9 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import java.time.Instant;
+import java.time.ZoneOffset;
+
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheBuilder;
import org.eclipse.jgit.junit.RepositoryTestCase;
@@ -162,7 +165,8 @@ public class CherryPickTest extends RepositoryTestCase {
final ObjectId[] parentIds) throws Exception {
final CommitBuilder c = new CommitBuilder();
c.setTreeId(treeB.writeTree(odi));
- c.setAuthor(new PersonIdent("A U Thor", "a.u.thor", 1L, 0));
+ c.setAuthor(new PersonIdent("A U Thor", "a.u.thor",
+ Instant.ofEpochSecond(1), ZoneOffset.UTC));
c.setCommitter(c.getAuthor());
c.setParentIds(parentIds);
c.setMessage("Tree " + c.getTreeId().name());
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/GitlinkMergeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/GitlinkMergeTest.java
index f410960bec..b1998f30f8 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/GitlinkMergeTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/GitlinkMergeTest.java
@@ -15,6 +15,8 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
+import java.time.Instant;
+import java.time.ZoneOffset;
import org.eclipse.jgit.annotations.Nullable;
import org.eclipse.jgit.dircache.DirCache;
@@ -357,7 +359,8 @@ public class GitlinkMergeTest extends SampleDataRepositoryTestCase {
ObjectId[] parentIds) throws Exception {
CommitBuilder c = new CommitBuilder();
c.setTreeId(treeB.writeTree(odi));
- c.setAuthor(new PersonIdent("A U Thor", "a.u.thor", 1L, 0));
+ c.setAuthor(new PersonIdent("A U Thor", "a.u.thor",
+ Instant.ofEpochSecond(1), ZoneOffset.UTC));
c.setCommitter(c.getAuthor());
c.setParentIds(parentIds);
c.setMessage("Tree " + c.getTreeId().name());
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergeAlgorithmTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergeAlgorithmTest.java
index 5f4331b04d..7a8a93e977 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergeAlgorithmTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergeAlgorithmTest.java
@@ -47,7 +47,10 @@ public class MergeAlgorithmTest {
@Test
public void testTwoConflictingModifications() throws IOException {
assertEquals(t("a<b=Z>Zdefghij"),
- merge("abcdefghij", "abZdefghij", "aZZdefghij"));
+ merge("abcdefghij", "abZdefghij", "aZZdefghij", false));
+
+ assertEquals(t("a<b|b=Z>Zdefghij"),
+ merge("abcdefghij", "abZdefghij", "aZZdefghij", true));
}
/**
@@ -60,7 +63,10 @@ public class MergeAlgorithmTest {
@Test
public void testOneAgainstTwoConflictingModifications() throws IOException {
assertEquals(t("aZ<Z=c>Zefghij"),
- merge("abcdefghij", "aZZZefghij", "aZcZefghij"));
+ merge("abcdefghij", "aZZZefghij", "aZcZefghij", false));
+
+ assertEquals(t("aZ<Z|c=c>Zefghij"),
+ merge("abcdefghij", "aZZZefghij", "aZcZefghij", true));
}
/**
@@ -72,7 +78,10 @@ public class MergeAlgorithmTest {
@Test
public void testNoAgainstOneModification() throws IOException {
assertEquals(t("aZcZefghij"),
- merge("abcdefghij", "abcdefghij", "aZcZefghij"));
+ merge("abcdefghij", "abcdefghij", "aZcZefghij", false));
+
+ assertEquals(t("aZcZefghij"),
+ merge("abcdefghij", "abcdefghij", "aZcZefghij", true));
}
/**
@@ -84,7 +93,10 @@ public class MergeAlgorithmTest {
@Test
public void testTwoNonConflictingModifications() throws IOException {
assertEquals(t("YbZdefghij"),
- merge("abcdefghij", "abZdefghij", "Ybcdefghij"));
+ merge("abcdefghij", "abZdefghij", "Ybcdefghij", false));
+
+ assertEquals(t("YbZdefghij"),
+ merge("abcdefghij", "abZdefghij", "Ybcdefghij", true));
}
/**
@@ -96,7 +108,10 @@ public class MergeAlgorithmTest {
@Test
public void testTwoComplicatedModifications() throws IOException {
assertEquals(t("a<ZZZZfZhZj=bYdYYYYiY>"),
- merge("abcdefghij", "aZZZZfZhZj", "abYdYYYYiY"));
+ merge("abcdefghij", "aZZZZfZhZj", "abYdYYYYiY", false));
+
+ assertEquals(t("a<ZZZZfZhZj|bcdefghij=bYdYYYYiY>"),
+ merge("abcdefghij", "aZZZZfZhZj", "abYdYYYYiY", true));
}
/**
@@ -109,7 +124,9 @@ public class MergeAlgorithmTest {
@Test
public void testTwoModificationsWithSharedDelete() throws IOException {
assertEquals(t("Cb}n}"),
- merge("ab}n}n}", "ab}n}", "Cb}n}"));
+ merge("ab}n}n}", "ab}n}", "Cb}n}", false));
+
+ assertEquals(t("Cb}n}"), merge("ab}n}n}", "ab}n}", "Cb}n}", true));
}
/**
@@ -122,7 +139,11 @@ public class MergeAlgorithmTest {
@Test
public void testModificationsWithMiddleInsert() throws IOException {
assertEquals(t("aBcd123123uvwxPq"),
- merge("abcd123uvwxpq", "aBcd123123uvwxPq", "abcd123123uvwxpq"));
+ merge("abcd123uvwxpq", "aBcd123123uvwxPq", "abcd123123uvwxpq",
+ false));
+
+ assertEquals(t("aBcd123123uvwxPq"), merge("abcd123uvwxpq",
+ "aBcd123123uvwxPq", "abcd123123uvwxpq", true));
}
/**
@@ -135,7 +156,23 @@ public class MergeAlgorithmTest {
@Test
public void testModificationsWithMiddleDelete() throws IOException {
assertEquals(t("Abz}z123Q"),
- merge("abz}z}z123q", "Abz}z123Q", "abz}z123q"));
+ merge("abz}z}z123q", "Abz}z123Q", "abz}z123q", false));
+
+ assertEquals(t("Abz}z123Q"),
+ merge("abz}z}z123q", "Abz}z123Q", "abz}z123q", true));
+ }
+
+ @Test
+ public void testInsertionAfterDeletion() throws IOException {
+ assertEquals(t("a<=bc>d"), merge("abd", "ad", "abcd", false));
+ assertEquals(t("a<|b=bc>d"),
+ merge("abd", "ad", "abcd", true));
+ }
+
+ @Test
+ public void testInsertionBeforeDeletion() throws IOException {
+ assertEquals(t("a<=cb>d"), merge("abd", "ad", "acbd", false));
+ assertEquals(t("a<|b=cb>d"), merge("abd", "ad", "acbd", true));
}
/**
@@ -146,7 +183,10 @@ public class MergeAlgorithmTest {
@Test
public void testConflictAtStart() throws IOException {
assertEquals(t("<Z=Y>bcdefghij"),
- merge("abcdefghij", "Zbcdefghij", "Ybcdefghij"));
+ merge("abcdefghij", "Zbcdefghij", "Ybcdefghij", false));
+
+ assertEquals(t("<Z|a=Y>bcdefghij"),
+ merge("abcdefghij", "Zbcdefghij", "Ybcdefghij", true));
}
/**
@@ -157,7 +197,10 @@ public class MergeAlgorithmTest {
@Test
public void testConflictAtEnd() throws IOException {
assertEquals(t("abcdefghi<Z=Y>"),
- merge("abcdefghij", "abcdefghiZ", "abcdefghiY"));
+ merge("abcdefghij", "abcdefghiZ", "abcdefghiY", false));
+
+ assertEquals(t("abcdefghi<Z|j=Y>"),
+ merge("abcdefghij", "abcdefghiZ", "abcdefghiY", true));
}
/**
@@ -169,7 +212,10 @@ public class MergeAlgorithmTest {
@Test
public void testSameModification() throws IOException {
assertEquals(t("abZdefghij"),
- merge("abcdefghij", "abZdefghij", "abZdefghij"));
+ merge("abcdefghij", "abZdefghij", "abZdefghij", false));
+
+ assertEquals(t("abZdefghij"),
+ merge("abcdefghij", "abZdefghij", "abZdefghij", true));
}
/**
@@ -181,27 +227,36 @@ public class MergeAlgorithmTest {
@Test
public void testDeleteVsModify() throws IOException {
assertEquals(t("ab<=Z>defghij"),
- merge("abcdefghij", "abdefghij", "abZdefghij"));
+ merge("abcdefghij", "abdefghij", "abZdefghij", false));
+
+ assertEquals(t("ab<|c=Z>defghij"),
+ merge("abcdefghij", "abdefghij", "abZdefghij", true));
}
@Test
public void testInsertVsModify() throws IOException {
- assertEquals(t("a<bZ=XY>"), merge("ab", "abZ", "aXY"));
+ assertEquals(t("a<bZ=XY>"), merge("ab", "abZ", "aXY", false));
+ assertEquals(t("a<bZ|b=XY>"), merge("ab", "abZ", "aXY", true));
}
@Test
public void testAdjacentModifications() throws IOException {
- assertEquals(t("a<Zc=bY>d"), merge("abcd", "aZcd", "abYd"));
+ assertEquals(t("a<Zc=bY>d"), merge("abcd", "aZcd", "abYd", false));
+ assertEquals(t("a<Zc|bc=bY>d"), merge("abcd", "aZcd", "abYd", true));
}
@Test
public void testSeparateModifications() throws IOException {
- assertEquals(t("aZcYe"), merge("abcde", "aZcde", "abcYe"));
+ assertEquals(t("aZcYe"), merge("abcde", "aZcde", "abcYe", false));
+ assertEquals(t("aZcYe"), merge("abcde", "aZcde", "abcYe", true));
}
@Test
public void testBlankLines() throws IOException {
- assertEquals(t("aZc\nYe"), merge("abc\nde", "aZc\nde", "abc\nYe"));
+ assertEquals(t("aZc\nYe"),
+ merge("abc\nde", "aZc\nde", "abc\nYe", false));
+ assertEquals(t("aZc\nYe"),
+ merge("abc\nde", "aZc\nde", "abc\nYe", true));
}
/**
@@ -214,11 +269,22 @@ public class MergeAlgorithmTest {
*/
@Test
public void testTwoSimilarModsAndOneInsert() throws IOException {
- assertEquals(t("aBcDde"), merge("abcde", "aBcde", "aBcDde"));
- assertEquals(t("IAAAJCAB"), merge("iACAB", "IACAB", "IAAAJCAB"));
- assertEquals(t("HIAAAJCAB"), merge("HiACAB", "HIACAB", "HIAAAJCAB"));
+ assertEquals(t("aBcDde"), merge("abcde", "aBcde", "aBcDde", false));
+ assertEquals(t("aBcDde"), merge("abcde", "aBcde", "aBcDde", true));
+
+ assertEquals(t("IAAAJCAB"), merge("iACAB", "IACAB", "IAAAJCAB", false));
+ assertEquals(t("IAAAJCAB"), merge("iACAB", "IACAB", "IAAAJCAB", true));
+
+ assertEquals(t("HIAAAJCAB"),
+ merge("HiACAB", "HIACAB", "HIAAAJCAB", false));
+ assertEquals(t("HIAAAJCAB"),
+ merge("HiACAB", "HIACAB", "HIAAAJCAB", true));
+
+ assertEquals(t("AGADEFHIAAAJCAB"),
+ merge("AGADEFHiACAB", "AGADEFHIACAB", "AGADEFHIAAAJCAB",
+ false));
assertEquals(t("AGADEFHIAAAJCAB"),
- merge("AGADEFHiACAB", "AGADEFHIACAB", "AGADEFHIAAAJCAB"));
+ merge("AGADEFHiACAB", "AGADEFHIACAB", "AGADEFHIAAAJCAB", true));
}
/**
@@ -232,18 +298,28 @@ public class MergeAlgorithmTest {
@Test
public void testTwoSimilarModsAndOneInsertAtEnd() throws IOException {
Assume.assumeTrue(newlineAtEnd);
- assertEquals(t("IAAJ"), merge("iA", "IA", "IAAJ"));
- assertEquals(t("IAJ"), merge("iA", "IA", "IAJ"));
- assertEquals(t("IAAAJ"), merge("iA", "IA", "IAAAJ"));
+ assertEquals(t("IAAJ"), merge("iA", "IA", "IAAJ", false));
+ assertEquals(t("IAAJ"), merge("iA", "IA", "IAAJ", true));
+
+ assertEquals(t("IAJ"), merge("iA", "IA", "IAJ", false));
+ assertEquals(t("IAJ"), merge("iA", "IA", "IAJ", true));
+
+ assertEquals(t("IAAAJ"), merge("iA", "IA", "IAAAJ", false));
+ assertEquals(t("IAAAJ"), merge("iA", "IA", "IAAAJ", true));
}
@Test
public void testTwoSimilarModsAndOneInsertAtEndNoNewlineAtEnd()
throws IOException {
Assume.assumeFalse(newlineAtEnd);
- assertEquals(t("I<A=AAJ>"), merge("iA", "IA", "IAAJ"));
- assertEquals(t("I<A=AJ>"), merge("iA", "IA", "IAJ"));
- assertEquals(t("I<A=AAAJ>"), merge("iA", "IA", "IAAAJ"));
+ assertEquals(t("I<A=AAJ>"), merge("iA", "IA", "IAAJ", false));
+ assertEquals(t("I<A|A=AAJ>"), merge("iA", "IA", "IAAJ", true));
+
+ assertEquals(t("I<A=AJ>"), merge("iA", "IA", "IAJ", false));
+ assertEquals(t("I<A|A=AJ>"), merge("iA", "IA", "IAJ", true));
+
+ assertEquals(t("I<A=AAAJ>"), merge("iA", "IA", "IAAAJ", false));
+ assertEquals(t("I<A|A=AAAJ>"), merge("iA", "IA", "IAAAJ", true));
}
/**
@@ -254,22 +330,34 @@ public class MergeAlgorithmTest {
@Test
public void testEmptyTexts() throws IOException {
// test modification against deletion
- assertEquals(t("<AB=>"), merge("A", "AB", ""));
- assertEquals(t("<=AB>"), merge("A", "", "AB"));
+ assertEquals(t("<AB=>"), merge("A", "AB", "", false));
+ assertEquals(t("<AB|A=>"), merge("A", "AB", "", true));
+
+ assertEquals(t("<=AB>"), merge("A", "", "AB", false));
+ assertEquals(t("<|A=AB>"), merge("A", "", "AB", true));
// test unmodified against deletion
- assertEquals(t(""), merge("AB", "AB", ""));
- assertEquals(t(""), merge("AB", "", "AB"));
+ assertEquals(t(""), merge("AB", "AB", "", false));
+ assertEquals(t(""), merge("AB", "AB", "", true));
+
+ assertEquals(t(""), merge("AB", "", "AB", false));
+ assertEquals(t(""), merge("AB", "", "AB", true));
// test deletion against deletion
- assertEquals(t(""), merge("AB", "", ""));
+ assertEquals(t(""), merge("AB", "", "", false));
+ assertEquals(t(""), merge("AB", "", "", true));
}
- private String merge(String commonBase, String ours, String theirs) throws IOException {
+ private String merge(String commonBase, String ours, String theirs,
+ boolean diff3) throws IOException {
MergeResult r = new MergeAlgorithm().merge(RawTextComparator.DEFAULT,
T(commonBase), T(ours), T(theirs));
ByteArrayOutputStream bo=new ByteArrayOutputStream(50);
- fmt.formatMerge(bo, r, "B", "O", "T", UTF_8);
+ if (diff3) {
+ fmt.formatMergeDiff3(bo, r, "B", "O", "T", UTF_8);
+ } else {
+ fmt.formatMerge(bo, r, "B", "O", "T", UTF_8);
+ }
return new String(bo.toByteArray(), UTF_8);
}
@@ -284,6 +372,9 @@ public class MergeAlgorithmTest {
case '=':
r.append("=======\n");
break;
+ case '|':
+ r.append("||||||| B\n");
+ break;
case '>':
r.append(">>>>>>> T\n");
break;
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergeAlgorithmUnionTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergeAlgorithmUnionTest.java
new file mode 100644
index 0000000000..3a8af7a00e
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergeAlgorithmUnionTest.java
@@ -0,0 +1,328 @@
+/*
+ * Copyright (C) 2024 Qualcomm Innovation Center, Inc.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.merge;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertEquals;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import org.eclipse.jgit.diff.RawText;
+import org.eclipse.jgit.diff.RawTextComparator;
+import org.eclipse.jgit.lib.Constants;
+import org.junit.Assume;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.runner.RunWith;
+
+@RunWith(Theories.class)
+public class MergeAlgorithmUnionTest {
+ MergeFormatter fmt = new MergeFormatter();
+
+ private final boolean newlineAtEnd;
+
+ @DataPoints
+ public static boolean[] newlineAtEndDataPoints = { false, true };
+
+ public MergeAlgorithmUnionTest(boolean newlineAtEnd) {
+ this.newlineAtEnd = newlineAtEnd;
+ }
+
+ /**
+ * Check for a conflict where the second text was changed similar to the
+ * first one, but the second texts modification covers one more line.
+ *
+ * @throws java.io.IOException
+ */
+ @Test
+ public void testTwoConflictingModifications() throws IOException {
+ assertEquals(t("abZZdefghij"),
+ merge("abcdefghij", "abZdefghij", "aZZdefghij"));
+ }
+
+ /**
+ * Test a case where we have three consecutive chunks. The first text
+ * modifies all three chunks. The second text modifies the first and the
+ * last chunk. This should be reported as one conflicting region.
+ *
+ * @throws java.io.IOException
+ */
+ @Test
+ public void testOneAgainstTwoConflictingModifications() throws IOException {
+ assertEquals(t("aZZcZefghij"),
+ merge("abcdefghij", "aZZZefghij", "aZcZefghij"));
+ }
+
+ /**
+ * Test a merge where only the second text contains modifications. Expect as
+ * merge result the second text.
+ *
+ * @throws java.io.IOException
+ */
+ @Test
+ public void testNoAgainstOneModification() throws IOException {
+ assertEquals(t("aZcZefghij"),
+ merge("abcdefghij", "abcdefghij", "aZcZefghij"));
+ }
+
+ /**
+ * Both texts contain modifications but not on the same chunks. Expect a
+ * non-conflict merge result.
+ *
+ * @throws java.io.IOException
+ */
+ @Test
+ public void testTwoNonConflictingModifications() throws IOException {
+ assertEquals(t("YbZdefghij"),
+ merge("abcdefghij", "abZdefghij", "Ybcdefghij"));
+ }
+
+ /**
+ * Merge two complicated modifications. The merge algorithm has to extend
+ * and combine conflicting regions to get to the expected merge result.
+ *
+ * @throws java.io.IOException
+ */
+ @Test
+ public void testTwoComplicatedModifications() throws IOException {
+ assertEquals(t("aZZZZfZhZjbYdYYYYiY"),
+ merge("abcdefghij", "aZZZZfZhZj", "abYdYYYYiY"));
+ }
+
+ /**
+ * Merge two modifications with a shared delete at the end. The underlying
+ * diff algorithm has to provide consistent edit results to get the expected
+ * merge result.
+ *
+ * @throws java.io.IOException
+ */
+ @Test
+ public void testTwoModificationsWithSharedDelete() throws IOException {
+ assertEquals(t("Cb}n}"), merge("ab}n}n}", "ab}n}", "Cb}n}"));
+ }
+
+ /**
+ * Merge modifications with a shared insert in the middle. The underlying
+ * diff algorithm has to provide consistent edit results to get the expected
+ * merge result.
+ *
+ * @throws java.io.IOException
+ */
+ @Test
+ public void testModificationsWithMiddleInsert() throws IOException {
+ assertEquals(t("aBcd123123uvwxPq"),
+ merge("abcd123uvwxpq", "aBcd123123uvwxPq", "abcd123123uvwxpq"));
+ }
+
+ /**
+ * Merge modifications with a shared delete in the middle. The underlying
+ * diff algorithm has to provide consistent edit results to get the expected
+ * merge result.
+ *
+ * @throws java.io.IOException
+ */
+ @Test
+ public void testModificationsWithMiddleDelete() throws IOException {
+ assertEquals(t("Abz}z123Q"),
+ merge("abz}z}z123q", "Abz}z123Q", "abz}z123q"));
+ }
+
+ @Test
+ public void testInsertionAfterDeletion() throws IOException {
+ assertEquals(t("abcd"), merge("abd", "ad", "abcd"));
+ }
+
+ @Test
+ public void testInsertionBeforeDeletion() throws IOException {
+ assertEquals(t("acbd"), merge("abd", "ad", "acbd"));
+ }
+
+ /**
+ * Test a conflicting region at the very start of the text.
+ *
+ * @throws java.io.IOException
+ */
+ @Test
+ public void testConflictAtStart() throws IOException {
+ assertEquals(t("ZYbcdefghij"),
+ merge("abcdefghij", "Zbcdefghij", "Ybcdefghij"));
+ }
+
+ /**
+ * Test a conflicting region at the very end of the text.
+ *
+ * @throws java.io.IOException
+ */
+ @Test
+ public void testConflictAtEnd() throws IOException {
+ assertEquals(t("abcdefghiZY"),
+ merge("abcdefghij", "abcdefghiZ", "abcdefghiY"));
+ }
+
+ /**
+ * Check for a conflict where the second text was changed similar to the
+ * first one, but the second texts modification covers one more line.
+ *
+ * @throws java.io.IOException
+ */
+ @Test
+ public void testSameModification() throws IOException {
+ assertEquals(t("abZdefghij"),
+ merge("abcdefghij", "abZdefghij", "abZdefghij"));
+ }
+
+ /**
+ * Check that a deleted vs. a modified line shows up as conflict (see Bug
+ * 328551)
+ *
+ * @throws java.io.IOException
+ */
+ @Test
+ public void testDeleteVsModify() throws IOException {
+ assertEquals(t("abZdefghij"),
+ merge("abcdefghij", "abdefghij", "abZdefghij"));
+ }
+
+ @Test
+ public void testInsertVsModify() throws IOException {
+ assertEquals(t("abZXY"), merge("ab", "abZ", "aXY"));
+ }
+
+ @Test
+ public void testAdjacentModifications() throws IOException {
+ assertEquals(t("aZcbYd"), merge("abcd", "aZcd", "abYd"));
+ }
+
+ @Test
+ public void testSeparateModifications() throws IOException {
+ assertEquals(t("aZcYe"), merge("abcde", "aZcde", "abcYe"));
+ }
+
+ @Test
+ public void testBlankLines() throws IOException {
+ assertEquals(t("aZc\nYe"), merge("abc\nde", "aZc\nde", "abc\nYe"));
+ }
+
+ /**
+ * Test merging two contents which do one similar modification and one
+ * insertion is only done by one side, in the middle. Between modification
+ * and insertion is a block which is common between the two contents and the
+ * common base
+ *
+ * @throws java.io.IOException
+ */
+ @Test
+ public void testTwoSimilarModsAndOneInsert() throws IOException {
+ assertEquals(t("aBcDde"), merge("abcde", "aBcde", "aBcDde"));
+
+ assertEquals(t("IAAAJCAB"), merge("iACAB", "IACAB", "IAAAJCAB"));
+
+ assertEquals(t("HIAAAJCAB"), merge("HiACAB", "HIACAB", "HIAAAJCAB"));
+
+ assertEquals(t("AGADEFHIAAAJCAB"),
+ merge("AGADEFHiACAB", "AGADEFHIACAB", "AGADEFHIAAAJCAB"));
+ }
+
+ /**
+ * Test merging two contents which do one similar modification and one
+ * insertion is only done by one side, at the end. Between modification and
+ * insertion is a block which is common between the two contents and the
+ * common base
+ *
+ * @throws java.io.IOException
+ */
+ @Test
+ public void testTwoSimilarModsAndOneInsertAtEnd() throws IOException {
+ Assume.assumeTrue(newlineAtEnd);
+ assertEquals(t("IAAJ"), merge("iA", "IA", "IAAJ"));
+
+ assertEquals(t("IAJ"), merge("iA", "IA", "IAJ"));
+
+ assertEquals(t("IAAAJ"), merge("iA", "IA", "IAAAJ"));
+ }
+
+ @Test
+ public void testTwoSimilarModsAndOneInsertAtEndNoNewlineAtEnd()
+ throws IOException {
+ Assume.assumeFalse(newlineAtEnd);
+ assertEquals(t("IAAAJ"), merge("iA", "IA", "IAAJ"));
+
+ assertEquals(t("IAAJ"), merge("iA", "IA", "IAJ"));
+
+ assertEquals(t("IAAAAJ"), merge("iA", "IA", "IAAAJ"));
+ }
+
+ // Test situations where (at least) one input value is the empty text
+
+ @Test
+ public void testEmptyTextModifiedAgainstDeletion() throws IOException {
+ // NOTE: git.git merge-file appends a '\n' to the end of the file even
+ // when the input files do not have a newline at the end. That appears
+ // to be a bug in git.git.
+ assertEquals(t("AB"), merge("A", "AB", ""));
+ assertEquals(t("AB"), merge("A", "", "AB"));
+ }
+
+ @Test
+ public void testEmptyTextUnmodifiedAgainstDeletion() throws IOException {
+ assertEquals(t(""), merge("AB", "AB", ""));
+
+ assertEquals(t(""), merge("AB", "", "AB"));
+ }
+
+ @Test
+ public void testEmptyTextDeletionAgainstDeletion() throws IOException {
+ assertEquals(t(""), merge("AB", "", ""));
+ }
+
+ private String merge(String commonBase, String ours, String theirs)
+ throws IOException {
+ MergeAlgorithm ma = new MergeAlgorithm();
+ ma.setContentMergeStrategy(ContentMergeStrategy.UNION);
+ MergeResult<RawText> r = ma.merge(RawTextComparator.DEFAULT,
+ T(commonBase), T(ours), T(theirs));
+ ByteArrayOutputStream bo = new ByteArrayOutputStream(50);
+ fmt.formatMerge(bo, r, "B", "O", "T", UTF_8);
+ return bo.toString(UTF_8);
+ }
+
+ public String t(String text) {
+ StringBuilder r = new StringBuilder();
+ for (int i = 0; i < text.length(); i++) {
+ char c = text.charAt(i);
+ switch (c) {
+ case '<':
+ r.append("<<<<<<< O\n");
+ break;
+ case '=':
+ r.append("=======\n");
+ break;
+ case '|':
+ r.append("||||||| B\n");
+ break;
+ case '>':
+ r.append(">>>>>>> T\n");
+ break;
+ default:
+ r.append(c);
+ if (newlineAtEnd || i < text.length() - 1)
+ r.append('\n');
+ }
+ }
+ return r.toString();
+ }
+
+ public RawText T(String text) {
+ return new RawText(Constants.encode(t(text)));
+ }
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java
index 022e8cd55e..c6a6321cf8 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java
@@ -22,9 +22,12 @@ import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
+import java.nio.file.Files;
import java.time.Instant;
import java.util.Arrays;
+import java.util.HashSet;
import java.util.Map;
+import java.util.Set;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.MergeResult;
@@ -51,6 +54,7 @@ import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.ObjectStream;
+import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason;
import org.eclipse.jgit.revwalk.RevCommit;
@@ -1442,6 +1446,8 @@ public class MergerTest extends RepositoryTestCase {
git.checkout().setName("master").call();
mergeResult = git.merge().include(commitX).setStrategy(strategy)
.call();
+ assertEquals(MergeResult.MergeStatus.MERGED,
+ mergeResult.getMergeStatus());
// Now, merge commit A and B (i.e. "master" and "second-branch").
// None of them have the file "a", so there is no conflict, BUT while
@@ -1735,25 +1741,25 @@ public class MergerTest extends RepositoryTestCase {
git.add().addFilepattern("c").call();
RevCommit commitI = git.commit().setMessage("Initial commit").call();
- File a = writeTrashFile("a", "content in Ancestor");
+ writeTrashFile("a", "content in Ancestor");
git.add().addFilepattern("a").call();
RevCommit commitA1 = git.commit().setMessage("Ancestor 1").call();
- a = writeTrashFile("a", "content in Child 1 (commited on master)");
+ writeTrashFile("a", "content in Child 1 (commited on master)");
git.add().addFilepattern("a").call();
// commit C1M
git.commit().setMessage("Child 1 on master").call();
git.checkout().setCreateBranch(true).setStartPoint(commitI).setName("branch-to-merge").call();
// "a" becomes executable in A2
- a = writeTrashFile("a", "content in Ancestor");
+ File a = writeTrashFile("a", "content in Ancestor");
a.setExecutable(true);
git.add().addFilepattern("a").call();
RevCommit commitA2 = git.commit().setMessage("Ancestor 2").call();
// second branch
git.checkout().setCreateBranch(true).setStartPoint(commitA1).setName("second-branch").call();
- a = writeTrashFile("a", "content in Child 2 (commited on second-branch)");
+ writeTrashFile("a", "content in Child 2 (commited on second-branch)");
git.add().addFilepattern("a").call();
// commit C2S
git.commit().setMessage("Child 2 on second-branch").call();
@@ -1786,7 +1792,259 @@ public class MergerTest extends RepositoryTestCase {
// children
mergeResult = git.merge().include(commitC3S).call();
assertEquals(mergeResult.getMergeStatus(), MergeStatus.MERGED);
+ }
+
+ /**
+ * Merging two commits when binary files have equal content, but conflicting content in the
+ * virtual ancestor.
+ *
+ * <p>
+ * This test has the same set up as
+ * {@code checkFileDirMergeConflictInVirtualAncestor_NoConflictInChildren}, only
+ * with the content conflict in A1 and A2.
+ */
+ @Theory
+ public void checkBinaryMergeConflictInVirtualAncestor(MergeStrategy strategy) throws Exception {
+ if (!strategy.equals(MergeStrategy.RECURSIVE)) {
+ return;
+ }
+
+ Git git = Git.wrap(db);
+
+ // master
+ writeTrashFile("c", "initial file");
+ git.add().addFilepattern("c").call();
+ RevCommit commitI = git.commit().setMessage("Initial commit").call();
+
+ writeTrashFile("a", "\0\1\1\1\1\0"); // content in Ancestor 1
+ git.add().addFilepattern("a").call();
+ RevCommit commitA1 = git.commit().setMessage("Ancestor 1").call();
+
+ writeTrashFile("a", "\0\1\2\3\4\5\0"); // content in Child 1 (commited on master)
+ git.add().addFilepattern("a").call();
+ // commit C1M
+ git.commit().setMessage("Child 1 on master").call();
+
+ git.checkout().setCreateBranch(true).setStartPoint(commitI).setName("branch-to-merge").call();
+ writeTrashFile("a", "\0\2\2\2\2\0"); // content in Ancestor 1
+ git.add().addFilepattern("a").call();
+ RevCommit commitA2 = git.commit().setMessage("Ancestor 2").call();
+
+ // second branch
+ git.checkout().setCreateBranch(true).setStartPoint(commitA1).setName("second-branch").call();
+ writeTrashFile("a", "\0\5\4\3\2\1\0"); // content in Child 2 (commited on second-branch)
+ git.add().addFilepattern("a").call();
+ // commit C2S
+ git.commit().setMessage("Child 2 on second-branch").call();
+
+ // Merge branch-to-merge into second-branch
+ MergeResult mergeResult = git.merge().include(commitA2).setStrategy(strategy).call();
+ assertEquals(mergeResult.getNewHead(), null);
+ assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
+ // Resolve the conflict manually
+ writeTrashFile("a", "\0\3\3\3\3\0"); // merge conflict resolution
+ git.add().addFilepattern("a").call();
+ RevCommit commitC3S = git.commit().setMessage("Child 3 on second bug - resolve merge conflict").call();
+
+ // Merge branch-to-merge into master
+ git.checkout().setName("master").call();
+ mergeResult = git.merge().include(commitA2).setStrategy(strategy).call();
+ assertEquals(mergeResult.getNewHead(), null);
+ assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
+
+ // Resolve the conflict manually - set the same value as in resolution above
+ writeTrashFile("a", "\0\3\3\3\3\0"); // merge conflict resolution
+ git.add().addFilepattern("a").call();
+ // commit C4M
+ git.commit().setMessage("Child 4 on master - resolve merge conflict").call();
+
+ // Merge C4M (second-branch) into master (C3S)
+ // Conflict in virtual base should be here, but there are no conflicts in
+ // children
+ mergeResult = git.merge().include(commitC3S).call();
+ assertEquals(mergeResult.getMergeStatus(), MergeStatus.MERGED);
+ }
+
+ /**
+ * File is binary in ours, theirs and base with different content in each of
+ * them. Content of the file should not change after the merge conflict as
+ * no conflict markers are added to the binary files
+ */
+ @Theory
+ public void oursBinaryTheirsBinaryBaseBinary(MergeStrategy strategy)
+ throws Exception {
+ Git git = Git.wrap(db);
+ String binaryFile = "file";
+
+ writeTrashFile(binaryFile, "\u0000\u0001");
+ git.add().addFilepattern(binaryFile).call();
+ RevCommit parent = git.commit().setMessage("BASE COMMIT").call();
+ String fileHashInBase = getFileHashInWorkTree(git, binaryFile);
+
+ writeTrashFile(binaryFile, "\u0001\u0002");
+ git.add().addFilepattern(binaryFile).call();
+ RevCommit child1 = git.commit().setMessage("THEIRS COMMIT").call();
+ String fileHashInChild1 = getFileHashInWorkTree(git, binaryFile);
+
+ git.checkout().setCreateBranch(true).setStartPoint(parent)
+ .setName("side").call();
+
+ writeTrashFile(binaryFile, "\u0002\u0000");
+ git.add().addFilepattern(binaryFile).call();
+ git.commit().setMessage("OURS COMMIT").call();
+ String fileHashInChild2 = getFileHashInWorkTree(git, binaryFile);
+
+ MergeResult mergeResult = git.merge().setStrategy(strategy)
+ .include(child1).call();
+
+ // check if the merge caused a conflict
+ assertTrue(mergeResult.getConflicts() != null
+ && !mergeResult.getConflicts().isEmpty());
+ String fileHashInChild2AfterMerge = getFileHashInWorkTree(git,
+ binaryFile);
+
+ // check if the file content changed during a conflicting merge
+ assertEquals(fileHashInChild2AfterMerge, fileHashInChild2);
+
+ Set<String> hashesInIndexFile = new HashSet<>();
+ DirCache indexContent = git.getRepository().readDirCache();
+ for (int i = 0; i < indexContent.getEntryCount(); ++i) {
+ DirCacheEntry indexEntry = indexContent.getEntry(i);
+ if (binaryFile.equals(indexEntry.getPathString())) {
+ hashesInIndexFile.add(indexEntry.getObjectId().name());
+ }
+ }
+
+ // check if all the three stages are added to index file
+ assertTrue(hashesInIndexFile.contains(fileHashInBase));
+ assertTrue(hashesInIndexFile.contains(fileHashInChild1));
+ assertTrue(hashesInIndexFile.contains(fileHashInChild2));
+ }
+
+ /**
+ * File is text in ours and theirs with different content but binary in
+ * base. Even in this case, file will be treated as a binary and no conflict
+ * markers are added to it
+ */
+ @Theory
+ public void oursAndTheirsDifferentTextBaseBinary(MergeStrategy strategy)
+ throws Exception {
+ Git git = Git.wrap(db);
+ String binaryFile = "file";
+
+ writeTrashFile(binaryFile, "\u0000\u0001");
+ git.add().addFilepattern(binaryFile).call();
+ RevCommit parent = git.commit().setMessage("BASE COMMIT").call();
+ String fileHashInBase = getFileHashInWorkTree(git, binaryFile);
+
+ writeTrashFile(binaryFile, "TEXT1");
+ git.add().addFilepattern(binaryFile).call();
+ RevCommit child1 = git.commit().setMessage("THEIRS COMMIT").call();
+ String fileHashInChild1 = getFileHashInWorkTree(git, binaryFile);
+
+ git.checkout().setCreateBranch(true).setStartPoint(parent)
+ .setName("side").call();
+
+ writeTrashFile(binaryFile, "TEXT2");
+ git.add().addFilepattern(binaryFile).call();
+ git.commit().setMessage("OURS COMMIT").call();
+ String fileHashInChild2 = getFileHashInWorkTree(git, binaryFile);
+
+ MergeResult mergeResult = git.merge().setStrategy(strategy)
+ .include(child1).call();
+
+ assertTrue(mergeResult.getConflicts() != null
+ && !mergeResult.getConflicts().isEmpty());
+ String fileHashInChild2AfterMerge = getFileHashInWorkTree(git,
+ binaryFile);
+
+ assertEquals(fileHashInChild2AfterMerge, fileHashInChild2);
+
+ Set<String> hashesInIndexFile = new HashSet<>();
+ DirCache indexContent = git.getRepository().readDirCache();
+ for (int i = 0; i < indexContent.getEntryCount(); ++i) {
+ DirCacheEntry indexEntry = indexContent.getEntry(i);
+ if (binaryFile.equals(indexEntry.getPathString())) {
+ hashesInIndexFile.add(indexEntry.getObjectId().name());
+ }
+ }
+
+ assertTrue(hashesInIndexFile.contains(fileHashInBase));
+ assertTrue(hashesInIndexFile.contains(fileHashInChild1));
+ assertTrue(hashesInIndexFile.contains(fileHashInChild2));
+ }
+
+ /**
+ * Tests the scenario where a file is expected to be treated as binary
+ * according to Git attributes
+ */
+ @Theory
+ public void fileInBinaryInAttribute(MergeStrategy strategy)
+ throws Exception {
+ Git git = Git.wrap(db);
+ String binaryFile = "file.bin";
+
+ writeTrashFile(".gitattributes", binaryFile + " binary");
+ git.add().addFilepattern(".gitattributes").call();
+ git.commit().setMessage("ADDING GITATTRIBUTES").call();
+
+ writeTrashFile(binaryFile, "\u0000\u0001");
+ git.add().addFilepattern(binaryFile).call();
+ RevCommit parent = git.commit().setMessage("BASE COMMIT").call();
+ String fileHashInBase = getFileHashInWorkTree(git, binaryFile);
+
+ writeTrashFile(binaryFile, "\u0001\u0002");
+ git.add().addFilepattern(binaryFile).call();
+ RevCommit child1 = git.commit().setMessage("THEIRS COMMIT").call();
+ String fileHashInChild1 = getFileHashInWorkTree(git, binaryFile);
+
+ git.checkout().setCreateBranch(true).setStartPoint(parent)
+ .setName("side").call();
+
+ writeTrashFile(binaryFile, "\u0002\u0000");
+ git.add().addFilepattern(binaryFile).call();
+ git.commit().setMessage("OURS COMMIT").call();
+ String fileHashInChild2 = getFileHashInWorkTree(git, binaryFile);
+
+ MergeResult mergeResult = git.merge().setStrategy(strategy)
+ .include(child1).call();
+
+ // check if the merge caused a conflict
+ assertTrue(mergeResult.getConflicts() != null
+ && !mergeResult.getConflicts().isEmpty());
+ String fileHashInChild2AfterMerge = getFileHashInWorkTree(git,
+ binaryFile);
+
+ // check if the file content changed during a conflicting merge
+ assertEquals(fileHashInChild2AfterMerge, fileHashInChild2);
+
+ Set<String> hashesInIndexFile = new HashSet<>();
+ DirCache indexContent = git.getRepository().readDirCache();
+ for (int i = 0; i < indexContent.getEntryCount(); ++i) {
+ DirCacheEntry indexEntry = indexContent.getEntry(i);
+ if (binaryFile.equals(indexEntry.getPathString())) {
+ hashesInIndexFile.add(indexEntry.getObjectId().name());
+ }
+ }
+
+ // check if all the three stages are added to index file
+ assertTrue(hashesInIndexFile.contains(fileHashInBase));
+ assertTrue(hashesInIndexFile.contains(fileHashInChild1));
+ assertTrue(hashesInIndexFile.contains(fileHashInChild2));
+ }
+
+ private String getFileHashInWorkTree(Git git, String filePath)
+ throws IOException {
+ Repository repository = git.getRepository();
+ ObjectInserter objectInserter = repository.newObjectInserter();
+
+ File conflictingFile = new File(repository.getWorkTree(), filePath);
+ byte[] fileContent = Files.readAllBytes(conflictingFile.toPath());
+ ObjectId blobId = objectInserter.insert(Constants.OBJ_BLOB,
+ fileContent);
+ objectInserter.flush();
+ return blobId.name();
}
private void writeSubmodule(String path, ObjectId commit)
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/SimpleMergeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/SimpleMergeTest.java
index 798aebe3b0..0016adfb66 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/SimpleMergeTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/SimpleMergeTest.java
@@ -16,6 +16,8 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
+import java.time.Instant;
+import java.time.ZoneOffset;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheBuilder;
@@ -375,7 +377,8 @@ public class SimpleMergeTest extends SampleDataRepositoryTestCase {
ObjectId[] parentIds) throws Exception {
CommitBuilder c = new CommitBuilder();
c.setTreeId(treeB.writeTree(odi));
- c.setAuthor(new PersonIdent("A U Thor", "a.u.thor", 1L, 0));
+ c.setAuthor(new PersonIdent("A U Thor", "a.u.thor",
+ Instant.ofEpochMilli(1L), ZoneOffset.UTC));
c.setCommitter(c.getAuthor());
c.setParentIds(parentIds);
c.setMessage("Tree " + c.getTreeId().name());
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/patch/PatchApplierTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/patch/PatchApplierTest.java
index e2637257c5..5507f8572d 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/patch/PatchApplierTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/patch/PatchApplierTest.java
@@ -24,6 +24,7 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
+
import org.eclipse.jgit.annotations.Nullable;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.attributes.FilterCommand;
@@ -47,8 +48,7 @@ import org.junit.runner.RunWith;
import org.junit.runners.Suite;
@RunWith(Suite.class)
-@Suite.SuiteClasses({
- PatchApplierTest.WithWorktree. class, //
+@Suite.SuiteClasses({ PatchApplierTest.WithWorktree.class, //
PatchApplierTest.InCore.class, //
})
public class PatchApplierTest {
@@ -127,6 +127,20 @@ public class PatchApplierTest {
}
}
+ protected Result applyPatchAllowConflicts() throws IOException {
+ InputStream patchStream = getTestResource(name + ".patch");
+ Patch patch = new Patch();
+ patch.parse(patchStream);
+ if (inCore) {
+ try (ObjectInserter oi = db.newObjectInserter()) {
+ return new PatchApplier(db, baseTip, oi).allowConflicts()
+ .applyPatch(patch);
+ }
+ }
+ return new PatchApplier(db).allowConflicts()
+ .applyPatch(patch);
+ }
+
protected static InputStream getTestResource(String patchFile) {
return PatchApplierTest.class.getClassLoader()
.getResourceAsStream("org/eclipse/jgit/diff/" + patchFile);
@@ -168,6 +182,13 @@ public class PatchApplierTest {
verifyContent(result, aName, exists);
}
+ void verifyChange(Result result, String aName, boolean exists,
+ int numConflicts) throws Exception {
+ assertEquals(numConflicts, result.getErrors().size());
+ assertEquals(1, result.getPaths().size());
+ verifyContent(result, aName, exists);
+ }
+
protected byte[] readBlob(ObjectId treeish, String path)
throws Exception {
try (TestRepository<?> tr = new TestRepository<>(db);
@@ -345,6 +366,44 @@ public class PatchApplierTest {
}
@Test
+ public void testConflictMarkers() throws Exception {
+ init("allowconflict", true, true);
+
+ Result result = applyPatchAllowConflicts();
+
+ assertEquals(result.getErrors().size(), 1);
+ PatchApplier.Result.Error error = result.getErrors().get(0);
+ assertEquals("cannot apply hunk", error.msg);
+ assertEquals("allowconflict", error.oldFileName);
+ assertTrue(error.isGitConflict());
+ verifyChange(result, "allowconflict", true, 1);
+ }
+
+ @Test
+ public void testConflictMarkersOutOfBounds() throws Exception {
+ init("ConflictOutOfBounds", true, true);
+
+ Result result = applyPatchAllowConflicts();
+
+ assertEquals(result.getErrors().size(), 1);
+ PatchApplier.Result.Error error = result.getErrors().get(0);
+ assertEquals("cannot apply hunk", error.msg);
+ assertEquals("ConflictOutOfBounds", error.oldFileName);
+ assertTrue(error.isGitConflict());
+ verifyChange(result, "ConflictOutOfBounds", true, 1);
+ }
+
+ @Test
+ public void testConflictMarkersFileDeleted() throws Exception {
+ init("allowconflict_file_deleted", false, false);
+
+ Result result = applyPatchAllowConflicts();
+
+ assertEquals(1, result.getErrors().size());
+ assertEquals(0, result.getPaths().size());
+ }
+
+ @Test
public void testShiftUp() throws Exception {
init("ShiftUp");
@@ -495,6 +554,14 @@ public class PatchApplierTest {
Result result = applyPatch();
verifyChange(result, "x_last_rm_nl");
}
+
+ @Test
+ public void testVeryLongFile() throws Exception {
+ init("very_long_file");
+
+ Result result = applyPatch();
+ verifyChange(result, "very_long_file");
+ }
}
public static class WithWorktree extends Base {
@@ -892,5 +959,30 @@ public class PatchApplierTest {
FilterCommandRegistry.unregister("jgit://builtin/a2e/smudge");
}
}
+
+ private void dotGitTest(String fileName) throws Exception {
+ init(fileName, false, false);
+ Result result = null;
+ IOException ex = null;
+ try {
+ result = applyPatch();
+ } catch (IOException e) {
+ ex = e;
+ }
+ assertTrue(ex != null
+ || (result != null && !result.getErrors().isEmpty()));
+ File b = new File(new File(trash, ".git"), "b");
+ assertFalse(".git/b should not exist", b.exists());
+ }
+
+ @Test
+ public void testDotGit() throws Exception {
+ dotGitTest("dotgit");
+ }
+
+ @Test
+ public void testDotGit2() throws Exception {
+ dotGitTest("dotgit2");
+ }
}
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/patch/PatchCcErrorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/patch/PatchCcErrorTest.java
index 71bda46ee5..8335c07b1f 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/patch/PatchCcErrorTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/patch/PatchCcErrorTest.java
@@ -10,7 +10,6 @@
package org.eclipse.jgit.patch;
-import static java.lang.Integer.valueOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
@@ -35,7 +34,7 @@ public class PatchCcErrorTest {
assertSame(FormatError.Severity.ERROR, e.getSeverity());
assertEquals(MessageFormat.format(
JGitText.get().truncatedHunkLinesMissingForAncestor,
- valueOf(1), valueOf(1)), e.getMessage());
+ Integer.valueOf(1), Integer.valueOf(1)), e.getMessage());
assertEquals(346, e.getOffset());
assertTrue(e.getLineText().startsWith(
"@@@ -55,12 -163,13 +163,15 @@@ public "));
@@ -45,7 +44,7 @@ public class PatchCcErrorTest {
assertSame(FormatError.Severity.ERROR, e.getSeverity());
assertEquals(MessageFormat.format(
JGitText.get().truncatedHunkLinesMissingForAncestor,
- valueOf(2), valueOf(2)), e.getMessage());
+ Integer.valueOf(2), Integer.valueOf(2)), e.getMessage());
assertEquals(346, e.getOffset());
assertTrue(e.getLineText().startsWith(
"@@@ -55,12 -163,13 +163,15 @@@ public "));
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revplot/AbstractPlotRendererTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revplot/AbstractPlotRendererTest.java
index 49f832a1aa..e4bd8506e3 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revplot/AbstractPlotRendererTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revplot/AbstractPlotRendererTest.java
@@ -11,7 +11,7 @@ package org.eclipse.jgit.revplot;
import static org.junit.Assert.assertEquals;
-import java.util.LinkedList;
+import java.util.ArrayList;
import java.util.List;
import org.eclipse.jgit.api.Git;
@@ -70,7 +70,7 @@ public class AbstractPlotRendererTest extends RepositoryTestCase {
private static class TestPlotRenderer extends
AbstractPlotRenderer<PlotLane, Object> {
- List<Integer> indentations = new LinkedList<>();
+ List<Integer> indentations = new ArrayList<>();
@Override
protected int drawLabel(int x, int y, Ref ref) {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/BitmapWalkerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/BitmapWalkerTest.java
new file mode 100644
index 0000000000..a59a71d19f
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/BitmapWalkerTest.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2023, Google Inc.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.revwalk;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import java.util.List;
+
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.internal.storage.file.GC;
+import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.BitmapIndex;
+import org.eclipse.jgit.lib.BitmapIndex.Bitmap;
+import org.eclipse.jgit.lib.BitmapIndex.BitmapBuilder;
+import org.eclipse.jgit.lib.BitmapIndex.BitmapLookupListener;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.junit.Before;
+import org.junit.Test;
+
+public class BitmapWalkerTest extends LocalDiskRepositoryTestCase {
+
+ private static final String MAIN = "refs/heads/main";
+
+ TestRepository<FileRepository> repo;
+
+ RevCommit tipWithBitmap;
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ FileRepository db = createWorkRepository();
+ repo = new TestRepository<>(db);
+
+ RevCommit base = repo.commit().create();
+ RevCommit one = repo.commit().parent(base).create();
+ tipWithBitmap = repo.commit().parent(one).create();
+ repo.update(MAIN, tipWithBitmap);
+
+ GC gc = new GC(repo.getRepository());
+ gc.setAuto(false);
+ gc.gc().get();
+
+ assertNotNull(repo.getRevWalk().getObjectReader().getBitmapIndex());
+ }
+
+ private static class BitmapWalkCounter implements BitmapLookupListener {
+ int withBitmap;
+
+ int withoutBitmap;
+
+ @Override
+ public void onBitmapFound(AnyObjectId oid) {
+ withBitmap += 1;
+ }
+
+ @Override
+ public void onBitmapNotFound(AnyObjectId oid) {
+ withoutBitmap += 1;
+ }
+ }
+
+ @Test
+ public void counters_bitmapAtTip() throws Exception {
+ try (RevWalk rw = repo.getRevWalk();
+ ObjectReader or = rw.getObjectReader()) {
+ BitmapWalkCounter counter = new BitmapWalkCounter();
+ BitmapIndex bitmapIndex = or.getBitmapIndex();
+ bitmapIndex.addBitmapLookupListener(counter);
+ BitmapWalker bw = new BitmapWalker(rw.toObjectWalkWithSameObjects(),
+ bitmapIndex, NullProgressMonitor.INSTANCE);
+ BitmapBuilder bitmap = bw.findObjects(List.of(tipWithBitmap), null,
+ true);
+ // First commit has a tree, so in total 4 objects
+ assertEquals(4, bitmap.cardinality());
+ assertEquals(1, counter.withBitmap);
+ assertEquals(0, counter.withoutBitmap);
+ assertEquals(0, bw.getCountOfBitmapIndexMisses());
+ }
+ }
+
+ @Test
+ public void counters_bitmapAfterAStep() throws Exception {
+ System.out.println("Old tip: " + tipWithBitmap);
+ RevCommit newTip = repo.commit().parent(tipWithBitmap).create();
+ System.out.println("New tip: " + newTip);
+ try (RevWalk rw = repo.getRevWalk();
+ ObjectReader or = rw.getObjectReader()) {
+ BitmapWalkCounter counter = new BitmapWalkCounter();
+ BitmapIndex bitmapIndex = or.getBitmapIndex();
+ bitmapIndex.addBitmapLookupListener(counter);
+ BitmapWalker bw = new BitmapWalker(rw.toObjectWalkWithSameObjects(),
+ bitmapIndex, NullProgressMonitor.INSTANCE);
+
+ bw.findObjects(List.of(newTip), null, true);
+
+ assertEquals(1, counter.withBitmap);
+ // It checks bitmap before marking as interesting, and again in the
+ // walk
+ assertEquals(2, counter.withoutBitmap);
+ assertEquals(1, bw.getCountOfBitmapIndexMisses());
+ }
+ }
+
+ @Test
+ public void counters_bitmapAfterThreeSteps() throws Exception {
+ RevCommit newOne = repo.commit().parent(tipWithBitmap).create();
+ RevCommit newTwo = repo.commit().parent(newOne).create();
+ RevCommit newTip = repo.commit().parent(newTwo).create();
+
+ try (RevWalk rw = repo.getRevWalk();
+ ObjectReader or = rw.getObjectReader()) {
+ BitmapWalkCounter counter = new BitmapWalkCounter();
+ BitmapIndex bitmapIndex = or.getBitmapIndex();
+ bitmapIndex.addBitmapLookupListener(counter);
+ BitmapWalker bw = new BitmapWalker(rw.toObjectWalkWithSameObjects(),
+ bitmapIndex, NullProgressMonitor.INSTANCE);
+
+ bw.findObjects(List.of(newTip), null, true);
+
+ assertEquals(1, counter.withBitmap);
+ assertEquals(4, counter.withoutBitmap);
+ assertEquals(3, bw.getCountOfBitmapIndexMisses());
+ }
+ }
+
+ @Test
+ public void counters_bitmapAfterThreeStepsWithSeen() throws Exception {
+ RevCommit newOne = repo.commit().parent(tipWithBitmap).create();
+ RevCommit newTwo = repo.commit().parent(newOne).create();
+ RevCommit newTip = repo.commit().parent(newTwo).create();
+
+ try (RevWalk rw = repo.getRevWalk();
+ ObjectReader or = rw.getObjectReader()) {
+ BitmapIndex bitmapIndex = or.getBitmapIndex();
+ Bitmap seen = bitmapIndex.getBitmap(tipWithBitmap);
+ BitmapBuilder seenBB = bitmapIndex.newBitmapBuilder().or(seen);
+ BitmapWalkCounter counter = new BitmapWalkCounter();
+ bitmapIndex.addBitmapLookupListener(counter);
+ BitmapWalker bw = new BitmapWalker(rw.toObjectWalkWithSameObjects(),
+ bitmapIndex, NullProgressMonitor.INSTANCE);
+
+ bw.findObjects(List.of(newTip), seenBB, true);
+
+ assertEquals(0, counter.withBitmap);
+ assertEquals(4, counter.withoutBitmap);
+ assertEquals(3, bw.getCountOfBitmapIndexMisses());
+ }
+ }
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/DateRevPriorityQueueTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/DateRevPriorityQueueTest.java
new file mode 100644
index 0000000000..369e2fae72
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/DateRevPriorityQueueTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2023, GerritForge Inc. and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.revwalk;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+
+import org.junit.Test;
+
+public class DateRevPriorityQueueTest extends RevQueueTestCase<DateRevPriorityQueue> {
+ @Override
+ protected DateRevPriorityQueue create() {
+ return new DateRevPriorityQueue();
+ }
+
+ @Override
+ @Test
+ public void testEmpty() throws Exception {
+ super.testEmpty();
+ assertNull(q.peek());
+ assertEquals(Generator.SORT_COMMIT_TIME_DESC, q.outputType());
+ }
+
+ @Test
+ public void testCloneEmpty() throws Exception {
+ q = new DateRevPriorityQueue(AbstractRevQueue.EMPTY_QUEUE);
+ assertNull(q.next());
+ }
+
+ @Test
+ public void testInsertOutOfOrder() throws Exception {
+ final RevCommit a = parseBody(commit());
+ final RevCommit b = parseBody(commit(10, a));
+ final RevCommit c1 = parseBody(commit(5, b));
+ final RevCommit c2 = parseBody(commit(-50, b));
+
+ q.add(c2);
+ q.add(a);
+ q.add(b);
+ q.add(c1);
+
+ assertCommit(c1, q.next());
+ assertCommit(b, q.next());
+ assertCommit(a, q.next());
+ assertCommit(c2, q.next());
+ assertNull(q.next());
+ }
+
+ @Test
+ public void testInsertTie() throws Exception {
+ final RevCommit a = parseBody(commit());
+ final RevCommit b = parseBody(commit(0, a));
+ final RevCommit c = parseBody(commit(0, b));
+
+ {
+ q = create();
+ q.add(a);
+ q.add(b);
+ q.add(c);
+
+ assertCommit(a, q.next());
+ assertCommit(b, q.next());
+ assertCommit(c, q.next());
+ assertNull(q.next());
+ }
+ {
+ q = create();
+ q.add(c);
+ q.add(b);
+ q.add(a);
+
+ assertCommit(c, q.next());
+ assertCommit(b, q.next());
+ assertCommit(a, q.next());
+ assertNull(q.next());
+ }
+ }
+
+ @Test
+ public void testCloneFIFO() throws Exception {
+ final RevCommit a = parseBody(commit());
+ final RevCommit b = parseBody(commit(200, a));
+ final RevCommit c = parseBody(commit(200, b));
+
+ final FIFORevQueue src = new FIFORevQueue();
+ src.add(a);
+ src.add(b);
+ src.add(c);
+
+ q = new DateRevPriorityQueue(src);
+ assertFalse(q.everbodyHasFlag(RevWalk.UNINTERESTING));
+ assertFalse(q.anybodyHasFlag(RevWalk.UNINTERESTING));
+ assertCommit(c, q.peek());
+ assertCommit(c, q.peek());
+
+ assertCommit(c, q.next());
+ assertCommit(b, q.next());
+ assertCommit(a, q.next());
+ assertNull(q.next());
+ }
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/FooterLineTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/FooterLineTest.java
index 113f3bee82..657c3d242f 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/FooterLineTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/FooterLineTest.java
@@ -16,76 +16,101 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
-import java.io.IOException;
import java.util.List;
import org.eclipse.jgit.junit.RepositoryTestCase;
-import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.junit.Test;
public class FooterLineTest extends RepositoryTestCase {
@Test
- public void testNoFooters_EmptyBody() throws IOException {
- final RevCommit commit = parse("");
- final List<FooterLine> footers = commit.getFooterLines();
+ public void testNoFooters_EmptyBody() {
+ String msg = buildMessage("");
+ List<FooterLine> footers = FooterLine.fromMessage(msg);
assertNotNull(footers);
assertEquals(0, footers.size());
}
@Test
- public void testNoFooters_NewlineOnlyBody1() throws IOException {
- final RevCommit commit = parse("\n");
- final List<FooterLine> footers = commit.getFooterLines();
+ public void testNoFooters_NewlineOnlyBody1() {
+ String msg = buildMessage("\n");
+ List<FooterLine> footers = FooterLine.fromMessage(msg);
assertNotNull(footers);
assertEquals(0, footers.size());
}
@Test
- public void testNoFooters_NewlineOnlyBody5() throws IOException {
- final RevCommit commit = parse("\n\n\n\n\n");
- final List<FooterLine> footers = commit.getFooterLines();
+ public void testNoFooters_NewlineOnlyBody5() {
+ String msg = buildMessage("\n\n\n\n\n");
+ List<FooterLine> footers = FooterLine.fromMessage(msg);
assertNotNull(footers);
assertEquals(0, footers.size());
}
@Test
- public void testNoFooters_OneLineBodyNoLF() throws IOException {
- final RevCommit commit = parse("this is a commit");
- final List<FooterLine> footers = commit.getFooterLines();
+ public void testNoFooters_OneLineBodyNoLF() {
+ String msg = buildMessage("this is a commit");
+ List<FooterLine> footers = FooterLine.fromMessage(msg);
assertNotNull(footers);
assertEquals(0, footers.size());
}
@Test
- public void testNoFooters_OneLineBodyWithLF() throws IOException {
- final RevCommit commit = parse("this is a commit\n");
- final List<FooterLine> footers = commit.getFooterLines();
+ public void testNoFooters_OneLineBodyWithLF() {
+ String msg = buildMessage("this is a commit\n");
+ List<FooterLine> footers = FooterLine.fromMessage(msg);
assertNotNull(footers);
assertEquals(0, footers.size());
}
@Test
- public void testNoFooters_ShortBodyNoLF() throws IOException {
- final RevCommit commit = parse("subject\n\nbody of commit");
- final List<FooterLine> footers = commit.getFooterLines();
+ public void testNoFooters_ShortBodyNoLF() {
+ String msg = buildMessage("subject\n\nbody of commit");
+ List<FooterLine> footers = FooterLine.fromMessage(msg);
assertNotNull(footers);
assertEquals(0, footers.size());
}
@Test
- public void testNoFooters_ShortBodyWithLF() throws IOException {
- final RevCommit commit = parse("subject\n\nbody of commit\n");
- final List<FooterLine> footers = commit.getFooterLines();
+ public void testNoFooters_ShortBodyWithLF() {
+ String msg = buildMessage("subject\n\nbody of commit\n");
+ List<FooterLine> footers = FooterLine.fromMessage(msg);
assertNotNull(footers);
assertEquals(0, footers.size());
}
@Test
- public void testSignedOffBy_OneUserNoLF() throws IOException {
- final RevCommit commit = parse("subject\n\nbody of commit\n" + "\n"
- + "Signed-off-by: A. U. Thor <a@example.com>");
- final List<FooterLine> footers = commit.getFooterLines();
+ public void testNoFooters_noRawMsg_SingleLineNoHeaders() {
+ String noRawMsg = "commit message with no header lines\n";
+ List<FooterLine> footers = FooterLine.fromMessage(noRawMsg);
+ assertNotNull(footers);
+ assertEquals(0, footers.size());
+ }
+
+ @Test
+ public void testOneFooter_noRawMsg_MultiParagraphNoHeaders() {
+ String noRawMsg = "subject\n\n"
+ + "Not: footer\n\n"
+ + "Footer: value\n";
+ List<FooterLine> footers = FooterLine.fromMessage(noRawMsg);
+ assertNotNull(footers);
+ assertEquals(1, footers.size());
+ }
+
+ @Test
+ public void testOneFooter_longSubject_NoHeaders() {
+ String noRawMsg = "50+ chars loooooooooooooong custom commit message.\n\n"
+ + "Footer: value\n";
+ List<FooterLine> footers = FooterLine.fromMessage(noRawMsg);
+ assertNotNull(footers);
+ assertEquals(1, footers.size());
+ }
+
+ @Test
+ public void testSignedOffBy_OneUserNoLF() {
+ String msg = buildMessage("subject\n\nbody of commit\n" + "\n"
+ + "Signed-off-by: A. U. Thor <a@example.com>");
+ List<FooterLine> footers = FooterLine.fromMessage(msg);
FooterLine f;
assertNotNull(footers);
@@ -98,10 +123,10 @@ public class FooterLineTest extends RepositoryTestCase {
}
@Test
- public void testSignedOffBy_OneUserWithLF() throws IOException {
- final RevCommit commit = parse("subject\n\nbody of commit\n" + "\n"
- + "Signed-off-by: A. U. Thor <a@example.com>\n");
- final List<FooterLine> footers = commit.getFooterLines();
+ public void testSignedOffBy_OneUserWithLF() {
+ String msg = buildMessage("subject\n\nbody of commit\n" + "\n"
+ + "Signed-off-by: A. U. Thor <a@example.com>\n");
+ List<FooterLine> footers = FooterLine.fromMessage(msg);
FooterLine f;
assertNotNull(footers);
@@ -114,14 +139,13 @@ public class FooterLineTest extends RepositoryTestCase {
}
@Test
- public void testSignedOffBy_IgnoreWhitespace() throws IOException {
+ public void testSignedOffBy_IgnoreWhitespace() {
// We only ignore leading whitespace on the value, trailing
// is assumed part of the value.
//
- final RevCommit commit = parse("subject\n\nbody of commit\n" + "\n"
- + "Signed-off-by: A. U. Thor <a@example.com> \n");
- final List<FooterLine> footers = commit.getFooterLines();
- FooterLine f;
+ String msg = buildMessage("subject\n\nbody of commit\n" + "\n"
+ + "Signed-off-by: A. U. Thor <a@example.com> \n");
+ List<FooterLine> footers = FooterLine.fromMessage(msg); FooterLine f;
assertNotNull(footers);
assertEquals(1, footers.size());
@@ -133,10 +157,10 @@ public class FooterLineTest extends RepositoryTestCase {
}
@Test
- public void testEmptyValueNoLF() throws IOException {
- final RevCommit commit = parse("subject\n\nbody of commit\n" + "\n"
- + "Signed-off-by:");
- final List<FooterLine> footers = commit.getFooterLines();
+ public void testEmptyValueNoLF() {
+ String msg = buildMessage("subject\n\nbody of commit\n" + "\n"
+ + "Signed-off-by:");
+ List<FooterLine> footers = FooterLine.fromMessage(msg);
FooterLine f;
assertNotNull(footers);
@@ -149,10 +173,10 @@ public class FooterLineTest extends RepositoryTestCase {
}
@Test
- public void testEmptyValueWithLF() throws IOException {
- final RevCommit commit = parse("subject\n\nbody of commit\n" + "\n"
- + "Signed-off-by:\n");
- final List<FooterLine> footers = commit.getFooterLines();
+ public void testEmptyValueWithLF() {
+ String msg = buildMessage("subject\n\nbody of commit\n" + "\n"
+ + "Signed-off-by:\n");
+ List<FooterLine> footers = FooterLine.fromMessage(msg);
FooterLine f;
assertNotNull(footers);
@@ -165,10 +189,10 @@ public class FooterLineTest extends RepositoryTestCase {
}
@Test
- public void testShortKey() throws IOException {
- final RevCommit commit = parse("subject\n\nbody of commit\n" + "\n"
- + "K:V\n");
- final List<FooterLine> footers = commit.getFooterLines();
+ public void testShortKey() {
+ String msg = buildMessage("subject\n\nbody of commit\n" + "\n"
+ + "K:V\n");
+ List<FooterLine> footers = FooterLine.fromMessage(msg);
FooterLine f;
assertNotNull(footers);
@@ -181,10 +205,10 @@ public class FooterLineTest extends RepositoryTestCase {
}
@Test
- public void testNonDelimtedEmail() throws IOException {
- final RevCommit commit = parse("subject\n\nbody of commit\n" + "\n"
- + "Acked-by: re@example.com\n");
- final List<FooterLine> footers = commit.getFooterLines();
+ public void testNonDelimtedEmail() {
+ String msg = buildMessage("subject\n\nbody of commit\n" + "\n"
+ + "Acked-by: re@example.com\n");
+ List<FooterLine> footers = FooterLine.fromMessage(msg);
FooterLine f;
assertNotNull(footers);
@@ -197,10 +221,10 @@ public class FooterLineTest extends RepositoryTestCase {
}
@Test
- public void testNotEmail() throws IOException {
- final RevCommit commit = parse("subject\n\nbody of commit\n" + "\n"
- + "Acked-by: Main Tain Er\n");
- final List<FooterLine> footers = commit.getFooterLines();
+ public void testNotEmail() {
+ String msg = buildMessage("subject\n\nbody of commit\n" + "\n"
+ + "Acked-by: Main Tain Er\n");
+ List<FooterLine> footers = FooterLine.fromMessage(msg);
FooterLine f;
assertNotNull(footers);
@@ -213,15 +237,15 @@ public class FooterLineTest extends RepositoryTestCase {
}
@Test
- public void testSignedOffBy_ManyUsers() throws IOException {
- final RevCommit commit = parse("subject\n\nbody of commit\n"
- + "Not-A-Footer-Line: this line must not be read as a footer\n"
- + "\n" // paragraph break, now footers appear in final block
- + "Signed-off-by: A. U. Thor <a@example.com>\n"
- + "CC: <some.mailing.list@example.com>\n"
- + "Acked-by: Some Reviewer <sr@example.com>\n"
- + "Signed-off-by: Main Tain Er <mte@example.com>\n");
- final List<FooterLine> footers = commit.getFooterLines();
+ public void testSignedOffBy_ManyUsers() {
+ String msg = buildMessage("subject\n\nbody of commit\n"
+ + "Not-A-Footer-Line: this line must not be read as a footer\n"
+ + "\n" // paragraph break, now footers appear in final block
+ + "Signed-off-by: A. U. Thor <a@example.com>\n"
+ + "CC: <some.mailing.list@example.com>\n"
+ + "Acked-by: Some Reviewer <sr@example.com>\n"
+ + "Signed-off-by: Main Tain Er <mte@example.com>\n");
+ List<FooterLine> footers = FooterLine.fromMessage(msg);
FooterLine f;
assertNotNull(footers);
@@ -249,16 +273,16 @@ public class FooterLineTest extends RepositoryTestCase {
}
@Test
- public void testSignedOffBy_SkipNonFooter() throws IOException {
- final RevCommit commit = parse("subject\n\nbody of commit\n"
- + "Not-A-Footer-Line: this line must not be read as a footer\n"
- + "\n" // paragraph break, now footers appear in final block
- + "Signed-off-by: A. U. Thor <a@example.com>\n"
- + "CC: <some.mailing.list@example.com>\n"
- + "not really a footer line but we'll skip it anyway\n"
- + "Acked-by: Some Reviewer <sr@example.com>\n"
- + "Signed-off-by: Main Tain Er <mte@example.com>\n");
- final List<FooterLine> footers = commit.getFooterLines();
+ public void testSignedOffBy_SkipNonFooter() {
+ String msg = buildMessage("subject\n\nbody of commit\n"
+ + "Not-A-Footer-Line: this line must not be read as a footer\n"
+ + "\n" // paragraph break, now footers appear in final block
+ + "Signed-off-by: A. U. Thor <a@example.com>\n"
+ + "CC: <some.mailing.list@example.com>\n"
+ + "not really a footer line but we'll skip it anyway\n"
+ + "Acked-by: Some Reviewer <sr@example.com>\n"
+ + "Signed-off-by: Main Tain Er <mte@example.com>\n");
+ List<FooterLine> footers = FooterLine.fromMessage(msg);
FooterLine f;
assertNotNull(footers);
@@ -267,30 +291,39 @@ public class FooterLineTest extends RepositoryTestCase {
f = footers.get(0);
assertEquals("Signed-off-by", f.getKey());
assertEquals("A. U. Thor <a@example.com>", f.getValue());
+ assertEquals(217, f.getStartOffset());
+ assertEquals(258, f.getEndOffset());
f = footers.get(1);
assertEquals("CC", f.getKey());
assertEquals("<some.mailing.list@example.com>", f.getValue());
+ assertEquals(259, f.getStartOffset());
+ assertEquals(305, f.getEndOffset());
f = footers.get(2);
assertEquals("Acked-by", f.getKey());
assertEquals("Some Reviewer <sr@example.com>", f.getValue());
+ assertEquals(356, f.getStartOffset());
+ assertEquals(396, f.getEndOffset());
f = footers.get(3);
assertEquals("Signed-off-by", f.getKey());
assertEquals("Main Tain Er <mte@example.com>", f.getValue());
+ assertEquals(397, f.getStartOffset());
+ assertEquals(442, f.getEndOffset());
}
@Test
- public void testFilterFootersIgnoreCase() throws IOException {
- final RevCommit commit = parse("subject\n\nbody of commit\n"
- + "Not-A-Footer-Line: this line must not be read as a footer\n"
- + "\n" // paragraph break, now footers appear in final block
- + "Signed-Off-By: A. U. Thor <a@example.com>\n"
- + "CC: <some.mailing.list@example.com>\n"
- + "Acked-by: Some Reviewer <sr@example.com>\n"
- + "signed-off-by: Main Tain Er <mte@example.com>\n");
- final List<String> footers = commit.getFooterLines("signed-off-by");
+ public void testFilterFootersIgnoreCase() {
+ String msg = buildMessage("subject\n\nbody of commit\n"
+ + "Not-A-Footer-Line: this line must not be read as a footer\n"
+ + "\n" // paragraph break, now footers appear in final block
+ + "Signed-Off-By: A. U. Thor <a@example.com>\n"
+ + "CC: <some.mailing.list@example.com>\n"
+ + "Acked-by: Some Reviewer <sr@example.com>\n"
+ + "signed-off-by: Main Tain Er <mte@example.com>\n");
+ List<String> footers = FooterLine.getValues(
+ FooterLine.fromMessage(msg), "signed-off-by");
assertNotNull(footers);
assertEquals(2, footers.size());
@@ -300,38 +333,104 @@ public class FooterLineTest extends RepositoryTestCase {
}
@Test
- public void testMatchesBugId() throws IOException {
- final RevCommit commit = parse("this is a commit subject for test\n"
- + "\n" // paragraph break, now footers appear in final block
- + "Simple-Bug-Id: 42\n");
- final List<FooterLine> footers = commit.getFooterLines();
+ public void testMatchesBugId() {
+ String msg = buildMessage("this is a commit subject for test\n"
+ + "\n" // paragraph break, now footers appear in final block
+ + "Simple-Bug-Id: 42\n");
+ List<FooterLine> footers = FooterLine.fromMessage(msg);
assertNotNull(footers);
assertEquals(1, footers.size());
- final FooterLine line = footers.get(0);
+ FooterLine line = footers.get(0);
assertNotNull(line);
assertEquals("Simple-Bug-Id", line.getKey());
assertEquals("42", line.getValue());
- final FooterKey bugid = new FooterKey("Simple-Bug-Id");
+ FooterKey bugid = new FooterKey("Simple-Bug-Id");
assertTrue("matches Simple-Bug-Id", line.matches(bugid));
assertFalse("not Signed-off-by", line.matches(FooterKey.SIGNED_OFF_BY));
assertFalse("not CC", line.matches(FooterKey.CC));
}
- private RevCommit parse(String msg) throws IOException {
- final StringBuilder buf = new StringBuilder();
+ @Test
+ public void testMultilineFooters() {
+ String msg = buildMessage("subject\n\nbody of commit\n"
+ + "Not-A-Footer-Line: this line must not be read as a footer\n"
+ + "\n" // paragraph break, now footers appear in final block
+ + "Notes: The change must not be merged until dependency ABC is\n"
+ + " updated.\n"
+ + "CC: <some.mailing.list@example.com>\n"
+ + "not really a footer line but we'll skip it anyway\n"
+ + "Acked-by: Some Reviewer <sr@example.com>\n");
+ List<FooterLine> footers = FooterLine.fromMessage(msg);
+ FooterLine f;
+
+ assertNotNull(footers);
+ assertEquals(3, footers.size());
+
+ f = footers.get(0);
+ assertEquals("Notes", f.getKey());
+ assertEquals(
+ "The change must not be merged until dependency ABC is updated.",
+ f.getValue());
+
+ f = footers.get(1);
+ assertEquals("CC", f.getKey());
+ assertEquals("<some.mailing.list@example.com>", f.getValue());
+
+ f = footers.get(2);
+ assertEquals("Acked-by", f.getKey());
+ assertEquals("Some Reviewer <sr@example.com>", f.getValue());
+ }
+
+ @Test
+ public void testMultilineFooters_multipleWhitespaceAreAllowed() {
+ String msg = buildMessage("subject\n\nbody of commit\n"
+ + "Not-A-Footer-Line: this line must not be read as a footer\n"
+ + "\n" // paragraph break, now footers appear in final block
+ + "Notes: The change must not be merged until dependency ABC is\n"
+ + " updated.\n");
+ List<FooterLine> footers = FooterLine.fromMessage(msg);
+ FooterLine f;
+
+ assertNotNull(footers);
+ assertEquals(1, footers.size());
+
+ f = footers.get(0);
+ assertEquals("Notes", f.getKey());
+ assertEquals(
+ "The change must not be merged until dependency ABC is updated.",
+ f.getValue());
+ }
+
+ @Test
+ public void testFirstLineNeverFooter() {
+ String msg = buildMessage(
+ String.join("\n", "First-Line: is never a footer", "Foo: ter",
+ "1-is: also a footer"));
+ List<FooterLine> footers = FooterLine.fromMessage(msg);
+ assertNotNull(footers);
+ assertEquals(2, footers.size());
+ }
+
+ @Test
+ public void testLineAfterFooters() {
+ String msg = buildMessage(
+ String.join("\n", "Subject line: is never a footer", "Foo: ter",
+ "1-is: also a footer", "this is not a footer"));
+ List<FooterLine> footers = FooterLine.fromMessage(msg);
+ assertNotNull(footers);
+ assertEquals(2, footers.size());
+ }
+
+ private String buildMessage(String msg) {
+ StringBuilder buf = new StringBuilder();
buf.append("tree " + ObjectId.zeroId().name() + "\n");
buf.append("author A. U. Thor <a@example.com> 1 +0000\n");
buf.append("committer A. U. Thor <a@example.com> 1 +0000\n");
buf.append("\n");
buf.append(msg);
-
- try (RevWalk walk = new RevWalk(db)) {
- RevCommit c = new RevCommit(ObjectId.zeroId());
- c.parseCanonical(walk, Constants.encode(buf.toString()));
- return c;
- }
+ return buf.toString();
}
}
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 82af34ded2..014ff928a8 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
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2008-2009, Google Inc. and others
+ * Copyright (C) 2008, 2024 Google Inc. and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -23,7 +23,9 @@ import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.UnsupportedCharsetException;
-import java.util.TimeZone;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
import org.eclipse.jgit.junit.RepositoryTestCase;
import org.eclipse.jgit.lib.CommitBuilder;
@@ -94,18 +96,17 @@ public class RevCommitParseTest extends RepositoryTestCase {
assertNotNull(cAuthor);
assertEquals(authorName, cAuthor.getName());
assertEquals(authorEmail, cAuthor.getEmailAddress());
- assertEquals((long) authorTime * 1000, cAuthor.getWhen().getTime());
- assertEquals(TimeZone.getTimeZone("GMT" + authorTimeZone),
- cAuthor.getTimeZone());
+ assertEquals(Instant.ofEpochSecond(authorTime),
+ cAuthor.getWhenAsInstant());
+ assertEquals(ZoneId.of(authorTimeZone), cAuthor.getZoneId());
final PersonIdent cCommitter = c.getCommitterIdent();
assertNotNull(cCommitter);
assertEquals(committerName, cCommitter.getName());
assertEquals(committerEmail, cCommitter.getEmailAddress());
- assertEquals((long) committerTime * 1000,
- cCommitter.getWhen().getTime());
- assertEquals(TimeZone.getTimeZone("GMT" + committerTimeZone),
- cCommitter.getTimeZone());
+ assertEquals(Instant.ofEpochSecond(committerTime),
+ cCommitter.getWhenAsInstant());
+ assertEquals(ZoneId.of(committerTimeZone), cCommitter.getZoneId());
}
private RevCommit create(String msg) throws Exception {
@@ -153,9 +154,13 @@ public class RevCommitParseTest extends RepositoryTestCase {
c.parseCanonical(rw, b.toString().getBytes(UTF_8));
}
assertEquals(
- new PersonIdent("", "a_u_thor@example.com", 1218123387000l, 7),
+ new PersonIdent("", "a_u_thor@example.com",
+ Instant.ofEpochMilli(1218123387000L),
+ ZoneOffset.ofHoursMinutes(0, 7)),
c.getAuthorIdent());
- assertEquals(new PersonIdent("", "", 1218123390000l, -5),
+ assertEquals(
+ new PersonIdent("", "", Instant.ofEpochMilli(1218123390000L),
+ ZoneOffset.ofHoursMinutes(0, -5)),
c.getCommitterIdent());
}
@@ -408,6 +413,7 @@ public class RevCommitParseTest extends RepositoryTestCase {
final RevCommit c = create(msg);
assertEquals(msg, c.getFullMessage());
assertEquals(msg, c.getShortMessage());
+ assertEquals(msg, c.getFirstMessageLine());
}
@Test
@@ -415,6 +421,7 @@ public class RevCommitParseTest extends RepositoryTestCase {
final RevCommit c = create("\n");
assertEquals("\n", c.getFullMessage());
assertEquals("", c.getShortMessage());
+ assertEquals("", c.getFirstMessageLine());
}
@Test
@@ -423,6 +430,7 @@ public class RevCommitParseTest extends RepositoryTestCase {
final RevCommit c = create(shortMsg);
assertEquals(shortMsg, c.getFullMessage());
assertEquals(shortMsg, c.getShortMessage());
+ assertEquals(shortMsg, c.getFirstMessageLine());
}
@Test
@@ -432,6 +440,7 @@ public class RevCommitParseTest extends RepositoryTestCase {
final RevCommit c = create(fullMsg);
assertEquals(fullMsg, c.getFullMessage());
assertEquals(shortMsg, c.getShortMessage());
+ assertEquals(shortMsg, c.getFirstMessageLine());
}
@Test
@@ -441,6 +450,7 @@ public class RevCommitParseTest extends RepositoryTestCase {
final RevCommit c = create(fullMsg);
assertEquals(fullMsg, c.getFullMessage());
assertEquals(shortMsg, c.getShortMessage());
+ assertEquals("This is a", c.getFirstMessageLine());
}
@Test
@@ -450,6 +460,7 @@ public class RevCommitParseTest extends RepositoryTestCase {
final RevCommit c = create(fullMsg);
assertEquals(fullMsg, c.getFullMessage());
assertEquals(shortMsg, c.getShortMessage());
+ assertEquals("This is a", c.getFirstMessageLine());
}
@Test
@@ -461,6 +472,7 @@ public class RevCommitParseTest extends RepositoryTestCase {
final RevCommit c = create(fullMsg);
assertEquals(fullMsg, c.getFullMessage());
assertEquals(shortMsg, c.getShortMessage());
+ assertEquals(shortMsg, c.getFirstMessageLine());
}
@Test
@@ -480,6 +492,7 @@ public class RevCommitParseTest extends RepositoryTestCase {
assertEquals(author, p.getAuthorIdent());
assertEquals(committer, p.getCommitterIdent());
assertEquals("Test commit", p.getShortMessage());
+ assertEquals("Test commit", p.getFirstMessageLine());
assertEquals(src.getMessage(), p.getFullMessage());
}
@@ -494,6 +507,7 @@ public class RevCommitParseTest extends RepositoryTestCase {
final RevCommit c = create(fullMsg);
assertEquals(fullMsg, c.getFullMessage());
assertEquals(shortMsg, c.getShortMessage());
+ assertEquals("This fixes a", c.getFirstMessageLine());
}
private static ObjectId id(String str) {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkCarryFlagsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkCarryFlagsTest.java
index 8c25e05986..529d5a9f09 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkCarryFlagsTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkCarryFlagsTest.java
@@ -39,7 +39,7 @@ public class RevWalkCarryFlagsTest extends RevWalkTestCase {
/**
* Similar to {@link #testRevWalkCarryUninteresting_fastClock()} but the
* last merge commit is created so fast that he has the same creationdate as
- * the previous commit. This will cause the underlying {@link DateRevQueue}
+ * the previous commit. This will cause the underlying {@link AbstractRevQueue}
* is not able to sort the commits in a way matching the topology. A parent
* (one of the commits which are merged) is handled before the child (the
* merge commit). This makes carrying over flags more complicated
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkCommitGraphTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkCommitGraphTest.java
index 3cc0368943..e47dd898b0 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkCommitGraphTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkCommitGraphTest.java
@@ -26,6 +26,8 @@ import java.util.Comparator;
import java.util.List;
import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.diff.DiffConfig;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.internal.storage.file.GC;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.ConfigConstants;
@@ -33,7 +35,10 @@ import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.revwalk.filter.MessageRevFilter;
import org.eclipse.jgit.revwalk.filter.RevFilter;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
+import org.eclipse.jgit.treewalk.filter.ChangedPathTreeFilter;
+import org.eclipse.jgit.treewalk.filter.OrTreeFilter;
import org.eclipse.jgit.treewalk.filter.PathFilter;
import org.eclipse.jgit.treewalk.filter.TreeFilter;
import org.junit.Test;
@@ -46,6 +51,7 @@ public class RevWalkCommitGraphTest extends RevWalkTestCase {
public void setUp() throws Exception {
super.setUp();
rw = new RevWalk(db);
+ mockSystemReader.setJGitConfig(new MockConfig());
}
@Test
@@ -167,6 +173,428 @@ public class RevWalkCommitGraphTest extends RevWalkTestCase {
}
@Test
+ public void testChangedPathFilter_allModify() throws Exception {
+ RevCommit c1 = commit(tree(file("file1", blob("1"))));
+ RevCommit c2 = commit(tree(file("file2", blob("2"))), c1);
+ RevCommit c3 = commit(tree(file("file1", blob("3"))), c2);
+ RevCommit c4 = commit(tree(file("file2", blob("4"))), c3);
+
+ branch(c4, "master");
+
+ enableAndWriteCommitGraph();
+
+ TreeRevFilter trf = new TreeRevFilter(rw,
+ ChangedPathTreeFilter.create("file1"));
+ rw.markStart(rw.lookupCommit(c4));
+ rw.setRevFilter(trf);
+ assertEquals(c4, rw.next());
+ assertEquals(c3, rw.next());
+ assertEquals(c2, rw.next());
+ assertEquals(c1, rw.next());
+ assertNull(rw.next());
+
+ // all commits modified file1 but c1 did not have a parent
+ assertEquals(3, trf.getChangedPathFilterTruePositive());
+
+ // No false positives
+ assertEquals(0, trf.getChangedPathFilterFalsePositive());
+
+ // No negatives because all 4 commits had modified file1
+ assertEquals(0, trf.getChangedPathFilterNegative());
+ }
+
+ @Test
+ public void testChangedPathFilter_someModify() throws Exception {
+ RevCommit c1 = commit(tree(file("file1", blob("1"))));
+ RevCommit c2 = commit(tree(file("file1", blob("1"))), c1);
+ RevCommit c3 = commit(tree(file("file1", blob("2"))), c2);
+ RevCommit c4 = commit(tree(file("file1", blob("1"))), c3);
+
+ branch(c4, "master");
+
+ enableAndWriteCommitGraph();
+
+ TreeRevFilter trf = new TreeRevFilter(rw,
+ ChangedPathTreeFilter.create("file1"));
+ rw.markStart(rw.lookupCommit(c4));
+ rw.setRevFilter(trf);
+ assertEquals(c4, rw.next());
+ assertEquals(c3, rw.next());
+ assertEquals(c1, rw.next());
+ assertNull(rw.next());
+
+ // c4 and c3 modified file1. c1 did not have a parent
+ assertEquals(2, trf.getChangedPathFilterTruePositive());
+
+ // No false positives
+ assertEquals(0, trf.getChangedPathFilterFalsePositive());
+
+ // c2 did not modify file1
+ assertEquals(1, trf.getChangedPathFilterNegative());
+ }
+
+ @Test
+ public void testChangedPathFilterWithMultiPaths() throws Exception {
+ RevCommit c1 = commit(tree(file("file1", blob("1"))));
+ RevCommit c2 = commit(tree(file("file1", blob("2"))), c1);
+ RevCommit c3 = commit(tree(file("file2", blob("3"))), c2);
+ RevCommit c4 = commit(tree(file("file3", blob("4"))), c3);
+
+ branch(c4, "master");
+
+ enableAndWriteCommitGraph();
+
+ TreeRevFilter trf = new TreeRevFilter(rw,
+ ChangedPathTreeFilter.create("file1", "file2"));
+ rw.markStart(rw.lookupCommit(c4));
+ rw.setRevFilter(trf);
+ assertEquals(c4, rw.next());
+ assertEquals(c3, rw.next());
+ assertEquals(c2, rw.next());
+ assertEquals(c1, rw.next());
+ assertNull(rw.next());
+
+ // all commits have modified either file1 or file2, c1 did not have a
+ // parent
+ assertEquals(3, trf.getChangedPathFilterTruePositive());
+
+ // No false positives
+ assertEquals(0, trf.getChangedPathFilterFalsePositive());
+
+ // No negative
+ assertEquals(0, trf.getChangedPathFilterNegative());
+ }
+
+ @Test
+ public void testChangedPathFilterWithFollowFilter() throws Exception {
+ RevCommit c0 = commit(tree());
+ RevCommit c1 = commit(tree(file("file", blob("contents"))), c0);
+ RevCommit c2 = commit(tree(file("file", blob("contents")),
+ file("unrelated", blob("unrelated change"))), c1);
+ RevCommit c3 = commit(tree(file("renamed-file", blob("contents")),
+ file("unrelated", blob("unrelated change"))), c2);
+ RevCommit c4 = commit(
+ tree(file("renamed-file", blob("contents")),
+ file("unrelated", blob("another unrelated change"))),
+ c3);
+ branch(c4, "master");
+
+ enableAndWriteCommitGraph();
+
+ db.getConfig().setString(ConfigConstants.CONFIG_DIFF_SECTION, null,
+ ConfigConstants.CONFIG_KEY_RENAMES, "true");
+
+ TreeRevFilter trf = new TreeRevFilter(rw, FollowFilter
+ .create("renamed-file", db.getConfig().get(DiffConfig.KEY)));
+ rw.markStart(rw.lookupCommit(c4));
+ rw.setRevFilter(trf);
+ assertEquals(c3, rw.next());
+ assertEquals(c1, rw.next());
+ assertNull(rw.next());
+
+ // Path "renamed-file" is in c3's bloom filter, and another path "file"
+ // is in c1's bloom filter (we know of "file" because the rev walk
+ // detected that "renamed-file" is a renaming of "file")
+ assertEquals(2, trf.getChangedPathFilterTruePositive());
+
+ // No false positives
+ assertEquals(0, trf.getChangedPathFilterFalsePositive());
+
+ // 2 commits that have exactly one parent and don't match path
+ assertEquals(2, trf.getChangedPathFilterNegative());
+ }
+
+ @Test
+ public void testChangedPathFilter_pathFilter_or_pathFilter_binaryOperation()
+ throws Exception {
+ RevCommit c1 = commit(tree(file("file1", blob("1"))));
+ RevCommit c2 = commit(
+ tree(file("file1", blob("1")), file("file2", blob("2"))), c1);
+ RevCommit c3 = commit(tree(file("file2", blob("2"))), c2);
+ RevCommit c4 = commit(
+ tree(file("file2", blob("2")), file("file3", blob("3"))), c3);
+ RevCommit c5 = commit(
+ tree(file("file2", blob("2")), file("file3", blob("3"))), c4);
+
+ branch(c5, "master");
+
+ enableAndWriteCommitGraph();
+
+ ChangedPathTreeFilter pf1 = ChangedPathTreeFilter.create("file1");
+ ChangedPathTreeFilter pf2 = ChangedPathTreeFilter.create("file2");
+
+ TreeFilter tf = OrTreeFilter
+ .create(new ChangedPathTreeFilter[] { pf1, pf2 });
+
+ TreeRevFilter trf = new TreeRevFilter(rw, tf);
+ rw.markStart(rw.lookupCommit(c5));
+ rw.setRevFilter(trf);
+ assertEquals(c3, rw.next());
+ assertEquals(c2, rw.next());
+ assertEquals(c1, rw.next());
+ assertNull(rw.next());
+
+ // c2 and c3 has either file1 or file2, c1 is not counted as
+ // ChangedPathFilter only applies to commits with 1 parent
+ assertEquals(2, trf.getChangedPathFilterTruePositive());
+
+ // No false positives
+ assertEquals(0, trf.getChangedPathFilterFalsePositive());
+
+ // c4 and c5 did not modify file1 or file2
+ assertEquals(2, trf.getChangedPathFilterNegative());
+ }
+
+ @Test
+ public void testChangedPathFilter_pathFilter_or_pathFilter_or_pathFilter_listOperation()
+ throws Exception {
+ RevCommit c1 = commit(tree(file("file1", blob("1"))));
+ RevCommit c2 = commit(
+ tree(file("file1", blob("1")), file("file2", blob("2"))), c1);
+ RevCommit c3 = commit(tree(file("file2", blob("2"))), c2);
+ RevCommit c4 = commit(tree(file("file3", blob("3"))), c3);
+ RevCommit c5 = commit(tree(file("file3", blob("3"))), c4);
+
+ branch(c5, "master");
+
+ enableAndWriteCommitGraph();
+
+ ChangedPathTreeFilter pf1 = ChangedPathTreeFilter.create("file1");
+ ChangedPathTreeFilter pf2 = ChangedPathTreeFilter.create("file2");
+ ChangedPathTreeFilter pf3 = ChangedPathTreeFilter.create("file3");
+
+ TreeFilter tf = OrTreeFilter
+ .create(new ChangedPathTreeFilter[] { pf1, pf2, pf3 });
+
+ TreeRevFilter trf = new TreeRevFilter(rw, tf);
+ rw.markStart(rw.lookupCommit(c5));
+ rw.setRevFilter(trf);
+ assertEquals(c4, rw.next());
+ assertEquals(c3, rw.next());
+ assertEquals(c2, rw.next());
+ assertEquals(c1, rw.next());
+ assertNull(rw.next());
+
+ // c2 and c3 has either modified file1 or file2 or file3, c1 is not
+ // counted as ChangedPathFilter only applies to commits with 1 parent
+ assertEquals(3, trf.getChangedPathFilterTruePositive());
+
+ // No false positives
+ assertEquals(0, trf.getChangedPathFilterFalsePositive());
+
+ // c5 does not modify either file1 or file2 or file3
+ assertEquals(1, trf.getChangedPathFilterNegative());
+ }
+
+ @Test
+ public void testChangedPathFilter_pathFilter_or_nonPathFilter_binaryOperation()
+ throws Exception {
+ RevCommit c1 = commit(tree(file("file1", blob("1"))));
+ RevCommit c2 = commit(tree(file("file2", blob("2"))), c1);
+ RevCommit c3 = commit(tree(file("file2", blob("3"))), c2);
+ RevCommit c4 = commit(tree(file("file2", blob("3"))), c3);
+
+ branch(c4, "master");
+
+ enableAndWriteCommitGraph();
+
+ ChangedPathTreeFilter pf = ChangedPathTreeFilter.create("file1");
+ TreeFilter npf = TreeFilter.ANY_DIFF;
+
+ TreeFilter tf = OrTreeFilter.create(new TreeFilter[] { pf, npf });
+
+ TreeRevFilter trf = new TreeRevFilter(rw, tf);
+ rw.markStart(rw.lookupCommit(c4));
+ rw.setRevFilter(trf);
+ assertEquals(c3, rw.next());
+ assertEquals(c2, rw.next());
+ assertEquals(c1, rw.next());
+ assertNull(rw.next());
+
+ // c2 modified file1, c3 defaulted positive due to ANY_DIFF, c1 is not
+ // counted as ChangedPathFilter only applies to commits with 1 parent
+ assertEquals(2, trf.getChangedPathFilterTruePositive());
+
+ // c4 defaulted positive due to ANY_DIFF, but didn't no diff with its
+ // parent c3
+ assertEquals(1, trf.getChangedPathFilterFalsePositive());
+
+ // No negative due to the OrTreeFilter
+ assertEquals(0, trf.getChangedPathFilterNegative());
+ }
+
+ @Test
+ public void testChangedPathFilter_nonPathFilter_or_nonPathFilter_binaryOperation()
+ throws Exception {
+ RevCommit c1 = commitFile("file1", "1", "master");
+ RevCommit c2 = commitFile("file2", "2", "master");
+ RevCommit c3 = commitFile("file3", "3", "master");
+ RevCommit c4 = commitFile("file4", "4", "master");
+
+ enableAndWriteCommitGraph();
+
+ TreeFilter npf1 = TreeFilter.ANY_DIFF;
+ TreeFilter npf2 = TreeFilter.ANY_DIFF;
+
+ TreeFilter tf = OrTreeFilter.create(new TreeFilter[] { npf1, npf2 });
+
+ TreeRevFilter trf = new TreeRevFilter(rw, tf);
+ rw.markStart(rw.lookupCommit(c4));
+ rw.setRevFilter(trf);
+ assertEquals(c4, rw.next());
+ assertEquals(c3, rw.next());
+ assertEquals(c2, rw.next());
+ assertEquals(c1, rw.next());
+ assertNull(rw.next());
+
+ // No true positives since there's no pathFilter
+ assertEquals(0, trf.getChangedPathFilterTruePositive());
+
+ // No false positives since there's no pathFilter
+ assertEquals(0, trf.getChangedPathFilterFalsePositive());
+
+ // No negative since there's no pathFilter
+ assertEquals(0, trf.getChangedPathFilterNegative());
+ }
+
+ @Test
+ public void testChangedPathFilter_pathFilter_and_pathFilter_binaryOperation()
+ throws Exception {
+ RevCommit c1 = commit(tree(file("file1", blob("1"))));
+ RevCommit c2 = commit(tree(file("file2", blob("2"))), c1);
+
+ branch(c2, "master");
+
+ enableAndWriteCommitGraph();
+
+ ChangedPathTreeFilter pf1 = ChangedPathTreeFilter.create("file1");
+ ChangedPathTreeFilter pf2 = ChangedPathTreeFilter.create("file2");
+
+ TreeFilter atf = AndTreeFilter
+ .create(new ChangedPathTreeFilter[] { pf1, pf2 });
+ TreeRevFilter trf = new TreeRevFilter(rw, atf);
+
+ rw.markStart(rw.lookupCommit(c2));
+ rw.setRevFilter(trf);
+
+ assertNull(rw.next());
+
+ // c1 is not counted as ChangedPathFilter only applies to commits with 1
+ // parent
+ assertEquals(0, trf.getChangedPathFilterTruePositive());
+
+ // c2 has modified both file 1 and file2,
+ // however nothing is returned from TreeWalk since a TreeHead
+ // cannot be two paths at once
+ assertEquals(1, trf.getChangedPathFilterFalsePositive());
+
+ // No negatives
+ assertEquals(0, trf.getChangedPathFilterNegative());
+ }
+
+ @Test
+ public void testChangedPathFilter_pathFilter_and_pathFilter_and_pathFilter_listOperation()
+ throws Exception {
+ RevCommit c1 = commit(tree(file("file1", blob("1"))));
+ RevCommit c2 = commit(tree(file("file2", blob("2"))), c1);
+ RevCommit c3 = commit(tree(file("file3", blob("3"))), c2);
+
+ branch(c3, "master");
+
+ enableAndWriteCommitGraph();
+
+ ChangedPathTreeFilter pf1 = ChangedPathTreeFilter.create("file1");
+ ChangedPathTreeFilter pf2 = ChangedPathTreeFilter.create("file2");
+ ChangedPathTreeFilter pf3 = ChangedPathTreeFilter.create("file3");
+
+ TreeFilter tf = AndTreeFilter
+ .create(new ChangedPathTreeFilter[] { pf1, pf2, pf3 });
+
+ TreeRevFilter trf = new TreeRevFilter(rw, tf);
+ rw.markStart(rw.lookupCommit(c3));
+ rw.setRevFilter(trf);
+ assertNull(rw.next());
+
+ // c1 is not counted as ChangedPathFilter only applies to commits with 1
+ // parent
+ assertEquals(0, trf.getChangedPathFilterTruePositive());
+
+ // No false positives
+ assertEquals(0, trf.getChangedPathFilterFalsePositive());
+
+ // c2 and c3 can not possibly have both file1, file2, and file3 as
+ // treeHead at once
+ assertEquals(2, trf.getChangedPathFilterNegative());
+ }
+
+ @Test
+ public void testChangedPathFilter_pathFilter_and_nonPathFilter_binaryOperation()
+ throws Exception {
+ RevCommit c1 = commit(tree(file("file1", blob("1"))));
+ RevCommit c2 = commit(tree(file("file1", blob("2"))), c1);
+ RevCommit c3 = commit(tree(file("file1", blob("2"))), c2);
+
+ branch(c3, "master");
+
+ enableAndWriteCommitGraph();
+
+ ChangedPathTreeFilter pf = ChangedPathTreeFilter.create("file1");
+ TreeFilter npf = TreeFilter.ANY_DIFF;
+
+ TreeFilter tf = AndTreeFilter.create(new TreeFilter[] { pf, npf });
+
+ TreeRevFilter trf = new TreeRevFilter(rw, tf);
+ rw.markStart(rw.lookupCommit(c3));
+ rw.setRevFilter(trf);
+ assertEquals(c2, rw.next());
+ assertEquals(c1, rw.next());
+ assertNull(rw.next());
+
+ // c2 modified file1 and c1 is not counted as ChangedPathFilter only
+ // applies to commits with 1 parent
+ assertEquals(1, trf.getChangedPathFilterTruePositive());
+
+ // No false positives
+ assertEquals(0, trf.getChangedPathFilterFalsePositive());
+
+ // c3 did not modify file1
+ assertEquals(1, trf.getChangedPathFilterNegative());
+ }
+
+ @Test
+ public void testChangedPathFilter_nonPathFilter_and_nonPathFilter_binaryOperation()
+ throws Exception {
+ RevCommit c1 = commitFile("file1", "1", "master");
+ commitFile("file1", "1", "master");
+ RevCommit c3 = commitFile("file3", "3", "master");
+ RevCommit c4 = commitFile("file4", "4", "master");
+
+ enableAndWriteCommitGraph();
+
+ TreeFilter npf1 = TreeFilter.ANY_DIFF;
+ TreeFilter npf2 = TreeFilter.ANY_DIFF;
+
+ TreeFilter tf = AndTreeFilter.create(new TreeFilter[] { npf1, npf2 });
+
+ TreeRevFilter trf = new TreeRevFilter(rw, tf);
+ rw.markStart(rw.lookupCommit(c4));
+ rw.setRevFilter(trf);
+ assertEquals(c4, rw.next());
+ assertEquals(c3, rw.next());
+ assertEquals(c1, rw.next());
+ assertNull(rw.next());
+
+ // No true positives since there's no path
+ assertEquals(0, trf.getChangedPathFilterTruePositive());
+
+ // No false positives since there's no path
+ assertEquals(0, trf.getChangedPathFilterFalsePositive());
+
+ // No negative since there's no path
+ assertEquals(0, trf.getChangedPathFilterNegative());
+ }
+
+ @Test
public void testWalkWithCommitMessageFilter() throws Exception {
RevCommit a = commit();
RevCommit b = commitBuilder().parent(a)
@@ -437,6 +865,8 @@ public class RevWalkCommitGraphTest extends RevWalkTestCase {
ConfigConstants.CONFIG_COMMIT_GRAPH, true);
db.getConfig().setBoolean(ConfigConstants.CONFIG_GC_SECTION, null,
ConfigConstants.CONFIG_KEY_WRITE_COMMIT_GRAPH, true);
+ db.getConfig().setBoolean(ConfigConstants.CONFIG_GC_SECTION, null,
+ ConfigConstants.CONFIG_KEY_WRITE_CHANGED_PATHS, true);
GC gc = new GC(db);
gc.gc().get();
}
@@ -445,4 +875,41 @@ public class RevWalkCommitGraphTest extends RevWalkTestCase {
rw.close();
rw = new RevWalk(db);
}
+
+ private static final class MockConfig extends FileBasedConfig {
+ private MockConfig() {
+ super(null, null);
+ }
+
+ @Override
+ public void load() throws IOException, ConfigInvalidException {
+ // Do nothing
+ }
+
+ @Override
+ public void save() throws IOException {
+ // Do nothing
+ }
+
+ @Override
+ public boolean isOutdated() {
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "MockConfig";
+ }
+
+ @Override
+ public boolean getBoolean(final String section, final String name,
+ final boolean defaultValue) {
+ if (section.equals(ConfigConstants.CONFIG_COMMIT_GRAPH_SECTION)
+ && name.equals(
+ ConfigConstants.CONFIG_KEY_READ_CHANGED_PATHS)) {
+ return true;
+ }
+ return defaultValue;
+ }
+ }
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkFilterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkFilterTest.java
index 81ff4a2f92..7fece66bf0 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkFilterTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkFilterTest.java
@@ -14,6 +14,7 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import java.io.IOException;
+import java.time.Instant;
import java.util.Date;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -217,14 +218,132 @@ public class RevWalkFilterTest extends RevWalkTestCase {
final RevCommit b = commit(a);
tick(100);
- Date since = getDate();
+ Instant since = getInstant();
final RevCommit c1 = commit(b);
tick(100);
final RevCommit c2 = commit(b);
tick(100);
- Date until = getDate();
+ Instant until = getInstant();
+ final RevCommit d = commit(c1, c2);
+ tick(100);
+
+ final RevCommit e = commit(d);
+
+ {
+ RevFilter after = CommitTimeRevFilter.after(since);
+ assertNotNull(after);
+ rw.setRevFilter(after);
+ markStart(e);
+ assertCommit(e, rw.next());
+ assertCommit(d, rw.next());
+ assertCommit(c2, rw.next());
+ assertCommit(c1, rw.next());
+ assertNull(rw.next());
+ }
+
+ {
+ RevFilter before = CommitTimeRevFilter.before(until);
+ assertNotNull(before);
+ rw.reset();
+ rw.setRevFilter(before);
+ markStart(e);
+ assertCommit(c2, rw.next());
+ assertCommit(c1, rw.next());
+ assertCommit(b, rw.next());
+ assertCommit(a, rw.next());
+ assertNull(rw.next());
+ }
+
+ {
+ RevFilter between = CommitTimeRevFilter.between(since, until);
+ assertNotNull(between);
+ rw.reset();
+ rw.setRevFilter(between);
+ markStart(e);
+ assertCommit(c2, rw.next());
+ assertCommit(c1, rw.next());
+ assertNull(rw.next());
+ }
+ }
+
+ @Test
+ public void testCommitTimeRevFilter_date() throws Exception {
+ // Using deprecated Date api for the commit time rev filter.
+ // Delete this tests when method is removed.
+ final RevCommit a = commit();
+ tick(100);
+
+ final RevCommit b = commit(a);
+ tick(100);
+
+ Date since = Date.from(getInstant());
+ final RevCommit c1 = commit(b);
+ tick(100);
+
+ final RevCommit c2 = commit(b);
+ tick(100);
+
+ Date until = Date.from(getInstant());
+ final RevCommit d = commit(c1, c2);
+ tick(100);
+
+ final RevCommit e = commit(d);
+
+ {
+ RevFilter after = CommitTimeRevFilter.after(since);
+ assertNotNull(after);
+ rw.setRevFilter(after);
+ markStart(e);
+ assertCommit(e, rw.next());
+ assertCommit(d, rw.next());
+ assertCommit(c2, rw.next());
+ assertCommit(c1, rw.next());
+ assertNull(rw.next());
+ }
+
+ {
+ RevFilter before = CommitTimeRevFilter.before(until);
+ assertNotNull(before);
+ rw.reset();
+ rw.setRevFilter(before);
+ markStart(e);
+ assertCommit(c2, rw.next());
+ assertCommit(c1, rw.next());
+ assertCommit(b, rw.next());
+ assertCommit(a, rw.next());
+ assertNull(rw.next());
+ }
+
+ {
+ RevFilter between = CommitTimeRevFilter.between(since, until);
+ assertNotNull(between);
+ rw.reset();
+ rw.setRevFilter(between);
+ markStart(e);
+ assertCommit(c2, rw.next());
+ assertCommit(c1, rw.next());
+ assertNull(rw.next());
+ }
+ }
+
+ @Test
+ public void testCommitTimeRevFilter_long() throws Exception {
+ final RevCommit a = commit();
+ tick(100);
+
+ final RevCommit b = commit(a);
+ tick(100);
+
+ long since = getInstant().toEpochMilli();
+ final RevCommit c1 = commit(b);
+ tick(100);
+
+ final RevCommit c2 = commit(b);
+ tick(100);
+
+ long until = getInstant().toEpochMilli();
final RevCommit d = commit(c1, c2);
tick(100);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkFollowFilterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkFollowFilterTest.java
index c62136e64d..5203e3fbea 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkFollowFilterTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkFollowFilterTest.java
@@ -27,9 +27,17 @@ public class RevWalkFollowFilterTest extends RevWalkTestCase {
private static class DiffCollector extends RenameCallback {
List<DiffEntry> diffs = new ArrayList<>();
+ List<RevCommit> commits = new ArrayList<>();
+
@Override
public void renamed(DiffEntry diff) {
+ throw new UnsupportedOperationException("unimplemented");
+ }
+
+ @Override
+ public void renamed(DiffEntry diff, RevCommit commit) {
diffs.add(diff);
+ commits.add(commit);
}
}
@@ -77,6 +85,7 @@ public class RevWalkFollowFilterTest extends RevWalkTestCase {
assertNull(rw.next());
assertRenames("a->b");
+ assertRenameCommits(renameCommit);
}
@Test
@@ -108,6 +117,7 @@ public class RevWalkFollowFilterTest extends RevWalkTestCase {
assertNull(rw.next());
assertRenames("c->a", "b->c", "a->b");
+ assertRenameCommits(renameCommit3, renameCommit2, renameCommit1);
}
/**
@@ -136,6 +146,20 @@ public class RevWalkFollowFilterTest extends RevWalkTestCase {
}
}
+ protected void assertRenameCommits(RevCommit... expectedCommits) {
+ Assert.assertEquals(
+ "Unexpected number of rename commits. Expected: "
+ + expectedCommits.length + ", actual: "
+ + diffCollector.diffs.size(),
+ expectedCommits.length, diffCollector.diffs.size());
+
+ for (int i = 0; i < expectedCommits.length; i++) {
+ RevCommit renameCommit = diffCollector.commits.get(i);
+ Assert.assertNotNull(renameCommit);
+ Assert.assertEquals(expectedCommits[i], renameCommit);
+ }
+ }
+
protected void assertNoRenames() {
Assert.assertEquals("Found unexpected rename/copy diff", 0,
diffCollector.diffs.size());
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkTestCase.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkTestCase.java
index ec0c0e7e84..8fa6a83670 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkTestCase.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkTestCase.java
@@ -12,6 +12,7 @@ package org.eclipse.jgit.revwalk;
import static org.junit.Assert.assertSame;
+import java.time.Instant;
import java.util.Date;
import org.eclipse.jgit.dircache.DirCacheEntry;
@@ -38,8 +39,14 @@ public abstract class RevWalkTestCase extends RepositoryTestCase {
return new RevWalk(db);
}
+ // Use getInstant() instead
+ @Deprecated
protected Date getDate() {
- return util.getDate();
+ return Date.from(util.getInstant());
+ }
+
+ protected Instant getInstant() {
+ return util.getInstant();
}
protected void tick(int secDelta) {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkUtilsReachableTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkUtilsReachableTest.java
index 0a045c917b..ffc7c96f69 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkUtilsReachableTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkUtilsReachableTest.java
@@ -14,6 +14,7 @@ import static java.util.Arrays.asList;
import static org.junit.Assert.assertEquals;
import java.util.Collection;
+import java.util.HashSet;
import java.util.List;
import org.eclipse.jgit.api.Git;
@@ -121,7 +122,7 @@ public class RevWalkUtilsReachableTest extends RevWalkTestCase {
Collection<Ref> sortedRefs = RefComparator.sort(allRefs);
List<Ref> actual = RevWalkUtils.findBranchesReachableFrom(commit,
rw, sortedRefs);
- assertEquals(refsThatShouldContainCommit, actual);
+ assertEquals(new HashSet<>(refsThatShouldContainCommit), new HashSet<>(actual));
}
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RewriteGeneratorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RewriteGeneratorTest.java
new file mode 100644
index 0000000000..04e372998c
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RewriteGeneratorTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2023, HIS eG
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.revwalk;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import org.junit.Test;
+
+public class RewriteGeneratorTest extends RevWalkTestCase {
+
+ @Test
+ public void testRewriteGeneratorDoesNotExhaustPreviousGenerator()
+ throws Exception {
+ RevCommit a = commit();
+ a.flags |= RevWalk.TREE_REV_FILTER_APPLIED;
+ RevCommit b = commit(a);
+
+ LIFORevQueue q = new LIFORevQueue();
+ q.add(a);
+ q.add(b);
+
+ /*
+ * Since the TREE_REV_FILTER has been applied to commit a and the
+ * REWRITE flag has not been applied to commit a, the RewriteGenerator
+ * must not rewrite the parent of b and thus must not call the previous
+ * generator (since b already has its correct parent).
+ */
+ RewriteGenerator rewriteGenerator = new RewriteGenerator(q);
+ rewriteGenerator.next();
+
+ assertNotNull(
+ "Previous generator was unnecessarily exhausted by RewriteGenerator",
+ q.next());
+ }
+
+ @Test
+ public void testRewriteGeneratorRewritesParent() throws Exception {
+ RevCommit a = commit();
+ a.flags |= RevWalk.TREE_REV_FILTER_APPLIED;
+ a.flags |= RevWalk.REWRITE;
+ RevCommit b = commit(a);
+ assertEquals(1, b.getParentCount());
+
+ LIFORevQueue q = new LIFORevQueue();
+ /*
+ * We are only adding commit b (and not a), because PendingGenerator
+ * should never emit a commit that has the REWRITE flag set.
+ */
+ q.add(b);
+
+ RewriteGenerator rewriteGenerator = new RewriteGenerator(q);
+ RevCommit returnedB = rewriteGenerator.next();
+ assertEquals(b.getId(), returnedB.getId());
+ assertEquals(0, returnedB.getParentCount());
+ }
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/TreeRevFilterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/TreeRevFilterTest.java
index 298facfd15..ddbb19cb8f 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/TreeRevFilterTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/TreeRevFilterTest.java
@@ -20,17 +20,33 @@ import org.eclipse.jgit.treewalk.filter.TreeFilter;
import org.junit.Test;
public class TreeRevFilterTest extends RevWalkTestCase {
- private RevFilter treeRevFilter() {
- return new TreeRevFilter(rw, TreeFilter.ANY_DIFF);
- }
+ @Test
+ public void testStringOfPearls_FilePath1_treeRevFilter()
+ throws Exception {
+ RevCommit a = commit(tree(file("d/f", blob("a"))));
+ RevCommit b = commit(tree(file("d/f", blob("a"))), a);
+ RevCommit c = commit(tree(file("d/f", blob("b"))), b);
+ rw.setRevFilter(new TreeRevFilter(rw, TreeFilter.ANY_DIFF));
+ markStart(c);
+
+ assertCommit(c, rw.next());
+ assertEquals(1, c.getParentCount());
+ assertCommit(b, c.getParent(0));
+
+ assertCommit(a, rw.next()); // b was skipped
+ assertEquals(0, a.getParentCount());
+ assertNull(rw.next());
+ }
@Test
- public void testStringOfPearls_FilePath1()
+ public void testStringOfPearls_FilePath1_noRewriteParents()
throws Exception {
RevCommit a = commit(tree(file("d/f", blob("a"))));
RevCommit b = commit(tree(file("d/f", blob("a"))), a);
RevCommit c = commit(tree(file("d/f", blob("b"))), b);
- rw.setRevFilter(treeRevFilter());
+
+ rw.setRewriteParents(false);
+ rw.setTreeFilter(TreeFilter.ANY_DIFF);
markStart(c);
assertCommit(c, rw.next());
@@ -43,12 +59,74 @@ public class TreeRevFilterTest extends RevWalkTestCase {
}
@Test
+ public void testStringOfPearls_FilePath1_RewriteParents()
+ throws Exception {
+ RevCommit a = commit(tree(file("d/f", blob("a"))));
+ RevCommit b = commit(tree(file("d/f", blob("a"))), a);
+ RevCommit c = commit(tree(file("d/f", blob("b"))), b);
+
+ rw.setRevFilter(new TreeRevFilter(rw, TreeFilter.ANY_DIFF));
+ rw.setTreeFilter(TreeFilter.ANY_DIFF);
+ markStart(c);
+
+ assertCommit(c, rw.next());
+ assertEquals(1, c.getParentCount());
+ assertCommit(a, c.getParent(0));
+
+ assertCommit(a, rw.next()); // b was skipped
+ assertEquals(0, a.getParentCount());
+ assertNull(rw.next());
+ }
+
+ @Test
public void testStringOfPearls_FilePath2() throws Exception {
RevCommit a = commit(tree(file("d/f", blob("a"))));
RevCommit b = commit(tree(file("d/f", blob("a"))), a);
RevCommit c = commit(tree(file("d/f", blob("b"))), b);
RevCommit d = commit(tree(file("d/f", blob("b"))), c);
- rw.setRevFilter(treeRevFilter());
+ rw.setRevFilter(new TreeRevFilter(rw, TreeFilter.ANY_DIFF));
+ markStart(d);
+
+ // d was skipped
+ assertCommit(c, rw.next());
+ assertEquals(1, c.getParentCount());
+ assertCommit(b, c.getParent(0));
+
+ // b was skipped
+ assertCommit(a, rw.next());
+ assertEquals(0, a.getParentCount());
+ assertNull(rw.next());
+ }
+
+ @Test
+ public void testStringOfPearls_FilePath2_RewriteParents() throws Exception {
+ RevCommit a = commit(tree(file("d/f", blob("a"))));
+ RevCommit b = commit(tree(file("d/f", blob("a"))), a);
+ RevCommit c = commit(tree(file("d/f", blob("b"))), b);
+ RevCommit d = commit(tree(file("d/f", blob("b"))), c);
+ rw.setTreeFilter(TreeFilter.ANY_DIFF);
+ markStart(d);
+
+ // d was skipped
+ assertCommit(c, rw.next());
+ assertEquals(1, c.getParentCount());
+ assertCommit(a, c.getParent(0));
+
+ // b was skipped
+ assertCommit(a, rw.next());
+ assertEquals(0, a.getParentCount());
+ assertNull(rw.next());
+ }
+
+ @Test
+ public void testStringOfPearls_FilePath2_RewriteParents_False() throws Exception {
+ RevCommit a = commit(tree(file("d/f", blob("a"))));
+ RevCommit b = commit(tree(file("d/f", blob("a"))), a);
+ RevCommit c = commit(tree(file("d/f", blob("b"))), b);
+ RevCommit d = commit(tree(file("d/f", blob("b"))), c);
+ rw.setRewriteParents(false);
+ rw.setRevFilter(new TreeRevFilter(rw, TreeFilter.ANY_DIFF));
+ rw.setTreeFilter(TreeFilter.ANY_DIFF);
markStart(d);
// d was skipped
@@ -68,7 +146,7 @@ public class TreeRevFilterTest extends RevWalkTestCase {
RevCommit b = commit(tree(file("d/f", blob("a"))), a);
RevCommit c = commit(tree(file("d/f", blob("b"))), b);
RevCommit d = commit(tree(file("d/f", blob("b"))), c);
- rw.setRevFilter(treeRevFilter());
+ rw.setRevFilter(new TreeRevFilter(rw, TreeFilter.ANY_DIFF));
markStart(d);
// d was skipped
@@ -93,7 +171,9 @@ public class TreeRevFilterTest extends RevWalkTestCase {
RevCommit g = commit(tree(file("d/f", blob("b"))), f);
RevCommit h = commit(tree(file("d/f", blob("b"))), g);
RevCommit i = commit(tree(file("d/f", blob("c"))), h);
- rw.setRevFilter(treeRevFilter());
+
+ // Doesn't rewrite parents since no TreeFilter is set
+ rw.setRevFilter(new TreeRevFilter(rw, TreeFilter.ANY_DIFF));
markStart(i);
assertCommit(i, rw.next());
@@ -112,8 +192,39 @@ public class TreeRevFilterTest extends RevWalkTestCase {
}
@Test
+ public void testStringOfPearls_FilePath3_RewriteParents() throws Exception {
+ RevCommit a = commit(tree(file("d/f", blob("a"))));
+ RevCommit b = commit(tree(file("d/f", blob("a"))), a);
+ RevCommit c = commit(tree(file("d/f", blob("b"))), b);
+ RevCommit d = commit(tree(file("d/f", blob("b"))), c);
+ RevCommit e = commit(tree(file("d/f", blob("b"))), d);
+ RevCommit f = commit(tree(file("d/f", blob("b"))), e);
+ RevCommit g = commit(tree(file("d/f", blob("b"))), f);
+ RevCommit h = commit(tree(file("d/f", blob("b"))), g);
+ RevCommit i = commit(tree(file("d/f", blob("c"))), h);
+
+ rw.setRevFilter(new TreeRevFilter(rw, TreeFilter.ANY_DIFF));
+ rw.setTreeFilter(TreeFilter.ANY_DIFF);
+ markStart(i);
+
+ assertCommit(i, rw.next());
+ assertEquals(1, i.getParentCount());
+ assertCommit(c, i.getParent(0));
+
+ // h..d was skipped
+ assertCommit(c, rw.next());
+ assertEquals(1, c.getParentCount());
+ assertCommit(a, c.getParent(0));
+
+ // b was skipped
+ assertCommit(a, rw.next());
+ assertEquals(0, a.getParentCount());
+ assertNull(rw.next());
+ }
+
+ @Test
public void testPathFilterOrOtherFilter() throws Exception {
- RevFilter pathFilter = treeRevFilter();
+ RevFilter pathFilter = new TreeRevFilter(rw, TreeFilter.ANY_DIFF);
RevFilter skipFilter = SkipRevFilter.create(1);
RevFilter orFilter = OrRevFilter.create(skipFilter, pathFilter);
@@ -125,6 +236,9 @@ public class TreeRevFilterTest extends RevWalkTestCase {
rw.setRevFilter(pathFilter);
markStart(c);
assertCommit(c, rw.next());
+ assertEquals(1, c.getParentCount());
+ assertCommit(b, c.getParent(0));
+
assertCommit(a, rw.next());
// Skip filter matches b, a.
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/storage/file/UserConfigFileTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/storage/file/UserConfigFileTest.java
new file mode 100644
index 0000000000..7d212d540f
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/storage/file/UserConfigFileTest.java
@@ -0,0 +1,301 @@
+/*
+ * Copyright (C) 2023, Thomas Wolf <twolf@apache.org> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.storage.file;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.eclipse.jgit.util.FS;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class UserConfigFileTest {
+
+ @Rule
+ public TemporaryFolder tmp = new TemporaryFolder();
+
+ @Test
+ public void testParentOnlyLoad() throws Exception {
+ Path xdg = tmp.getRoot().toPath().resolve("xdg.cfg");
+ Files.writeString(xdg, "[user]\n\tname = Archibald Ulysses Thor");
+ Path user = tmp.getRoot().toPath().resolve("user.cfg");
+ UserConfigFile config = new UserConfigFile(null, user.toFile(),
+ xdg.toFile(), FS.DETECTED);
+ config.load();
+ assertEquals("Archibald Ulysses Thor",
+ config.getString("user", null, "name"));
+ }
+
+ @Test
+ public void testLoadBoth() throws Exception {
+ Path xdg = tmp.getRoot().toPath().resolve("xdg.cfg");
+ Files.writeString(xdg, "[user]\n\tname = Archibald Ulysses Thor");
+ Path user = tmp.getRoot().toPath().resolve("user.cfg");
+ Files.writeString(user, "[user]\n\temail = a.u.thor@example.com");
+ UserConfigFile config = new UserConfigFile(null, user.toFile(),
+ xdg.toFile(), FS.DETECTED);
+ config.load();
+ assertEquals("Archibald Ulysses Thor",
+ config.getString("user", null, "name"));
+ assertEquals("a.u.thor@example.com",
+ config.getString("user", null, "email"));
+ }
+
+ @Test
+ public void testOverwriteChild() throws Exception {
+ Path xdg = tmp.getRoot().toPath().resolve("xdg.cfg");
+ Files.writeString(xdg, "[user]\n\tname = Archibald Ulysses Thor");
+ Path user = tmp.getRoot().toPath().resolve("user.cfg");
+ Files.writeString(user, "[user]\n\temail = a.u.thor@example.com");
+ UserConfigFile config = new UserConfigFile(null, user.toFile(),
+ xdg.toFile(), FS.DETECTED);
+ config.load();
+ assertEquals("Archibald Ulysses Thor",
+ config.getString("user", null, "name"));
+ assertEquals("a.u.thor@example.com",
+ config.getString("user", null, "email"));
+ config.setString("user", null, "name", "A U Thor");
+ assertEquals("A U Thor", config.getString("user", null, "name"));
+ config.save();
+ UserConfigFile config2 = new UserConfigFile(null, user.toFile(),
+ xdg.toFile(), FS.DETECTED);
+ config2.load();
+ assertEquals("A U Thor", config2.getString("user", null, "name"));
+ assertEquals("a.u.thor@example.com",
+ config.getString("user", null, "email"));
+ FileBasedConfig cfg = new FileBasedConfig(null, xdg.toFile(),
+ FS.DETECTED);
+ cfg.load();
+ assertEquals("Archibald Ulysses Thor",
+ cfg.getString("user", null, "name"));
+ assertNull(cfg.getString("user", null, "email"));
+ }
+
+ @Test
+ public void testUnset() throws Exception {
+ Path xdg = tmp.getRoot().toPath().resolve("xdg.cfg");
+ Files.writeString(xdg, "[user]\n\tname = Archibald Ulysses Thor");
+ Path user = tmp.getRoot().toPath().resolve("user.cfg");
+ Files.writeString(user, "[user]\n\temail = a.u.thor@example.com");
+ UserConfigFile config = new UserConfigFile(null, user.toFile(),
+ xdg.toFile(), FS.DETECTED);
+ config.load();
+ assertEquals("Archibald Ulysses Thor",
+ config.getString("user", null, "name"));
+ assertEquals("a.u.thor@example.com",
+ config.getString("user", null, "email"));
+ config.setString("user", null, "name", "A U Thor");
+ assertEquals("A U Thor", config.getString("user", null, "name"));
+ config.unset("user", null, "name");
+ assertEquals("Archibald Ulysses Thor",
+ config.getString("user", null, "name"));
+ assertEquals("a.u.thor@example.com",
+ config.getString("user", null, "email"));
+ config.save();
+ UserConfigFile config2 = new UserConfigFile(null, user.toFile(),
+ xdg.toFile(), FS.DETECTED);
+ config2.load();
+ assertEquals("Archibald Ulysses Thor",
+ config2.getString("user", null, "name"));
+ assertEquals("a.u.thor@example.com",
+ config.getString("user", null, "email"));
+ FileBasedConfig cfg = new FileBasedConfig(null, user.toFile(),
+ FS.DETECTED);
+ cfg.load();
+ assertNull(cfg.getString("user", null, "name"));
+ assertEquals("a.u.thor@example.com",
+ cfg.getString("user", null, "email"));
+ }
+
+ @Test
+ public void testUnsetSection() throws Exception {
+ Path xdg = tmp.getRoot().toPath().resolve("xdg.cfg");
+ Files.writeString(xdg, "[user]\n\tname = Archibald Ulysses Thor");
+ Path user = tmp.getRoot().toPath().resolve("user.cfg");
+ Files.writeString(user, "[user]\n\temail = a.u.thor@example.com");
+ UserConfigFile config = new UserConfigFile(null, user.toFile(),
+ xdg.toFile(), FS.DETECTED);
+ config.load();
+ assertEquals("Archibald Ulysses Thor",
+ config.getString("user", null, "name"));
+ assertEquals("a.u.thor@example.com",
+ config.getString("user", null, "email"));
+ config.unsetSection("user", null);
+ assertEquals("Archibald Ulysses Thor",
+ config.getString("user", null, "name"));
+ config.save();
+ assertTrue(Files.readString(user).strip().isEmpty());
+ }
+
+ @Test
+ public void testNoChild() throws Exception {
+ Path xdg = tmp.getRoot().toPath().resolve("xdg.cfg");
+ Files.writeString(xdg, "[user]\n\tname = Archibald Ulysses Thor");
+ Path user = tmp.getRoot().toPath().resolve("user.cfg");
+ UserConfigFile config = new UserConfigFile(null, user.toFile(),
+ xdg.toFile(), FS.DETECTED);
+ config.load();
+ assertEquals("Archibald Ulysses Thor",
+ config.getString("user", null, "name"));
+ assertNull(config.getString("user", null, "email"));
+ config.setString("user", null, "email", "a.u.thor@example.com");
+ assertEquals("a.u.thor@example.com",
+ config.getString("user", null, "email"));
+ config.save();
+ assertFalse(Files.exists(user));
+ UserConfigFile config2 = new UserConfigFile(null, user.toFile(),
+ xdg.toFile(), FS.DETECTED);
+ config2.load();
+ assertEquals("Archibald Ulysses Thor",
+ config2.getString("user", null, "name"));
+ assertEquals("a.u.thor@example.com",
+ config2.getString("user", null, "email"));
+ }
+
+ @Test
+ public void testNoFiles() throws Exception {
+ Path xdg = tmp.getRoot().toPath().resolve("xdg.cfg");
+ Path user = tmp.getRoot().toPath().resolve("user.cfg");
+ UserConfigFile config = new UserConfigFile(null, user.toFile(),
+ xdg.toFile(), FS.DETECTED);
+ config.load();
+ assertNull(config.getString("user", null, "name"));
+ assertNull(config.getString("user", null, "email"));
+ config.setString("user", null, "name", "Archibald Ulysses Thor");
+ config.setString("user", null, "email", "a.u.thor@example.com");
+ assertEquals("Archibald Ulysses Thor",
+ config.getString("user", null, "name"));
+ assertEquals("a.u.thor@example.com",
+ config.getString("user", null, "email"));
+ config.save();
+ assertTrue(Files.exists(user));
+ assertFalse(Files.exists(xdg));
+ UserConfigFile config2 = new UserConfigFile(null, user.toFile(),
+ xdg.toFile(), FS.DETECTED);
+ config2.load();
+ assertEquals("Archibald Ulysses Thor",
+ config2.getString("user", null, "name"));
+ assertEquals("a.u.thor@example.com",
+ config2.getString("user", null, "email"));
+ }
+
+ @Test
+ public void testSetInXdg() throws Exception {
+ Path xdg = tmp.getRoot().toPath().resolve("xdg.cfg");
+ Files.writeString(xdg, "[user]\n\tname = Archibald Ulysses Thor");
+ Path user = tmp.getRoot().toPath().resolve("user.cfg");
+ UserConfigFile config = new UserConfigFile(null, user.toFile(),
+ xdg.toFile(), FS.DETECTED);
+ config.load();
+ assertEquals("Archibald Ulysses Thor",
+ config.getString("user", null, "name"));
+ config.setString("user", null, "email", "a.u.thor@example.com");
+ config.save();
+ assertFalse(Files.exists(user));
+ FileBasedConfig cfg = new FileBasedConfig(null, xdg.toFile(),
+ FS.DETECTED);
+ cfg.load();
+ assertEquals("Archibald Ulysses Thor",
+ cfg.getString("user", null, "name"));
+ assertEquals("a.u.thor@example.com",
+ cfg.getString("user", null, "email"));
+ }
+
+ @Test
+ public void testUserConfigCreated() throws Exception {
+ Path xdg = tmp.getRoot().toPath().resolve("xdg.cfg");
+ Files.writeString(xdg, "[user]\n\tname = Archibald Ulysses Thor");
+ Path user = tmp.getRoot().toPath().resolve("user.cfg");
+ Thread.sleep(3000); // Avoid racily clean isOutdated() below.
+ UserConfigFile config = new UserConfigFile(null, user.toFile(),
+ xdg.toFile(), FS.DETECTED);
+ config.load();
+ assertEquals("Archibald Ulysses Thor",
+ config.getString("user", null, "name"));
+ Files.writeString(user,
+ "[user]\n\temail = a.u.thor@example.com\n\tname = A U Thor");
+ assertEquals("Archibald Ulysses Thor",
+ config.getString("user", null, "name"));
+ assertTrue(config.isOutdated());
+ config.load();
+ assertEquals("A U Thor", config.getString("user", null, "name"));
+ assertEquals("a.u.thor@example.com",
+ config.getString("user", null, "email"));
+ }
+
+ @Test
+ public void testUserConfigDeleted() throws Exception {
+ Path xdg = tmp.getRoot().toPath().resolve("xdg.cfg");
+ Files.writeString(xdg, "[user]\n\tname = Archibald Ulysses Thor");
+ Path user = tmp.getRoot().toPath().resolve("user.cfg");
+ Files.writeString(user,
+ "[user]\n\temail = a.u.thor@example.com\n\tname = A U Thor");
+ Thread.sleep(3000); // Avoid racily clean isOutdated() below.
+ UserConfigFile config = new UserConfigFile(null, user.toFile(),
+ xdg.toFile(), FS.DETECTED);
+ config.load();
+ assertEquals("A U Thor", config.getString("user", null, "name"));
+ assertEquals("a.u.thor@example.com",
+ config.getString("user", null, "email"));
+ Files.delete(user);
+ assertEquals("A U Thor", config.getString("user", null, "name"));
+ assertEquals("a.u.thor@example.com",
+ config.getString("user", null, "email"));
+ assertTrue(config.isOutdated());
+ config.load();
+ assertEquals("Archibald Ulysses Thor",
+ config.getString("user", null, "name"));
+ assertNull(config.getString("user", null, "email"));
+ }
+
+ @Test
+ public void testXdgConfigDeleted() throws Exception {
+ Path xdg = tmp.getRoot().toPath().resolve("xdg.cfg");
+ Files.writeString(xdg, "[user]\n\tname = Archibald Ulysses Thor");
+ Path user = tmp.getRoot().toPath().resolve("user.cfg");
+ Thread.sleep(3000); // Avoid racily clean isOutdated() below.
+ UserConfigFile config = new UserConfigFile(null, user.toFile(),
+ xdg.toFile(), FS.DETECTED);
+ config.load();
+ assertEquals("Archibald Ulysses Thor",
+ config.getString("user", null, "name"));
+ Files.delete(xdg);
+ assertEquals("Archibald Ulysses Thor",
+ config.getString("user", null, "name"));
+ assertTrue(config.isOutdated());
+ config.load();
+ assertNull(config.getString("user", null, "name"));
+ }
+
+ @Test
+ public void testXdgConfigDeletedUserConfigExists() throws Exception {
+ Path xdg = tmp.getRoot().toPath().resolve("xdg.cfg");
+ Files.writeString(xdg, "[user]\n\tname = Archibald Ulysses Thor");
+ Path user = tmp.getRoot().toPath().resolve("user.cfg");
+ Files.writeString(user,
+ "[user]\n\temail = a.u.thor@example.com\n\tname = A U Thor");
+ Thread.sleep(3000); // Avoid racily clean isOutdated() below.
+ UserConfigFile config = new UserConfigFile(null, user.toFile(),
+ xdg.toFile(), FS.DETECTED);
+ config.load();
+ assertEquals("A U Thor", config.getString("user", null, "name"));
+ Files.delete(xdg);
+ assertTrue(config.isOutdated());
+ config.load();
+ assertEquals("A U Thor", config.getString("user", null, "name"));
+ }
+
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleAddTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleAddTest.java
index 300c869b78..4306975f7b 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleAddTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleAddTest.java
@@ -114,6 +114,13 @@ public class SubmoduleAddTest extends RepositoryTestCase {
try (Repository subModRepo = generator.getRepository()) {
assertNotNull(subModRepo);
assertEquals(subCommit, commit);
+ String worktreeDir = subModRepo.getConfig().getString(
+ ConfigConstants.CONFIG_CORE_SECTION, null,
+ ConfigConstants.CONFIG_KEY_WORKTREE);
+ assertEquals("../../../sub", worktreeDir);
+ String gitdir = read(new File(subModRepo.getWorkTree(),
+ Constants.DOT_GIT));
+ assertEquals("gitdir: ../.git/modules/sub", gitdir);
}
}
Status status = Git.wrap(db).status().call();
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleUpdateTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleUpdateTest.java
index b10bd73208..d54117005d 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleUpdateTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleUpdateTest.java
@@ -17,21 +17,25 @@ import java.io.File;
import java.io.IOException;
import java.util.Collection;
+import org.eclipse.jgit.api.CheckoutCommand;
import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.InitCommand;
+import org.eclipse.jgit.api.SubmoduleAddCommand;
import org.eclipse.jgit.api.SubmoduleUpdateCommand;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheEditor;
import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
import org.eclipse.jgit.dircache.DirCacheEntry;
+import org.eclipse.jgit.junit.JGitTestUtil;
import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.StoredConfig;
-import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.junit.Test;
@@ -40,6 +44,91 @@ import org.junit.Test;
*/
public class SubmoduleUpdateTest extends RepositoryTestCase {
+ private Repository submoduleRepo;
+
+ private Git git;
+
+ private AnyObjectId subRepoCommit2;
+
+ private void createSubmoduleRepo() throws IOException, GitAPIException {
+ File directory = createTempDirectory("submodule_repo");
+ InitCommand init = Git.init();
+ init.setDirectory(directory);
+ init.call();
+ submoduleRepo = Git.open(directory).getRepository();
+ try (Git sub = Git.wrap(submoduleRepo)) {
+ // commit something
+ JGitTestUtil.writeTrashFile(submoduleRepo, "commit1.txt",
+ "commit 1");
+ sub.add().addFilepattern("commit1.txt").call();
+ sub.commit().setMessage("commit 1").call().getId();
+
+ JGitTestUtil.writeTrashFile(submoduleRepo, "commit2.txt",
+ "commit 2");
+ sub.add().addFilepattern("commit2.txt").call();
+ subRepoCommit2 = sub.commit().setMessage("commit 2").call().getId();
+ }
+ }
+
+ private void addSubmodule(String path) throws GitAPIException {
+ SubmoduleAddCommand command = new SubmoduleAddCommand(db);
+ command.setPath(path);
+ String uri = submoduleRepo.getDirectory().toURI().toString();
+ command.setURI(uri);
+ try (Repository repo = command.call()) {
+ assertNotNull(repo);
+ }
+ git.add().addFilepattern(path).addFilepattern(Constants.DOT_GIT_MODULES)
+ .call();
+ git.commit().setMessage("adding submodule").call();
+ recursiveDelete(new File(git.getRepository().getWorkTree(), path));
+ recursiveDelete(
+ new File(new File(git.getRepository().getCommonDirectory(),
+ Constants.MODULES), path));
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ createSubmoduleRepo();
+
+ git = Git.wrap(db);
+ // commit something
+ writeTrashFile("initial.txt", "initial");
+ git.add().addFilepattern("initial.txt").call();
+ git.commit().setMessage("initial commit").call();
+ }
+
+ public void updateModeClonedRestoredSubmoduleTemplate(String mode)
+ throws Exception {
+ String path = "sub";
+ addSubmodule(path);
+
+ StoredConfig cfg = git.getRepository().getConfig();
+ if (mode != null) {
+ cfg.load();
+ cfg.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path,
+ ConfigConstants.CONFIG_KEY_UPDATE, mode);
+ cfg.save();
+ }
+ SubmoduleUpdateCommand update = new SubmoduleUpdateCommand(db);
+ update.call();
+ try (Git subGit = Git.open(new File(db.getWorkTree(), path))) {
+ update.call();
+ assertEquals(subRepoCommit2.getName(),
+ subGit.getRepository().getBranch());
+ }
+
+ recursiveDelete(new File(db.getWorkTree(), path));
+
+ update.call();
+ try (Git subGit = Git.open(new File(db.getWorkTree(), path))) {
+ update.call();
+ assertEquals(subRepoCommit2.getName(),
+ subGit.getRepository().getBranch());
+ }
+ }
+
@Test
public void repositoryWithNoSubmodules() throws GitAPIException {
SubmoduleUpdateCommand command = new SubmoduleUpdateCommand(db);
@@ -50,35 +139,9 @@ public class SubmoduleUpdateTest extends RepositoryTestCase {
@Test
public void repositoryWithSubmodule() throws Exception {
- writeTrashFile("file.txt", "content");
- Git git = Git.wrap(db);
- git.add().addFilepattern("file.txt").call();
- final RevCommit commit = git.commit().setMessage("create file").call();
final String path = "sub";
- DirCache cache = db.lockDirCache();
- DirCacheEditor editor = cache.editor();
- editor.add(new PathEdit(path) {
-
- @Override
- public void apply(DirCacheEntry ent) {
- ent.setFileMode(FileMode.GITLINK);
- ent.setObjectId(commit);
- }
- });
- editor.commit();
-
- StoredConfig config = db.getConfig();
- config.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path,
- ConfigConstants.CONFIG_KEY_URL, db.getDirectory().toURI()
- .toString());
- config.save();
-
- FileBasedConfig modulesConfig = new FileBasedConfig(new File(
- db.getWorkTree(), Constants.DOT_GIT_MODULES), db.getFS());
- modulesConfig.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path,
- ConfigConstants.CONFIG_KEY_PATH, path);
- modulesConfig.save();
+ addSubmodule(path);
SubmoduleUpdateCommand command = new SubmoduleUpdateCommand(db);
Collection<String> updated = command.call();
@@ -90,14 +153,22 @@ public class SubmoduleUpdateTest extends RepositoryTestCase {
assertTrue(generator.next());
try (Repository subRepo = generator.getRepository()) {
assertNotNull(subRepo);
- assertEquals(commit, subRepo.resolve(Constants.HEAD));
+ assertEquals(subRepoCommit2, subRepo.resolve(Constants.HEAD));
+ String worktreeDir = subRepo.getConfig().getString(
+ ConfigConstants.CONFIG_CORE_SECTION, null,
+ ConfigConstants.CONFIG_KEY_WORKTREE);
+ assertEquals("../../../sub", worktreeDir);
+ String gitdir = read(
+ new File(subRepo.getWorkTree(), Constants.DOT_GIT));
+ assertEquals("gitdir: ../.git/modules/sub", gitdir);
+
}
}
}
@Test
- public void repositoryWithUnconfiguredSubmodule() throws IOException,
- GitAPIException {
+ public void repositoryWithUnconfiguredSubmodule()
+ throws IOException, GitAPIException {
final ObjectId id = ObjectId
.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
final String path = "sub";
@@ -113,16 +184,14 @@ public class SubmoduleUpdateTest extends RepositoryTestCase {
});
editor.commit();
- FileBasedConfig modulesConfig = new FileBasedConfig(new File(
- db.getWorkTree(), Constants.DOT_GIT_MODULES), db.getFS());
+ FileBasedConfig modulesConfig = new FileBasedConfig(
+ new File(db.getWorkTree(), Constants.DOT_GIT_MODULES),
+ db.getFS());
modulesConfig.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path,
ConfigConstants.CONFIG_KEY_PATH, path);
String url = "git://server/repo.git";
modulesConfig.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path,
ConfigConstants.CONFIG_KEY_URL, url);
- String update = "rebase";
- modulesConfig.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path,
- ConfigConstants.CONFIG_KEY_UPDATE, update);
modulesConfig.save();
SubmoduleUpdateCommand command = new SubmoduleUpdateCommand(db);
@@ -132,8 +201,8 @@ public class SubmoduleUpdateTest extends RepositoryTestCase {
}
@Test
- public void repositoryWithInitializedSubmodule() throws IOException,
- GitAPIException {
+ public void repositoryWithInitializedSubmodule()
+ throws IOException, GitAPIException {
final ObjectId id = ObjectId
.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
final String path = "sub";
@@ -160,4 +229,77 @@ public class SubmoduleUpdateTest extends RepositoryTestCase {
assertNotNull(updated);
assertTrue(updated.isEmpty());
}
+
+ @Test
+ public void updateModeMergeClonedRestoredSubmodule() throws Exception {
+ updateModeClonedRestoredSubmoduleTemplate(
+ ConfigConstants.CONFIG_KEY_MERGE);
+ }
+
+ @Test
+ public void updateModeRebaseClonedRestoredSubmodule() throws Exception {
+ updateModeClonedRestoredSubmoduleTemplate(
+ ConfigConstants.CONFIG_KEY_REBASE);
+ }
+
+ @Test
+ public void updateModeCheckoutClonedRestoredSubmodule() throws Exception {
+ updateModeClonedRestoredSubmoduleTemplate(
+ ConfigConstants.CONFIG_KEY_CHECKOUT);
+ }
+
+ @Test
+ public void updateModeMissingClonedRestoredSubmodule() throws Exception {
+ updateModeClonedRestoredSubmoduleTemplate(null);
+ }
+
+ @Test
+ public void updateMode() throws Exception {
+ String path = "sub";
+ addSubmodule(path);
+
+ StoredConfig cfg = git.getRepository().getConfig();
+ cfg.load();
+ cfg.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path,
+ ConfigConstants.CONFIG_KEY_UPDATE,
+ ConfigConstants.CONFIG_KEY_REBASE);
+ cfg.save();
+
+ SubmoduleUpdateCommand update = new SubmoduleUpdateCommand(db);
+ update.call();
+ try (Git subGit = Git.open(new File(db.getWorkTree(), path))) {
+ CheckoutCommand checkout = subGit.checkout();
+ checkout.setName("master");
+ checkout.call();
+ update.call();
+ assertEquals("master", subGit.getRepository().getBranch());
+ }
+
+ cfg.load();
+ cfg.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path,
+ ConfigConstants.CONFIG_KEY_UPDATE,
+ ConfigConstants.CONFIG_KEY_CHECKOUT);
+ cfg.save();
+
+ update.call();
+ try (Git subGit = Git.open(new File(db.getWorkTree(), path))) {
+ assertEquals(subRepoCommit2.getName(),
+ subGit.getRepository().getBranch());
+ }
+
+ cfg.load();
+ cfg.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path,
+ ConfigConstants.CONFIG_KEY_UPDATE,
+ ConfigConstants.CONFIG_KEY_MERGE);
+ cfg.save();
+
+ update.call();
+ try (Git subGit = Git.open(new File(db.getWorkTree(), path))) {
+ CheckoutCommand checkout = subGit.checkout();
+ checkout.setName("master");
+ checkout.call();
+ update.call();
+ assertEquals("master", subGit.getRepository().getBranch());
+ }
+ }
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/symlinks/DirectoryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/symlinks/DirectoryTest.java
new file mode 100644
index 0000000000..490c45b558
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/symlinks/DirectoryTest.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2023 Thomas Wolf <twolf@apache.org> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.symlinks;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.ResetCommand.ResetType;
+import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ConfigConstants;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.patch.Patch;
+import org.eclipse.jgit.patch.PatchApplier;
+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.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class DirectoryTest extends RepositoryTestCase {
+
+ @BeforeClass
+ public static void checkPrecondition() throws Exception {
+ Assume.assumeTrue(FS.DETECTED.supportsSymlinks());
+ Path tempDir = Files.createTempDirectory("jgit");
+ try {
+ Path a = tempDir.resolve("a");
+ Files.writeString(a, "test");
+ Path b = tempDir.resolve("A");
+ Assume.assumeTrue(Files.exists(b));
+ } finally {
+ FileUtils.delete(tempDir.toFile(),
+ FileUtils.RECURSIVE | FileUtils.IGNORE_ERRORS);
+ }
+ }
+
+ @Parameters(name = "core.symlinks={0}")
+ public static Boolean[] parameters() {
+ return new Boolean[] { Boolean.TRUE, Boolean.FALSE };
+ }
+
+ @Parameter(0)
+ public boolean useSymlinks;
+
+ private void checkFiles() throws Exception {
+ File a = new File(trash, "a");
+ assertTrue("a should be a directory",
+ Files.isDirectory(a.toPath(), LinkOption.NOFOLLOW_LINKS));
+ File b = new File(a, "b");
+ assertTrue("a/b should exist", b.isFile());
+ File x = new File(trash, "x");
+ assertTrue("x should be a directory",
+ Files.isDirectory(x.toPath(), LinkOption.NOFOLLOW_LINKS));
+ File y = new File(x, "y");
+ assertTrue("x/y should exist", y.isFile());
+ }
+
+ @Test
+ public void testCheckout() throws Exception {
+ StoredConfig config = db.getConfig();
+ config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
+ ConfigConstants.CONFIG_KEY_SYMLINKS, useSymlinks);
+ config.save();
+ try (TestRepository<Repository> repo = new TestRepository<>(db)) {
+ db.incrementOpen();
+ // Create links directly in the git repo, then use a hard reset
+ // to get them into the workspace.
+ RevCommit base = repo.commit(
+ repo.tree(
+ repo.link("A", repo.blob(".git")),
+ repo.file("a/b", repo.blob("test")),
+ repo.file("x/y", repo.blob("test2"))));
+ try (Git git = new Git(db)) {
+ git.reset().setMode(ResetType.HARD).setRef(base.name()).call();
+ File b = new File(new File(trash, ".git"), "b");
+ assertFalse(".git/b should not exist", b.exists());
+ checkFiles();
+ }
+ }
+ }
+
+ @Test
+ public void testCheckout2() throws Exception {
+ StoredConfig config = db.getConfig();
+ config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
+ ConfigConstants.CONFIG_KEY_SYMLINKS, useSymlinks);
+ config.save();
+ try (TestRepository<Repository> repo = new TestRepository<>(db)) {
+ db.incrementOpen();
+ RevCommit base = repo.commit(
+ repo.tree(
+ repo.link("A/B", repo.blob("../.git")),
+ repo.file("a/b/a/b", repo.blob("test")),
+ repo.file("x/y", repo.blob("test2"))));
+ try (Git git = new Git(db)) {
+ boolean testFiles = true;
+ try {
+ git.reset().setMode(ResetType.HARD).setRef(base.name())
+ .call();
+ } catch (Exception e) {
+ if (!useSymlinks) {
+ // There is a file in the middle of the path where we'd
+ // expect a directory. This case is not handled
+ // anywhere. What would be a better reply than an IOE?
+ testFiles = false;
+ } else {
+ throw e;
+ }
+ }
+ File a = new File(new File(trash, ".git"), "a");
+ assertFalse(".git/a should not exist", a.exists());
+ if (testFiles) {
+ a = new File(trash, "a");
+ assertTrue("a should be a directory", Files.isDirectory(
+ a.toPath(), LinkOption.NOFOLLOW_LINKS));
+ File b = new File(a, "b");
+ assertTrue("a/b should be a directory", Files.isDirectory(
+ a.toPath(), LinkOption.NOFOLLOW_LINKS));
+ a = new File(b, "a");
+ assertTrue("a/b/a should be a directory", Files.isDirectory(
+ a.toPath(), LinkOption.NOFOLLOW_LINKS));
+ b = new File(a, "b");
+ assertTrue("a/b/a/b should exist", b.isFile());
+ File x = new File(trash, "x");
+ assertTrue("x should be a directory", Files.isDirectory(
+ x.toPath(), LinkOption.NOFOLLOW_LINKS));
+ File y = new File(x, "y");
+ assertTrue("x/y should exist", y.isFile());
+ }
+ }
+ }
+ }
+
+ @Test
+ public void testMerge() throws Exception {
+ StoredConfig config = db.getConfig();
+ config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
+ ConfigConstants.CONFIG_KEY_SYMLINKS, useSymlinks);
+ config.save();
+ try (TestRepository<Repository> repo = new TestRepository<>(db)) {
+ db.incrementOpen();
+ RevCommit base = repo.commit(
+ repo.tree(repo.file("q", repo.blob("test"))));
+ RevCommit side = repo.commit(
+ repo.tree(
+ repo.link("A", repo.blob(".git")),
+ repo.file("a/b", repo.blob("test")),
+ repo.file("x/y", repo.blob("test2"))));
+ try (Git git = new Git(db)) {
+ git.reset().setMode(ResetType.HARD).setRef(base.name()).call();
+ git.merge().include(side)
+ .setMessage("merged").call();
+ File b = new File(new File(trash, ".git"), "b");
+ assertFalse(".git/b should not exist", b.exists());
+ checkFiles();
+ }
+ }
+ }
+
+ @Test
+ public void testMerge2() throws Exception {
+ StoredConfig config = db.getConfig();
+ config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
+ ConfigConstants.CONFIG_KEY_SYMLINKS, useSymlinks);
+ config.save();
+ try (TestRepository<Repository> repo = new TestRepository<>(db)) {
+ db.incrementOpen();
+ RevCommit base = repo.commit(
+ repo.tree(
+ repo.file("q", repo.blob("test")),
+ repo.link("A", repo.blob(".git"))));
+ RevCommit side = repo.commit(
+ repo.tree(
+ repo.file("a/b", repo.blob("test")),
+ repo.file("x/y", repo.blob("test2"))));
+ try (Git git = new Git(db)) {
+ git.reset().setMode(ResetType.HARD).setRef(base.name()).call();
+ git.merge().include(side)
+ .setMessage("merged").call();
+ File b = new File(new File(trash, ".git"), "b");
+ assertFalse(".git/b should not exist", b.exists());
+ checkFiles();
+ }
+ }
+ }
+
+ @Test
+ public void testApply() throws Exception {
+ StoredConfig config = db.getConfig();
+ config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
+ ConfigConstants.CONFIG_KEY_SYMLINKS, useSymlinks);
+ config.save();
+ // PatchApplier doesn't do symlinks yet.
+ try (TestRepository<Repository> repo = new TestRepository<>(db)) {
+ db.incrementOpen();
+ RevCommit base = repo.commit(
+ repo.tree(
+ repo.file("x", repo.blob("test")),
+ repo.link("A", repo.blob(".git"))));
+ try (Git git = new Git(db)) {
+ git.reset().setMode(ResetType.HARD).setRef(base.name()).call();
+ Patch patch = new Patch();
+ try (InputStream patchStream = this.getClass()
+ .getResourceAsStream("dirtest.patch")) {
+ patch.parse(patchStream);
+ }
+ boolean testFiles = true;
+ try {
+ PatchApplier.Result result = new PatchApplier(db)
+ .applyPatch(patch);
+ assertNotNull(result);
+ } catch (IOException e) {
+ if (!useSymlinks) {
+ // There is a file there, so the patch won't apply.
+ // Unclear whether an IOE is the correct response,
+ // though. Probably some negative PatchApplier.Result is
+ // more appropriate.
+ testFiles = false;
+ } else {
+ throw e;
+ }
+ }
+ File b = new File(new File(trash, ".git"), "b");
+ assertFalse(".git/b should not exist", b.exists());
+ if (testFiles) {
+ File a = new File(trash, "a");
+ assertTrue("a should be a directory",
+ Files.isDirectory(a.toPath(), LinkOption.NOFOLLOW_LINKS));
+ b = new File(a, "b");
+ assertTrue("a/b should exist", b.isFile());
+ }
+ }
+ }
+ }
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/test/resources/SampleDataRepositoryTestCase.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/test/resources/SampleDataRepositoryTestCase.java
index 578128326f..4f5e35f129 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/test/resources/SampleDataRepositoryTestCase.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/test/resources/SampleDataRepositoryTestCase.java
@@ -35,6 +35,7 @@ public abstract class SampleDataRepositoryTestCase extends RepositoryTestCase {
* @param repo
* test repository to receive packfile copies
* @throws IOException
+ * an error occurred
*/
public static void copyCGitTestPacks(FileRepository repo) throws IOException {
final String[] packs = {
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 c47e591445..0ba8926a7f 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
@@ -25,10 +25,6 @@ import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.transport.resolver.ReceivePackFactory;
-import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException;
-import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -46,16 +42,8 @@ public class AtomicPushTest {
public void setUp() throws Exception {
server = newRepo("server");
client = newRepo("client");
- testProtocol = new TestProtocol<>(
- null,
- new ReceivePackFactory<Object>() {
- @Override
- public ReceivePack create(Object req, Repository db)
- throws ServiceNotEnabledException,
- ServiceNotAuthorizedException {
- return new ReceivePack(db);
- }
- });
+ testProtocol = new TestProtocol<>(null,
+ (req, db) -> new ReceivePack(db));
uri = testProtocol.register(ctx, server);
try (TestRepository<?> clientRepo = new TestRepository<>(client)) {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/HttpAuthTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/HttpAuthTest.java
index 947ca97615..c1ab43eadf 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/HttpAuthTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/HttpAuthTest.java
@@ -160,7 +160,7 @@ public class HttpAuthTest {
String value = header.substring(i + 1).trim();
if (!headerFields.containsKey(key))
- headerFields.put(key, new ArrayList<String>());
+ headerFields.put(key, new ArrayList<>());
List<String> values = headerFields.get(key);
values.add(value);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/InMemoryPack.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/InMemoryPack.java
new file mode 100644
index 0000000000..cad80ef707
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/InMemoryPack.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2023, Google LLC. and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.transport;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.MessageDigest;
+import java.util.zip.Deflater;
+
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.util.NB;
+import org.eclipse.jgit.util.TemporaryBuffer;
+import org.eclipse.jgit.util.TemporaryBuffer.Heap;
+
+/**
+ * Helper class to create packs for tests.
+ */
+public class InMemoryPack {
+
+ private final Heap tinyPack;
+
+ public InMemoryPack() {
+ this(1024);
+ }
+
+ public InMemoryPack(int size) {
+ this.tinyPack = new TemporaryBuffer.Heap(size);
+ }
+
+ public InMemoryPack header(int cnt)
+ throws IOException {
+ final byte[] hdr = new byte[8];
+ NB.encodeInt32(hdr, 0, 2);
+ NB.encodeInt32(hdr, 4, cnt);
+
+ tinyPack.write(Constants.PACK_SIGNATURE);
+ tinyPack.write(hdr, 0, 8);
+ return this;
+ }
+
+ public InMemoryPack write(int i) throws IOException {
+ tinyPack.write(i);
+ return this;
+ }
+
+ public InMemoryPack deflate(byte[] content)
+ throws IOException {
+ Deflater deflater = new Deflater();
+ byte[] buf = new byte[128];
+ deflater.setInput(content, 0, content.length);
+ deflater.finish();
+ do {
+ final int n = deflater.deflate(buf, 0, buf.length);
+ if (n > 0)
+ tinyPack.write(buf, 0, n);
+ } while (!deflater.finished());
+ return this;
+ }
+
+ public InMemoryPack copyRaw(AnyObjectId o) throws IOException {
+ o.copyRawTo(tinyPack);
+ return this;
+ }
+
+ public InMemoryPack digest() throws IOException {
+ MessageDigest md = Constants.newMessageDigest();
+ md.update(tinyPack.toByteArray());
+ tinyPack.write(md.digest());
+ return this;
+ }
+
+ public InputStream toInputStream() throws IOException {
+ return new ByteArrayInputStream(tinyPack.toByteArray());
+ }
+
+ public byte[] toByteArray() throws IOException {
+ return tinyPack.toByteArray();
+ }
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ObjectDirectoryPackParserTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ObjectDirectoryPackParserTest.java
new file mode 100644
index 0000000000..b17c577087
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ObjectDirectoryPackParserTest.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2021, Google LLC. and others
+ * Copyright (C) 2008, Imran M Yousuf <imyousuf@smartitengineering.com>
+ * Copyright (C) 2007-2008, Robin Rosenberg <robin.rosenberg@dewire.com>
+ * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.transport;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.MessageDigest;
+import java.util.zip.Deflater;
+
+import org.eclipse.jgit.internal.storage.file.ObjectDirectoryPackParser;
+import org.eclipse.jgit.internal.storage.file.Pack;
+import org.eclipse.jgit.junit.JGitTestUtil;
+import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.junit.TestRepository;
+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.ObjectInserter;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevBlob;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.NB;
+import org.eclipse.jgit.util.TemporaryBuffer;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Pack parsing is covered in {@link PackParserTest}.
+ *
+ * Here we test ObjectDirectoryPackParser specific parts. e.g. that is creates
+ * the object-size index.
+ */
+public class ObjectDirectoryPackParserTest extends RepositoryTestCase {
+
+ @Before
+ public void setup() throws IOException {
+ FileBasedConfig jGitConfig = mockSystemReader.getJGitConfig();
+ jGitConfig.setInt(ConfigConstants.CONFIG_PACK_SECTION, null,
+ ConfigConstants.CONFIG_KEY_MIN_BYTES_OBJ_SIZE_INDEX, 7);
+ jGitConfig.save();
+ }
+
+ /**
+ * Test indexing one of the test packs in the egit repo. It has deltas.
+ *
+ * @throws IOException
+ */
+ @Test
+ public void testGitPack() throws IOException {
+ File packFile = JGitTestUtil.getTestResourceFile("pack-34be9032ac282b11fa9babdc2b2a93ca996c9c2f.pack");
+ try (InputStream is = new FileInputStream(packFile)) {
+ ObjectDirectoryPackParser p = index(is);
+ p.parse(NullProgressMonitor.INSTANCE);
+
+ Pack pack = p.getPack();
+ assertTrue(pack.hasObjectSizeIndex());
+
+ // Only blobs in the pack
+ ObjectId blob1 = ObjectId
+ .fromString("6ff87c4664981e4397625791c8ea3bbb5f2279a3");
+ ObjectId blob2 = ObjectId
+ .fromString("5b6e7c66c276e7610d4a73c70ec1a1f7c1003259");
+ assertEquals(18787, pack.getIndexedObjectSize(blob1));
+ assertEquals(18009, pack.getIndexedObjectSize(blob2));
+
+ // Indexed sizes match object db sizes
+ assertEquals(db.getObjectDatabase().open(blob1).getSize(),
+ pack.getIndexedObjectSize(blob1));
+ assertEquals(db.getObjectDatabase().open(blob2).getSize(),
+ pack.getIndexedObjectSize(blob2));
+
+ }
+ }
+
+ /**
+ * This is just another pack. It so happens that we have two convenient pack to
+ * test with in the repository.
+ *
+ * @throws IOException
+ */
+ @Test
+ public void testAnotherGitPack() throws IOException {
+ File packFile = JGitTestUtil.getTestResourceFile("pack-df2982f284bbabb6bdb59ee3fcc6eb0983e20371.pack");
+ try (InputStream is = new FileInputStream(packFile)) {
+ ObjectDirectoryPackParser p = index(is);
+ p.parse(NullProgressMonitor.INSTANCE);
+ Pack pack = p.getPack();
+
+ // Blob smaller than threshold:
+ assertEquals(-1, pack.getIndexedObjectSize(ObjectId
+ .fromString("15fae9e651043de0fd1deef588aa3fbf5a7a41c6")));
+
+ // Blob bigger than threshold
+ assertEquals(10, pack.getIndexedObjectSize(ObjectId
+ .fromString("8230f48330e0055d9e0bc5a2a77718f6dd9324b8")));
+
+ // A commit (not indexed)
+ assertEquals(-1, pack.getIndexedObjectSize(ObjectId
+ .fromString("d0114ab8ac326bab30e3a657a0397578c5a1af88")));
+
+ // Object not in pack
+ assertEquals(-1, pack.getIndexedObjectSize(ObjectId
+ .fromString("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")));
+ }
+ }
+
+ @Test
+ public void testTinyThinPack() throws Exception {
+ // less than 16 bytes, so its length fits in a single byte later
+ String base = "abcdefghijklmn";
+ RevBlob a;
+ try (TestRepository d = new TestRepository<Repository>(db)) {
+ a = d.blob(base);
+ }
+
+ TemporaryBuffer.Heap pack = new TemporaryBuffer.Heap(1024);
+
+ packHeader(pack, 1);
+
+ pack.write(Constants.OBJ_REF_DELTA << 4 | 4);
+ a.copyRawTo(pack);
+ deflate(pack, new byte[] { (byte) base.length(), // size of the base
+ (byte) (base.length() + 1), // size after reconstruction
+ 0x1, 'b' }); // append one byte
+
+ digest(pack);
+
+ ObjectDirectoryPackParser p = index(new ByteArrayInputStream(pack.toByteArray()));
+ p.setAllowThin(true);
+ p.parse(NullProgressMonitor.INSTANCE);
+
+ Pack writtenPack = p.getPack();
+ // base
+ assertEquals(base.length(), writtenPack.getIndexedObjectSize(a));
+ // undeltified blob
+ assertEquals(base.length() + 1,
+ writtenPack.getIndexedObjectSize(ObjectId.fromString(
+ "f177875498138143c9657cc52b049ad4d20d5223")));
+ }
+
+ @Test
+ public void testPackWithDuplicateBlob() throws Exception {
+ final byte[] data = Constants.encode("0123456789abcdefg");
+ RevBlob blob;
+ try (TestRepository<Repository> d = new TestRepository<>(db)) {
+ blob = d.blob(data);
+ assertTrue(db.getObjectDatabase().has(blob));
+ }
+
+ TemporaryBuffer.Heap pack = new TemporaryBuffer.Heap(1024);
+ packHeader(pack, 1);
+ pack.write(Constants.OBJ_BLOB << 4 | 0x80 | 1);
+ pack.write(1);
+ deflate(pack, data);
+ digest(pack);
+
+ ObjectDirectoryPackParser p = index(
+ new ByteArrayInputStream(pack.toByteArray()));
+ p.setAllowThin(false);
+ p.parse(NullProgressMonitor.INSTANCE);
+
+ assertEquals(data.length, p.getPack().getIndexedObjectSize(blob));
+ }
+
+ private static void packHeader(TemporaryBuffer.Heap tinyPack, int cnt)
+ throws IOException {
+ final byte[] hdr = new byte[8];
+ NB.encodeInt32(hdr, 0, 2);
+ NB.encodeInt32(hdr, 4, cnt);
+
+ tinyPack.write(Constants.PACK_SIGNATURE);
+ tinyPack.write(hdr, 0, 8);
+ }
+
+ private static void deflate(TemporaryBuffer.Heap tinyPack,
+ final byte[] content)
+ throws IOException {
+ final Deflater deflater = new Deflater();
+ final byte[] buf = new byte[128];
+ deflater.setInput(content, 0, content.length);
+ deflater.finish();
+ do {
+ final int n = deflater.deflate(buf, 0, buf.length);
+ if (n > 0)
+ tinyPack.write(buf, 0, n);
+ } while (!deflater.finished());
+ }
+
+ private static void digest(TemporaryBuffer.Heap buf) throws IOException {
+ MessageDigest md = Constants.newMessageDigest();
+ md.update(buf.toByteArray());
+ buf.write(md.digest());
+ }
+
+ private ObjectInserter inserter;
+
+ @After
+ public void release() {
+ if (inserter != null) {
+ inserter.close();
+ }
+ }
+
+ private ObjectDirectoryPackParser index(InputStream in) throws IOException {
+ if (inserter == null)
+ inserter = db.newObjectInserter();
+ return (ObjectDirectoryPackParser) inserter.newPackParser(in);
+ }
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PackParserTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PackParserTest.java
index f02428efc9..6148df99f3 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PackParserTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PackParserTest.java
@@ -22,12 +22,10 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
-import java.security.MessageDigest;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.zip.Deflater;
import org.eclipse.jgit.errors.TooLargeObjectInPackException;
import org.eclipse.jgit.internal.JGitText;
@@ -42,8 +40,6 @@ import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevBlob;
-import org.eclipse.jgit.util.NB;
-import org.eclipse.jgit.util.TemporaryBuffer;
import org.eclipse.jgit.util.io.UnionInputStream;
import org.junit.After;
import org.junit.Test;
@@ -193,17 +189,14 @@ public class PackParserTest extends RepositoryTestCase {
a = d.blob("a");
}
- TemporaryBuffer.Heap pack = new TemporaryBuffer.Heap(1024);
+ InMemoryPack pack = new InMemoryPack();
+ pack.header(1);
+ pack.write(Constants.OBJ_REF_DELTA << 4 | 4);
+ pack.copyRaw(a);
+ pack.deflate(new byte[] { 0x1, 0x1, 0x1, 'b' });
+ pack.digest();
- packHeader(pack, 1);
-
- pack.write((Constants.OBJ_REF_DELTA) << 4 | 4);
- a.copyRawTo(pack);
- deflate(pack, new byte[] { 0x1, 0x1, 0x1, 'b' });
-
- digest(pack);
-
- PackParser p = index(new ByteArrayInputStream(pack.toByteArray()));
+ PackParser p = index(pack.toInputStream());
p.setAllowThin(true);
p.parse(NullProgressMonitor.INSTANCE);
}
@@ -216,14 +209,14 @@ public class PackParserTest extends RepositoryTestCase {
assertTrue(db.getObjectDatabase().has(d.blob(data)));
}
- TemporaryBuffer.Heap pack = new TemporaryBuffer.Heap(1024);
- packHeader(pack, 1);
- pack.write((Constants.OBJ_BLOB) << 4 | 0x80 | 1);
+ InMemoryPack pack = new InMemoryPack();
+ pack.header(1);
+ pack.write(Constants.OBJ_BLOB << 4 | 0x80 | 1);
pack.write(1);
- deflate(pack, data);
- digest(pack);
+ pack.deflate(data);
+ pack.digest();
- PackParser p = index(new ByteArrayInputStream(pack.toByteArray()));
+ PackParser p = index(pack.toInputStream());
p.setAllowThin(false);
p.parse(NullProgressMonitor.INSTANCE);
}
@@ -236,16 +229,16 @@ public class PackParserTest extends RepositoryTestCase {
assertTrue(db.getObjectDatabase().has(d.blob(data)));
}
- TemporaryBuffer.Heap pack = new TemporaryBuffer.Heap(1024);
- packHeader(pack, 2);
- pack.write((Constants.OBJ_BLOB) << 4 | 10); // offset 12
- deflate(pack, data);
- pack.write((Constants.OBJ_OFS_DELTA) << 4 | 4); // offset 31
+ InMemoryPack pack = new InMemoryPack();
+ pack.header(2);
+ pack.write(Constants.OBJ_BLOB << 4 | 10); // offset 12
+ pack.deflate(data);
+ pack.write(Constants.OBJ_OFS_DELTA << 4 | 4); // offset 31
pack.write(19);
- deflate(pack, new byte[] { 0xA, 0xB, 0x1, 'b' });
- digest(pack);
+ pack.deflate(new byte[] { 0xA, 0xB, 0x1, 'b' });
+ pack.digest();
- PackParser p = index(new ByteArrayInputStream(pack.toByteArray()));
+ PackParser p = index(pack.toInputStream());
p.parse(NullProgressMonitor.INSTANCE);
List<PackedObjectInfo> sortedObjectList = p.getSortedObjectList(null);
@@ -275,15 +268,15 @@ public class PackParserTest extends RepositoryTestCase {
a = d.blob("a");
}
- TemporaryBuffer.Heap pack = new TemporaryBuffer.Heap(1024);
- packHeader(pack, 1);
- pack.write((Constants.OBJ_REF_DELTA) << 4 | 4);
- a.copyRawTo(pack);
- deflate(pack, new byte[] { 0x1, 0x1, 0x1, 'b' });
- digest(pack);
+ InMemoryPack pack = new InMemoryPack();
+ pack.header(1);
+ pack.write(Constants.OBJ_REF_DELTA << 4 | 4);
+ pack.copyRaw(a);
+ pack.deflate(new byte[] { 0x1, 0x1, 0x1, 'b' });
+ pack.digest();
PackParser p = index(new UnionInputStream(
- new ByteArrayInputStream(pack.toByteArray()),
+ pack.toInputStream(),
new ByteArrayInputStream(new byte[] { 0x7e })));
p.setAllowThin(true);
p.setCheckEofAfterPackFooter(true);
@@ -305,22 +298,21 @@ public class PackParserTest extends RepositoryTestCase {
d.blob(data);
}
- TemporaryBuffer.Heap pack = new TemporaryBuffer.Heap(1024);
-
- packHeader(pack, 1);
- pack.write((Constants.OBJ_BLOB) << 4 | 10);
- deflate(pack, data);
- digest(pack);
+ InMemoryPack pack = new InMemoryPack();
+ pack.header(1);
+ pack.write(Constants.OBJ_BLOB << 4 | 10);
+ pack.deflate(data);
+ pack.digest();
- PackParser p = index(new ByteArrayInputStream(pack.toByteArray()));
+ PackParser p = index(pack.toInputStream());
p.setMaxObjectSizeLimit(11);
p.parse(NullProgressMonitor.INSTANCE);
- p = index(new ByteArrayInputStream(pack.toByteArray()));
+ p = index(pack.toInputStream());
p.setMaxObjectSizeLimit(10);
p.parse(NullProgressMonitor.INSTANCE);
- p = index(new ByteArrayInputStream(pack.toByteArray()));
+ p = index(pack.toInputStream());
p.setMaxObjectSizeLimit(9);
try {
p.parse(NullProgressMonitor.INSTANCE);
@@ -339,21 +331,20 @@ public class PackParserTest extends RepositoryTestCase {
a = d.blob("a");
}
- TemporaryBuffer.Heap pack = new TemporaryBuffer.Heap(1024);
-
- packHeader(pack, 1);
- pack.write((Constants.OBJ_REF_DELTA) << 4 | 14);
- a.copyRawTo(pack);
- deflate(pack, new byte[] { 1, 11, 11, 'a', '0', '1', '2', '3', '4',
+ InMemoryPack pack = new InMemoryPack();
+ pack.header(1);
+ pack.write(Constants.OBJ_REF_DELTA << 4 | 14);
+ pack.copyRaw(a);
+ pack.deflate(new byte[] { 1, 11, 11, 'a', '0', '1', '2', '3', '4',
'5', '6', '7', '8', '9' });
- digest(pack);
+ pack.digest();
- PackParser p = index(new ByteArrayInputStream(pack.toByteArray()));
+ PackParser p = index(pack.toInputStream());
p.setAllowThin(true);
p.setMaxObjectSizeLimit(14);
p.parse(NullProgressMonitor.INSTANCE);
- p = index(new ByteArrayInputStream(pack.toByteArray()));
+ p = index(pack.toInputStream());
p.setAllowThin(true);
p.setMaxObjectSizeLimit(13);
try {
@@ -373,20 +364,19 @@ public class PackParserTest extends RepositoryTestCase {
a = d.blob("0123456789");
}
- TemporaryBuffer.Heap pack = new TemporaryBuffer.Heap(1024);
-
- packHeader(pack, 1);
- pack.write((Constants.OBJ_REF_DELTA) << 4 | 4);
- a.copyRawTo(pack);
- deflate(pack, new byte[] { 10, 11, 1, 'a' });
- digest(pack);
+ InMemoryPack pack = new InMemoryPack();
+ pack.header(1);
+ pack.write(Constants.OBJ_REF_DELTA << 4 | 4);
+ pack.copyRaw(a);
+ pack.deflate(new byte[] { 10, 11, 1, 'a' });
+ pack.digest();
- PackParser p = index(new ByteArrayInputStream(pack.toByteArray()));
+ PackParser p = index(pack.toInputStream());
p.setAllowThin(true);
p.setMaxObjectSizeLimit(11);
p.parse(NullProgressMonitor.INSTANCE);
- p = index(new ByteArrayInputStream(pack.toByteArray()));
+ p = index(pack.toInputStream());
p.setAllowThin(true);
p.setMaxObjectSizeLimit(10);
try {
@@ -406,12 +396,12 @@ public class PackParserTest extends RepositoryTestCase {
a = d.blob("a");
}
- TemporaryBuffer.Heap pack = new TemporaryBuffer.Heap(1024);
- packHeader(pack, 1);
- pack.write((Constants.OBJ_REF_DELTA) << 4 | 4);
- a.copyRawTo(pack);
- deflate(pack, new byte[] { 0x1, 0x1, 0x1, 'b' });
- digest(pack);
+ InMemoryPack pack = new InMemoryPack();
+ pack.header(1);
+ pack.write(Constants.OBJ_REF_DELTA << 4 | 4);
+ pack.copyRaw(a);
+ pack.deflate(new byte[] { 0x1, 0x1, 0x1, 'b' });
+ pack.digest();
InputStream in = new ByteArrayInputStream(pack.toByteArray()) {
@Override
@@ -447,12 +437,12 @@ public class PackParserTest extends RepositoryTestCase {
a = d.blob("a");
}
- TemporaryBuffer.Heap pack = new TemporaryBuffer.Heap(32*1024);
- packHeader(pack, 1);
- pack.write((Constants.OBJ_REF_DELTA) << 4 | 4);
- a.copyRawTo(pack);
- deflate(pack, new byte[] { 0x1, 0x1, 0x1, 'b' });
- digest(pack);
+ InMemoryPack pack = new InMemoryPack();
+ pack.header(1);
+ pack.write(Constants.OBJ_REF_DELTA << 4 | 4);
+ pack.copyRaw(a);
+ pack.deflate(new byte[] { 0x1, 0x1, 0x1, 'b' });
+ pack.digest();
byte packData[] = pack.toByteArray();
byte streamData[] = new byte[packData.length + 1];
@@ -476,14 +466,14 @@ public class PackParserTest extends RepositoryTestCase {
// Build a pack ~17k
int objects = 900;
- TemporaryBuffer.Heap pack = new TemporaryBuffer.Heap(32 * 1024);
- packHeader(pack, objects);
+ InMemoryPack pack = new InMemoryPack(32 * 1024);
+ pack.header(objects);
for (int i = 0; i < objects; i++) {
- pack.write((Constants.OBJ_BLOB) << 4 | 10);
- deflate(pack, data);
+ pack.write(Constants.OBJ_BLOB << 4 | 10);
+ pack.deflate(data);
}
- digest(pack);
+ pack.digest();
byte packData[] = pack.toByteArray();
byte streamData[] = new byte[packData.length + 1];
@@ -510,14 +500,15 @@ public class PackParserTest extends RepositoryTestCase {
}
int objects = 248;
- TemporaryBuffer.Heap pack = new TemporaryBuffer.Heap(32 * 1024);
- packHeader(pack, objects + 1);
+ InMemoryPack pack = new InMemoryPack(32 * 1024);
+ pack.header(objects + 1);
+
int offset = 13;
StringBuilder sb = new StringBuilder();
for (int i = 0; i < offset; i++)
sb.append(i);
offset = sb.toString().length();
- int lenByte = (Constants.OBJ_BLOB) << 4 | (offset & 0x0F);
+ int lenByte = Constants.OBJ_BLOB << 4 | (offset & 0x0F);
offset >>= 4;
if (offset > 0)
lenByte |= 1 << 7;
@@ -529,16 +520,16 @@ public class PackParserTest extends RepositoryTestCase {
lenByte |= 1 << 7;
pack.write(lenByte);
}
- deflate(pack, Constants.encode(sb.toString()));
+ pack.deflate(Constants.encode(sb.toString()));
for (int i = 0; i < objects; i++) {
// The last pack header written falls across the 8192 byte boundary
// between [8189:8210]
- pack.write((Constants.OBJ_REF_DELTA) << 4 | 4);
- b.copyRawTo(pack);
- deflate(pack, new byte[] { 0x1, 0x1, 0x1, 'b' });
+ pack.write(Constants.OBJ_REF_DELTA << 4 | 4);
+ pack.copyRaw(b);
+ pack.deflate(new byte[] { 0x1, 0x1, 0x1, 'b' });
}
- digest(pack);
+ pack.digest();
byte packData[] = pack.toByteArray();
byte streamData[] = new byte[packData.length + 1];
@@ -555,36 +546,6 @@ public class PackParserTest extends RepositoryTestCase {
assertEquals(0x7e, in.read());
}
- private static void packHeader(TemporaryBuffer.Heap tinyPack, int cnt)
- throws IOException {
- final byte[] hdr = new byte[8];
- NB.encodeInt32(hdr, 0, 2);
- NB.encodeInt32(hdr, 4, cnt);
-
- tinyPack.write(Constants.PACK_SIGNATURE);
- tinyPack.write(hdr, 0, 8);
- }
-
- private static void deflate(TemporaryBuffer.Heap tinyPack,
- final byte[] content)
- throws IOException {
- final Deflater deflater = new Deflater();
- final byte[] buf = new byte[128];
- deflater.setInput(content, 0, content.length);
- deflater.finish();
- do {
- final int n = deflater.deflate(buf, 0, buf.length);
- if (n > 0)
- tinyPack.write(buf, 0, n);
- } while (!deflater.finished());
- }
-
- private static void digest(TemporaryBuffer.Heap buf) throws IOException {
- MessageDigest md = Constants.newMessageDigest();
- md.update(buf.toByteArray());
- buf.write(md.digest());
- }
-
private ObjectInserter inserter;
@After
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateIdentTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateIdentTest.java
index f5658abceb..6290b7978e 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateIdentTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateIdentTest.java
@@ -73,7 +73,7 @@ public class PushCertificateIdentTest {
@Test
public void fuzzyCasesMatchPersonIdent() throws Exception {
// See RawParseUtils_ParsePersonIdentTest#testParsePersonIdent_fuzzyCases()
- Date when = new Date(1234567890000l);
+ Date when = new Date(1234567890000L);
TimeZone tz = TimeZone.getTimeZone("GMT-7");
assertMatchesPersonIdent(
@@ -89,7 +89,7 @@ public class PushCertificateIdentTest {
@Test
public void incompleteCasesMatchPersonIdent() throws Exception {
// See RawParseUtils_ParsePersonIdentTest#testParsePersonIdent_incompleteCases()
- Date when = new Date(1234567890000l);
+ Date when = new Date(1234567890000L);
TimeZone tz = TimeZone.getTimeZone("GMT-7");
assertMatchesPersonIdent(
@@ -138,6 +138,51 @@ public class PushCertificateIdentTest {
"Me <me@example.com>");
}
+ @Test
+ public void timezoneRange_hours() {
+ int HOUR_TO_MS = 60 * 60 * 1000;
+
+ // java.util.TimeZone: Hours must be between 0 to 23
+ PushCertificateIdent hourLimit = PushCertificateIdent
+ .parse("A U. Thor <a_u_thor@example.com> 1218123387 +2300");
+ assertEquals(1380, hourLimit.getTimeZoneOffset());
+ assertEquals(23 * HOUR_TO_MS,
+ hourLimit.getTimeZone().getOffset(1218123387));
+
+ PushCertificateIdent hourDubious = PushCertificateIdent
+ .parse("A U. Thor <a_u_thor@example.com> 1218123387 +2400");
+ assertEquals(1440, hourDubious.getTimeZoneOffset());
+ assertEquals(0, hourDubious.getTimeZone().getOffset(1218123387));
+ }
+
+ @Test
+ public void timezoneRange_minutes() {
+ PushCertificateIdent hourLimit = PushCertificateIdent
+ .parse("A U. Thor <a_u_thor@example.com> 1218123387 +0059");
+ assertEquals(59, hourLimit.getTimeZoneOffset());
+ assertEquals(59 * 60 * 1000,
+ hourLimit.getTimeZone().getOffset(1218123387));
+
+ // This becomes one hour and one minute (!)
+ PushCertificateIdent hourDubious = PushCertificateIdent
+ .parse("A U. Thor <a_u_thor@example.com> 1218123387 +0061");
+ assertEquals(61, hourDubious.getTimeZoneOffset());
+ assertEquals(61 * 60 * 1000,
+ hourDubious.getTimeZone().getOffset(1218123387));
+
+ PushCertificateIdent weirdCase = PushCertificateIdent
+ .parse("A U. Thor <a_u_thor@example.com> 1218123387 +0099");
+ assertEquals(99, weirdCase.getTimeZoneOffset());
+ assertEquals(99 * 60 * 1000,
+ weirdCase.getTimeZone().getOffset(1218123387));
+
+ PushCertificateIdent weirdCase2 = PushCertificateIdent
+ .parse("A U. Thor <a_u_thor@example.com> 1218123387 +0199");
+ assertEquals(60 + 99, weirdCase2.getTimeZoneOffset());
+ assertEquals((60 + 99) * 60 * 1000,
+ weirdCase2.getTimeZone().getOffset(1218123387));
+ }
+
private static void assertMatchesPersonIdent(String raw,
PersonIdent expectedPersonIdent, String expectedUserId) {
PushCertificateIdent certIdent = PushCertificateIdent.parse(raw);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateStoreTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateStoreTest.java
index 4f01e4d445..a03222be0c 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateStoreTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateStoreTest.java
@@ -23,6 +23,8 @@ import static org.junit.Assert.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
+import java.time.Instant;
+import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -318,8 +320,8 @@ public class PushCertificateStoreTest {
}
private PersonIdent newIdent() {
- return new PersonIdent(
- "A U. Thor", "author@example.com", ts.getAndIncrement(), 0);
+ return new PersonIdent("A U. Thor", "author@example.com",
+ Instant.ofEpochMilli(ts.getAndIncrement()), ZoneOffset.UTC);
}
private PushCertificateStore newStore() {
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 a91bc95c8d..7dac27f612 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
@@ -187,10 +187,10 @@ public class ReceivePackAdvertiseRefsHookTest extends LocalDiskRepositoryTestCas
TemporaryBuffer.Heap pack = new TemporaryBuffer.Heap(1024);
packHeader(pack, 2);
- pack.write((Constants.OBJ_BLOB) << 4 | 1);
+ pack.write(Constants.OBJ_BLOB << 4 | 1);
deflate(pack, new byte[] { 'a' });
- pack.write((Constants.OBJ_REF_DELTA) << 4 | 4);
+ pack.write(Constants.OBJ_REF_DELTA << 4 | 4);
a.copyRawTo(pack);
deflate(pack, new byte[] { 0x1, 0x1, 0x1, 'b' });
@@ -296,7 +296,7 @@ public class ReceivePackAdvertiseRefsHookTest extends LocalDiskRepositoryTestCas
packHeader(pack, 3);
copy(pack, src.open(N));
copy(pack, src.open(s.parseBody(N).getTree()));
- pack.write((Constants.OBJ_REF_DELTA) << 4 | 4);
+ pack.write(Constants.OBJ_REF_DELTA << 4 | 4);
b.copyRawTo(pack);
deflate(pack, delta);
digest(pack);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RequestValidatorTestCase.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RequestValidatorTestCase.java
index cc910b3b9f..f589a399a4 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RequestValidatorTestCase.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RequestValidatorTestCase.java
@@ -70,20 +70,30 @@ public abstract class RequestValidatorTestCase {
}
/**
+ * Check if the validator accepts a reachable commit
+ *
* @return true if a commit reachable from a visible tip (but not directly
* the tip) is valid
*/
protected abstract boolean isReachableCommitValid();
- /** @return true if a commit not reachable from any tip is valid */
+ /**
+ * Check if the validator accepts an unreachable commit
+ *
+ * @return true if a commit not reachable from any tip is valid
+ **/
protected abstract boolean isUnreachableCommitValid();
/**
+ * Check if the validator accepts a previously advertised tip
+ *
* @return true if the commit directly pointed by an advertised ref is valid
*/
protected abstract boolean isAdvertisedTipValid();
/**
+ * Check if the validator accepts a previous unadvertised tip
+ *
* @return true if the object directly pointed by a non-advertised ref is
* valid
*/
@@ -92,17 +102,23 @@ public abstract class RequestValidatorTestCase {
// UploadPack doesn't allow to ask for blobs when there is no
// bitmap. Test both cases separately.
/**
+ * Check if the validator accepts a reachable blob (repo with bitmaps)
+ *
* @return true if a reachable blob is valid (and the repo has bitmaps)
*/
protected abstract boolean isReachableBlobValid_withBitmaps();
/**
+ * Check if the validator accepts a reachable blob (repo without bitmaps)
+ *
* @return true if a reachable blob is valid (and the repo does NOT have
* bitmaps)
*/
protected abstract boolean isReachableBlobValid_withoutBitmaps();
/**
+ * Check if the validator accepts an unreachable blob
+ *
* @return true if a blob unreachable from any tip is valid
*/
protected abstract boolean isUnreachableBlobValid();
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/SideBandOutputStreamTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/SideBandOutputStreamTest.java
index 3516ed01fc..444e958ae8 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/SideBandOutputStreamTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/SideBandOutputStreamTest.java
@@ -10,7 +10,6 @@
package org.eclipse.jgit.transport;
-import static java.lang.Integer.valueOf;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.eclipse.jgit.transport.SideBandOutputStream.CH_DATA;
import static org.eclipse.jgit.transport.SideBandOutputStream.CH_ERROR;
@@ -224,7 +223,8 @@ public class SideBandOutputStreamTest {
} catch (IllegalArgumentException e) {
assertEquals(MessageFormat.format(
JGitText.get().packetSizeMustBeAtMost,
- valueOf(Integer.MAX_VALUE), valueOf(65520)), e.getMessage());
+ Integer.valueOf(Integer.MAX_VALUE), Integer.valueOf(65520)),
+ e.getMessage());
}
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportHttpTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportHttpTest.java
index 029b45e1e6..96d3a5835a 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportHttpTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportHttpTest.java
@@ -14,10 +14,10 @@ import static org.hamcrest.MatcherAssert.assertThat;
import java.io.File;
import java.io.IOException;
import java.net.HttpCookie;
+import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
-import java.util.Date;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
@@ -101,7 +101,7 @@ public class TransportHttpTest extends SampleDataRepositoryTestCase {
.singletonList("cookie2=some value; Max-Age=1234; Path=/"));
try (TransportHttp transportHttp = new TransportHttp(db, uri)) {
- Date creationDate = new Date();
+ Instant creationDate = Instant.now();
transportHttp.processResponseCookies(connection);
// evaluate written cookie file
@@ -112,8 +112,9 @@ public class TransportHttpTest extends SampleDataRepositoryTestCase {
cookie.setPath("/u/2/");
cookie.setMaxAge(
- (Instant.parse("2100-01-01T11:00:00.000Z").toEpochMilli()
- - creationDate.getTime()) / 1000);
+ Duration.between(creationDate,
+ Instant.parse("2100-01-01T11:00:00.000Z"))
+ .getSeconds());
cookie.setSecure(true);
cookie.setHttpOnly(true);
expectedCookies.add(cookie);
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 d403624b71..67920029d4 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
@@ -82,6 +82,43 @@ public class URIishTest {
}
@Test
+ public void testBrokenFilePath() throws Exception {
+ String str = "D:\\\\my\\\\x";
+ URIish u = new URIish(str);
+ assertNull(u.getScheme());
+ assertFalse(u.isRemote());
+ assertEquals(str, u.getPath());
+ assertEquals(u, new URIish(str));
+ }
+
+ @Test
+ public void testStackOverflow() throws Exception {
+ StringBuilder b = new StringBuilder("D:\\");
+ for (int i = 0; i < 4000; i++) {
+ b.append("x\\");
+ }
+ String str = b.toString();
+ URIish u = new URIish(str);
+ assertNull(u.getScheme());
+ assertFalse(u.isRemote());
+ assertEquals(str, u.getPath());
+ }
+
+ @Test
+ public void testStackOverflow2() throws Exception {
+ StringBuilder b = new StringBuilder("D:\\");
+ for (int i = 0; i < 4000; i++) {
+ b.append("x\\");
+ }
+ b.append('y');
+ String str = b.toString();
+ URIish u = new URIish(str);
+ assertNull(u.getScheme());
+ assertFalse(u.isRemote());
+ assertEquals(str, u.getPath());
+ }
+
+ @Test
public void testRelativePath() throws Exception {
final String str = "../../foo/bar";
URIish u = new URIish(str);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackHandleDeletedPackFileTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackHandleDeletedPackFileTest.java
new file mode 100644
index 0000000000..272c5ea5b5
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackHandleDeletedPackFileTest.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2023, Dariusz Luksza <dariusz.luksza@gmail.com> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.transport;
+
+import static org.eclipse.jgit.lib.Constants.HEAD;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+
+import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.internal.storage.file.GC;
+import org.eclipse.jgit.internal.storage.file.Pack;
+import org.eclipse.jgit.internal.storage.pack.PackExt;
+import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.junit.TestRepository.CommitBuilder;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.UploadPack.RequestPolicy;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class UploadPackHandleDeletedPackFileTest
+ extends LocalDiskRepositoryTestCase {
+
+ private FileRepository server;
+
+ private TestRepository<FileRepository> remote;
+
+ private Repository client;
+
+ private RevCommit head;
+
+ @Parameter
+ public boolean emptyCommit;
+
+ @Parameters(name="empty commit: {0}")
+ public static Collection<Boolean[]> initTestData() {
+ return Arrays.asList(
+ new Boolean[][] { { Boolean.TRUE }, { Boolean.FALSE } });
+ }
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ server = createBareRepository();
+ server.getConfig().setString("protocol", null, "version", "2");
+
+ remote = new TestRepository<>(server);
+ client = new InMemoryRepository(new DfsRepositoryDescription("client"));
+
+ setupServerRepo();
+ head = server.parseCommit(server.resolve(HEAD));
+ }
+
+ @Test
+ public void testV2PackFileRemovedDuringUploadPack() throws Exception {
+ doRemovePackFileDuringUploadPack(PackExt.PACK);
+ }
+
+ @Test
+ public void testV2IdxFileRemovedDuringUploadPack() throws Exception {
+ doRemovePackFileDuringUploadPack(PackExt.INDEX);
+ }
+
+ @Test
+ public void testV2BitmapFileRemovedDuringUploadPack() throws Exception {
+ doRemovePackFileDuringUploadPack(PackExt.BITMAP_INDEX);
+ }
+
+ private void doRemovePackFileDuringUploadPack(PackExt packExt)
+ throws Exception {
+ Object ctx = new Object();
+ TestProtocol<Object> testProtocol = new TestProtocol<>(
+ (Object req, Repository db) -> {
+ UploadPack up = new UploadPack(db);
+ up.setRequestPolicy(RequestPolicy.REACHABLE_COMMIT);
+ Collection<Pack> packs = server.getObjectDatabase()
+ .getPacks();
+ assertEquals("single pack expected", 1, packs.size());
+ Pack pack = packs.iterator().next();
+
+ try {
+ addNewCommit();
+
+ new GC(remote.getRepository()).gc().get();
+
+ pack.getPackFile().create(packExt).delete();
+ } catch (Exception e) {
+ throw new AssertionError(
+ "GC or pack file removal failed", e);
+ }
+
+ return up;
+ }, null);
+
+ URIish uri = testProtocol.register(ctx, server);
+
+ try (Transport tn = testProtocol.open(uri, client, "server")) {
+ tn.fetch(NullProgressMonitor.INSTANCE,
+ Collections.singletonList(new RefSpec(head.name())));
+ assertTrue(client.getObjectDatabase().has(head));
+ }
+ }
+
+ private void addNewCommit() throws Exception {
+ CommitBuilder commit = remote.commit().message("2");
+ if (!emptyCommit) {
+ commit = commit.add("test2.txt", remote.blob("2"));
+ }
+ remote.update("master", commit.parent(head).create());
+ }
+
+ private void setupServerRepo() throws Exception {
+ RevCommit commit0 = remote.commit().message("0")
+ .add("test.txt", remote.blob("0"))
+ .create();
+ remote.update("master", commit0);
+
+ new GC(remote.getRepository()).gc().get(); // create pack files
+
+ head = remote.commit().message("1").parent(commit0)
+ .add("test1.txt", remote.blob("1"))
+ .create();
+ remote.update("master", head);
+ }
+} \ No newline at end of file
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackReachabilityTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackReachabilityTest.java
index 2711762640..a5507c8f81 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackReachabilityTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackReachabilityTest.java
@@ -27,9 +27,6 @@ import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevBlob;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.transport.UploadPack.RequestPolicy;
-import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException;
-import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
-import org.eclipse.jgit.transport.resolver.UploadPackFactory;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -264,15 +261,10 @@ public class UploadPackReachabilityTest {
}
private static TestProtocol<Object> generateReachableCommitUploadPackProtocol() {
- return new TestProtocol<>(new UploadPackFactory<Object>() {
- @Override
- public UploadPack create(Object req, Repository db)
- throws ServiceNotEnabledException,
- ServiceNotAuthorizedException {
- UploadPack up = new UploadPack(db);
- up.setRequestPolicy(RequestPolicy.REACHABLE_COMMIT);
- return up;
- }
+ return new TestProtocol<>((req, db) -> {
+ UploadPack up = new UploadPack(db);
+ up.setRequestPolicy(RequestPolicy.REACHABLE_COMMIT);
+ return up;
}, null);
}
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackTest.java
index 9755ed1b69..5c2f0e5c7d 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackTest.java
@@ -1,5 +1,6 @@
package org.eclipse.jgit.transport;
+import static java.time.ZoneOffset.UTC;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
@@ -11,12 +12,14 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -501,15 +504,15 @@ public class UploadPackTest {
assertThat(hook.capabilitiesRequest, notNullValue());
assertThat(pckIn.readString(), is("version 2"));
assertThat(
- Arrays.asList(pckIn.readString(), pckIn.readString(),
- pckIn.readString()),
+ Arrays.asList(pckIn.readString(),pckIn.readString(),
+ pckIn.readString(), pckIn.readString()),
// TODO(jonathantanmy) This check is written this way
// to make it simple to see that we expect this list of
// capabilities, but probably should be loosened to
// allow additional commands to be added to the list,
// and additional capabilities to be added to existing
// commands without requiring test changes.
- hasItems("ls-refs", "fetch=shallow", "server-option"));
+ hasItems("agent=" + UserAgent.get() ,"ls-refs", "fetch=shallow", "server-option"));
assertTrue(PacketLineIn.isEnd(pckIn.readString()));
}
@@ -535,7 +538,7 @@ public class UploadPackTest {
lines.add(line);
}
}
- assertThat(lines, containsInAnyOrder("ls-refs", "fetch", "server-option"));
+ assertThat(lines, containsInAnyOrder("ls-refs", "fetch", "server-option", "agent=" + UserAgent.get()));
}
private void checkUnadvertisedIfUnallowed(String configSection,
@@ -564,6 +567,47 @@ public class UploadPackTest {
}
@Test
+ public void testV0CapabilitiesAllowAnySha1InWant() throws Exception {
+ checkAvertisedCapabilityProtocolV0IfAllowed("uploadpack",
+ "allowanysha1inwant", "allow-reachable-sha1-in-want",
+ "allow-tip-sha1-in-want");
+ }
+
+ @Test
+ public void testV0CapabilitiesAllowReachableSha1InWant() throws Exception {
+ checkAvertisedCapabilityProtocolV0IfAllowed("uploadpack",
+ "allowreachablesha1inwant", "allow-reachable-sha1-in-want");
+ }
+
+ @Test
+ public void testV0CapabilitiesAllowTipSha1InWant() throws Exception {
+ checkAvertisedCapabilityProtocolV0IfAllowed("uploadpack",
+ "allowtipsha1inwant", "allow-tip-sha1-in-want");
+ }
+
+ private void checkAvertisedCapabilityProtocolV0IfAllowed(
+ String configSection, String configName, String... capabilities)
+ throws Exception {
+ server.getConfig().setBoolean(configSection, null, configName, true);
+ ByteArrayInputStream recvStream = uploadPackSetup(
+ TransferConfig.ProtocolVersion.V0.version(), null,
+ PacketLineIn.end());
+ PacketLineIn pckIn = new PacketLineIn(recvStream);
+
+ String line;
+ while (!PacketLineIn.isEnd((line = pckIn.readString()))) {
+ if (line.contains("capabilities")) {
+ List<String> linesCapabilities = Arrays.asList(line.substring(
+ line.indexOf(" ", line.indexOf("capabilities")) + 1)
+ .split(" "));
+ assertThat(linesCapabilities, hasItems(capabilities));
+ return;
+ }
+ }
+ fail("Server side protocol did not contain any capabilities'");
+ }
+
+ @Test
public void testV2CapabilitiesAllowFilter() throws Exception {
checkAdvertisedIfAllowed("uploadpack", "allowfilter", "filter");
checkUnadvertisedIfUnallowed("uploadpack", "allowfilter", "filter");
@@ -601,9 +645,9 @@ public class UploadPackTest {
assertThat(pckIn.readString(), is("version 2"));
assertThat(
- Arrays.asList(pckIn.readString(), pckIn.readString(),
+ Arrays.asList(pckIn.readString(),pckIn.readString(), pckIn.readString(),
pckIn.readString()),
- hasItems("ls-refs", "fetch=shallow", "server-option"));
+ hasItems("agent="+ UserAgent.get(),"ls-refs", "fetch=shallow", "server-option"));
assertTrue(PacketLineIn.isEnd(pckIn.readString()));
}
@@ -1464,14 +1508,19 @@ public class UploadPackTest {
public void testV2FetchShallowSince() throws Exception {
PersonIdent person = new PersonIdent(remote.getRepository());
- RevCommit beyondBoundary = remote.commit()
- .committer(new PersonIdent(person, 1510000000, 0)).create();
- RevCommit boundary = remote.commit().parent(beyondBoundary)
- .committer(new PersonIdent(person, 1520000000, 0)).create();
- RevCommit tooOld = remote.commit()
- .committer(new PersonIdent(person, 1500000000, 0)).create();
+ RevCommit beyondBoundary = remote.commit().committer(
+ new PersonIdent(person, Instant.ofEpochSecond(1510000), UTC))
+ .create();
+ RevCommit boundary = remote.commit().parent(beyondBoundary).committer(
+ new PersonIdent(person, Instant.ofEpochSecond(1520000), UTC))
+ .create();
+ RevCommit tooOld = remote.commit().committer(
+ new PersonIdent(person, Instant.ofEpochSecond(1500000), UTC))
+ .create();
RevCommit merge = remote.commit().parent(boundary).parent(tooOld)
- .committer(new PersonIdent(person, 1530000000, 0)).create();
+ .committer(new PersonIdent(person,
+ Instant.ofEpochSecond(1530000), UTC))
+ .create();
remote.update("branch1", merge);
@@ -1517,12 +1566,15 @@ public class UploadPackTest {
public void testV2FetchShallowSince_excludedParentWithMultipleChildren() throws Exception {
PersonIdent person = new PersonIdent(remote.getRepository());
- RevCommit base = remote.commit()
- .committer(new PersonIdent(person, 1500000000, 0)).create();
- RevCommit child1 = remote.commit().parent(base)
- .committer(new PersonIdent(person, 1510000000, 0)).create();
- RevCommit child2 = remote.commit().parent(base)
- .committer(new PersonIdent(person, 1520000000, 0)).create();
+ RevCommit base = remote.commit().committer(
+ new PersonIdent(person, Instant.ofEpochSecond(1500000), UTC))
+ .create();
+ RevCommit child1 = remote.commit().parent(base).committer(
+ new PersonIdent(person, Instant.ofEpochSecond(1510000), UTC))
+ .create();
+ RevCommit child2 = remote.commit().parent(base).committer(
+ new PersonIdent(person, Instant.ofEpochSecond(1520000), UTC))
+ .create();
remote.update("branch1", child1);
remote.update("branch2", child2);
@@ -1559,8 +1611,9 @@ public class UploadPackTest {
public void testV2FetchShallowSince_noCommitsSelected() throws Exception {
PersonIdent person = new PersonIdent(remote.getRepository());
- RevCommit tooOld = remote.commit()
- .committer(new PersonIdent(person, 1500000000, 0)).create();
+ RevCommit tooOld = remote.commit().committer(
+ new PersonIdent(person, Instant.ofEpochSecond(1500000), UTC))
+ .create();
remote.update("branch1", tooOld);
@@ -1684,12 +1737,15 @@ public class UploadPackTest {
public void testV2FetchDeepenNot_excludedParentWithMultipleChildren() throws Exception {
PersonIdent person = new PersonIdent(remote.getRepository());
- RevCommit base = remote.commit()
- .committer(new PersonIdent(person, 1500000000, 0)).create();
- RevCommit child1 = remote.commit().parent(base)
- .committer(new PersonIdent(person, 1510000000, 0)).create();
- RevCommit child2 = remote.commit().parent(base)
- .committer(new PersonIdent(person, 1520000000, 0)).create();
+ RevCommit base = remote.commit().committer(
+ new PersonIdent(person, Instant.ofEpochSecond(1500000), UTC))
+ .create();
+ RevCommit child1 = remote.commit().parent(base).committer(
+ new PersonIdent(person, Instant.ofEpochSecond(1510000), UTC))
+ .create();
+ RevCommit child2 = remote.commit().parent(base).committer(
+ new PersonIdent(person, Instant.ofEpochSecond(1520000), UTC))
+ .create();
remote.update("base", base);
remote.update("branch1", child1);
@@ -1800,14 +1856,15 @@ public class UploadPackTest {
RevBlob blobHighDepth = remote.blob("hi");
RevTree subtree = remote.tree(remote.file("1", blobHighDepth));
- RevTree rootTree = (new TreeBuilder() {
+
+ RevTree rootTree = new TreeBuilder() {
@Override
void addElements(DirCacheBuilder dcBuilder) throws Exception {
dcBuilder.add(remote.file("1", blobLowDepth));
dcBuilder.addTree(new byte[] {'2'}, DirCacheEntry.STAGE_0,
remote.getRevWalk().getObjectReader(), subtree);
}
- }).build();
+ }.build();
RevCommit commit = remote.commit(rootTree);
DeepTreePreparator() throws Exception {}
@@ -1904,21 +1961,23 @@ public class UploadPackTest {
class RepeatedSubtreePreparator {
RevBlob foo = remote.blob("foo");
RevTree subtree3 = remote.tree(remote.file("foo", foo));
- RevTree subtree2 = (new TreeBuilder() {
+ RevTree subtree2 = new TreeBuilder() {
@Override
void addElements(DirCacheBuilder dcBuilder) throws Exception {
dcBuilder.addTree(new byte[] {'b'}, DirCacheEntry.STAGE_0,
remote.getRevWalk().getObjectReader(), subtree3);
}
- }).build();
- RevTree subtree1 = (new TreeBuilder() {
+ }.build();
+
+ RevTree subtree1 = new TreeBuilder() {
@Override
void addElements(DirCacheBuilder dcBuilder) throws Exception {
dcBuilder.addTree(new byte[] {'x'}, DirCacheEntry.STAGE_0,
remote.getRevWalk().getObjectReader(), subtree2);
}
- }).build();
- RevTree rootTree = (new TreeBuilder() {
+ }.build();
+
+ RevTree rootTree = new TreeBuilder() {
@Override
void addElements(DirCacheBuilder dcBuilder) throws Exception {
dcBuilder.addTree(new byte[] {'a'}, DirCacheEntry.STAGE_0,
@@ -1926,7 +1985,7 @@ public class UploadPackTest {
dcBuilder.addTree(new byte[] {'x'}, DirCacheEntry.STAGE_0,
remote.getRevWalk().getObjectReader(), subtree2);
}
- }).build();
+ }.build();
RevCommit commit = remote.commit(rootTree);
RepeatedSubtreePreparator() throws Exception {}
@@ -1970,22 +2029,22 @@ public class UploadPackTest {
RevTree subtree1 = remote.tree(remote.file("foo", foo));
/** b/foo */
- RevTree subtree2 = (new TreeBuilder() {
+ RevTree subtree2 = new TreeBuilder() {
@Override
void addElements(DirCacheBuilder dcBuilder) throws Exception {
dcBuilder.addTree(new byte[] {'b'}, DirCacheEntry.STAGE_0,
remote.getRevWalk().getObjectReader(), subtree1);
}
- }).build();
+ }.build();
/** x/b/foo */
- RevTree subtree3 = (new TreeBuilder() {
+ RevTree subtree3 = new TreeBuilder() {
@Override
void addElements(DirCacheBuilder dcBuilder) throws Exception {
dcBuilder.addTree(new byte[] {'x'}, DirCacheEntry.STAGE_0,
remote.getRevWalk().getObjectReader(), subtree2);
}
- }).build();
+ }.build();
RevBlob baz = remote.blob("baz");
@@ -1993,33 +2052,33 @@ public class UploadPackTest {
RevTree subtree4 = remote.tree(remote.file("baz", baz));
/** c/baz */
- RevTree subtree5 = (new TreeBuilder() {
+ RevTree subtree5 = new TreeBuilder() {
@Override
void addElements(DirCacheBuilder dcBuilder) throws Exception {
dcBuilder.addTree(new byte[] {'c'}, DirCacheEntry.STAGE_0,
remote.getRevWalk().getObjectReader(), subtree4);
}
- }).build();
+ }.build();
/** u/c/baz */
- RevTree subtree6 = (new TreeBuilder() {
+ RevTree subtree6 = new TreeBuilder() {
@Override
void addElements(DirCacheBuilder dcBuilder) throws Exception {
dcBuilder.addTree(new byte[] {'u'}, DirCacheEntry.STAGE_0,
remote.getRevWalk().getObjectReader(), subtree5);
}
- }).build();
+ }.build();
/** v/c/baz */
- RevTree subtree7 = (new TreeBuilder() {
+ RevTree subtree7 = new TreeBuilder() {
@Override
void addElements(DirCacheBuilder dcBuilder) throws Exception {
dcBuilder.addTree(new byte[] {'v'}, DirCacheEntry.STAGE_0,
remote.getRevWalk().getObjectReader(), subtree5);
}
- }).build();
+ }.build();
- RevTree rootTree = (new TreeBuilder() {
+ RevTree rootTree = new TreeBuilder() {
@Override
void addElements(DirCacheBuilder dcBuilder) throws Exception {
dcBuilder.addTree(new byte[] {'a'}, DirCacheEntry.STAGE_0,
@@ -2031,7 +2090,7 @@ public class UploadPackTest {
dcBuilder.addTree(new byte[] {'z'}, DirCacheEntry.STAGE_0,
remote.getRevWalk().getObjectReader(), subtree7);
}
- }).build();
+ }.build();
RevCommit commit = remote.commit(rootTree);
RepeatedSubtreeAtSameLevelPreparator() throws Exception {}
@@ -2168,12 +2227,12 @@ public class UploadPackTest {
/**
* <pre>
* remote:
- * foo <- foofoo <-- branchFoo
- * bar <- barbar <-- branchBar
+ * foo &lt;- foofoo &lt;-- branchFoo
+ * bar &lt;- barbar &lt;-- branchBar
*
* client:
- * foo <-- branchFoo
- * bar <-- branchBar
+ * foo &lt;-- branchFoo
+ * bar &lt;-- branchBar
*
* fetch(branchFoo) should send exactly 1 have (i.e. foo) from branchFoo
* </pre>
@@ -2215,7 +2274,8 @@ public class UploadPackTest {
uri = testProtocol.register(ctx, server);
- TestProtocol.setFetchConfig(new FetchConfig(true, MAX_HAVES, true));
+ TestProtocol.setFetchConfig(new FetchConfig(true, MAX_HAVES,
+ /* useNegotiationTip= */true));
try (Transport tn = testProtocol.open(uri,
clientRepo.getRepository(), "server")) {
@@ -2335,7 +2395,8 @@ public class UploadPackTest {
}, null);
uri = testProtocol.register(ctx, server);
- TestProtocol.setFetchConfig(new FetchConfig(true, MAX_HAVES, true));
+ TestProtocol.setFetchConfig(new FetchConfig(true, MAX_HAVES,
+ /* useNegotiationTip= */true));
try (Transport tn = testProtocol.open(uri,
clientRepo.getRepository(), "server")) {
@@ -2362,6 +2423,67 @@ public class UploadPackTest {
}
}
+ /**
+ * <pre>
+ * remote:
+ * foo &lt;- foofoo &lt;-- branchFoo
+ * bar &lt;- barbar &lt;-- branchBar
+ *
+ * client:
+ * none
+ *
+ * fetch(branchFoo) should not send have and should get only branchFoo back
+ * </pre>
+ */
+ @Test
+ public void testNegotiationTipDoesNotDoFullClone() throws Exception {
+ RevCommit fooParent = remote.commit().message("foo").create();
+ RevCommit fooChild = remote.commit().message("foofoo").parent(fooParent)
+ .create();
+ RevCommit barParent = remote.commit().message("bar").create();
+ RevCommit barChild = remote.commit().message("barbar").parent(barParent)
+ .create();
+
+ // Remote has branchFoo at fooChild and branchBar at barChild
+ remote.update("branchFoo", fooChild);
+ remote.update("branchBar", barChild);
+
+ AtomicReference<UploadPack> uploadPack = new AtomicReference<>();
+ CountHavesPreUploadHook countHavesHook = new CountHavesPreUploadHook();
+
+ // Client does not have branchFoo & branchBar
+ try (TestRepository<InMemoryRepository> clientRepo = new TestRepository<>(
+ client)) {
+ testProtocol = new TestProtocol<>((Object req, Repository db) -> {
+ UploadPack up = new UploadPack(db);
+ up.setPreUploadHook(countHavesHook);
+ uploadPack.set(up);
+ return up;
+ }, null);
+
+ uri = testProtocol.register(ctx, server);
+
+ TestProtocol.setFetchConfig(new FetchConfig(true, MAX_HAVES,
+ /* useNegotiationTip= */true));
+ try (Transport tn = testProtocol.open(uri,
+ clientRepo.getRepository(), "server")) {
+
+ tn.fetch(NullProgressMonitor.INSTANCE,
+ Collections.singletonList(
+ new RefSpec("refs/heads/branchFoo")),
+ "branchFoo");
+ }
+ }
+
+ assertTrue(client.getObjectDatabase().has(fooParent.toObjectId()));
+ assertTrue(client.getObjectDatabase().has(fooChild.toObjectId()));
+ assertFalse(client.getObjectDatabase().has(barParent.toObjectId()));
+ assertFalse(client.getObjectDatabase().has(barChild.toObjectId()));
+
+ assertEquals(0, uploadPack.get().getStatistics().getHaves());
+ assertTrue(countHavesHook.havesSentDuringNegotiation.isEmpty());
+ }
+
private static class CountHavesPreUploadHook implements PreUploadHook {
Set<ObjectId> havesSentDuringNegotiation = new HashSet<>();
@@ -2754,7 +2876,7 @@ public class UploadPackTest {
RevTag heavyTag2 = remote.tag("middleTagRing", heavyTag1);
remote.lightweightTag("refTagRing", heavyTag2);
- UploadPack uploadPack = new UploadPack(remote.getRepository());
+ try (UploadPack uploadPack = new UploadPack(remote.getRepository())) {
ByteArrayOutputStream cli = new ByteArrayOutputStream();
PacketLineOut clientWant = new PacketLineOut(cli);
@@ -2764,7 +2886,6 @@ public class UploadPackTest {
clientWant.writeString("done\n");
try (ByteArrayOutputStream serverResponse = new ByteArrayOutputStream()) {
-
uploadPack.setPreUploadHook(new PreUploadHook() {
@Override
public void onBeginNegotiateRound(UploadPack up,
@@ -2817,6 +2938,7 @@ public class UploadPackTest {
assertTrue(objDb.has(heavyTag2.toObjectId()));
}
}
+}
@Test
public void testSingleBranchShallowCloneTagChainWithReflessTag() throws Exception {
@@ -2828,7 +2950,7 @@ public class UploadPackTest {
RevTag tag3 = remote.tag("t3", tag2);
remote.lightweightTag("t3", tag3);
- UploadPack uploadPack = new UploadPack(remote.getRepository());
+ try (UploadPack uploadPack = new UploadPack(remote.getRepository())) {
ByteArrayOutputStream cli = new ByteArrayOutputStream();
PacketLineOut clientWant = new PacketLineOut(cli);
@@ -2838,7 +2960,6 @@ public class UploadPackTest {
clientWant.writeString("done\n");
try (ByteArrayOutputStream serverResponse = new ByteArrayOutputStream()) {
-
uploadPack.setPreUploadHook(new PreUploadHook() {
@Override
public void onBeginNegotiateRound(UploadPack up,
@@ -2886,6 +3007,7 @@ public class UploadPackTest {
assertTrue(objDb.has(one.toObjectId()));
}
}
+}
@Test
public void testSafeToClearRefsInFetchV0() throws Exception {
@@ -2937,7 +3059,70 @@ public class UploadPackTest {
assertThat(pckIn.readString(), is("packfile"));
parsePack(recvStream);
assertTrue(client.getObjectDatabase().has(one.toObjectId()));
- assertEquals(1, ((RefCallsCountingRepository)server).numRefCalls());
+ assertEquals(0, ((RefCallsCountingRepository)server).numRefCalls());
+ }
+
+ /*
+ * Invokes UploadPack with specified protocol version and sends it the given
+ * lines, and returns UploadPack statistics (use uploadPackSetup to get the
+ * output stream)
+ */
+ private PackStatistics uploadPackV2SetupStats(String... inputLines)
+ throws Exception {
+
+ ByteArrayInputStream send = linesAsInputStream(inputLines);
+ String version = TransferConfig.ProtocolVersion.V2.version();
+ server.getConfig().setString(ConfigConstants.CONFIG_PROTOCOL_SECTION,
+ null, ConfigConstants.CONFIG_KEY_VERSION, version);
+ try (UploadPack up = new UploadPack(server)) {
+ up.setExtraParameters(Sets.of("version=".concat(version)));
+
+ ByteArrayOutputStream recv = new ByteArrayOutputStream();
+ up.upload(send, recv, null);
+ return up.getStatistics();
+ }
+ }
+
+ @Test
+ public void testUseWantedRefsAsAdvertisedSetV2_onlyWantedRefs()
+ throws Exception {
+ server = new RefCallsCountingRepository(
+ new DfsRepositoryDescription("server"));
+ remote = new TestRepository<>(server);
+ RevCommit one = remote.commit().message("1").create();
+ RevCommit two = remote.commit().message("2").create();
+ RevCommit three = remote.commit().message("3").create();
+ remote.update("one", one);
+ remote.update("two", two);
+ remote.update("three", three);
+ server.getConfig().setBoolean("uploadpack", null, "allowrefinwant",
+ true);
+ PackStatistics packStats = uploadPackV2SetupStats("command=fetch\n",
+ PacketLineIn.delimiter(), "want-ref refs/heads/one\n",
+ "want-ref refs/heads/two\n", "done\n", PacketLineIn.end());
+ assertEquals("only wanted-refs", 2, packStats.getAdvertised());
+ assertEquals(0, ((RefCallsCountingRepository) server).numRefCalls());
+ }
+
+ @Test
+ public void testUseWantedRefsAsAdvertisedSetV2_withWantId()
+ throws Exception {
+ server = new RefCallsCountingRepository(
+ new DfsRepositoryDescription("server"));
+ remote = new TestRepository<>(server);
+ RevCommit one = remote.commit().message("1").create();
+ RevCommit two = remote.commit().message("2").create();
+ RevCommit three = remote.commit().message("3").create();
+ remote.update("one", one);
+ remote.update("two", two);
+ remote.update("three", three);
+ server.getConfig().setBoolean("uploadpack", null, "allowrefinwant",
+ true);
+ PackStatistics packStats = uploadPackV2SetupStats("command=fetch\n",
+ PacketLineIn.delimiter(), "want-ref refs/heads/one\n",
+ "want " + one.getName() + "\n", "done\n", PacketLineIn.end());
+ assertEquals("all refs", 3, packStats.getAdvertised());
+ assertEquals(1, ((RefCallsCountingRepository) server).numRefCalls());
}
@Test
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/http/JDKHttpConnectionTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/http/JDKHttpConnectionTest.java
index 37f9514494..f71781dcf3 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/http/JDKHttpConnectionTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/http/JDKHttpConnectionTest.java
@@ -16,9 +16,9 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.net.HttpURLConnection;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
-import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@@ -74,7 +74,7 @@ public class JDKHttpConnectionTest {
}
private void assertValues(String key, String... values) {
- List<String> l = new LinkedList<>();
+ List<String> l = new ArrayList<>();
List<String> hf = c.getHeaderFields(key);
if (hf != null) {
l.addAll(hf);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorTest.java
index e463e9070a..7b9e70d4da 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/FileTreeIteratorTest.java
@@ -25,8 +25,9 @@ import java.time.Instant;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.ResetCommand.ResetType;
+import org.eclipse.jgit.dircache.Checkout;
import org.eclipse.jgit.dircache.DirCache;
-import org.eclipse.jgit.dircache.DirCacheCheckout;
+import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata;
import org.eclipse.jgit.dircache.DirCacheEditor;
import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
import org.eclipse.jgit.dircache.DirCacheEntry;
@@ -38,6 +39,7 @@ import org.eclipse.jgit.junit.JGitTestUtil;
import org.eclipse.jgit.junit.RepositoryTestCase;
import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.CoreConfig.EolStreamType;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
@@ -303,11 +305,12 @@ public class FileTreeIteratorTest extends RepositoryTestCase {
DirCacheEntry dce = db.readDirCache().getEntry("symlink");
dce.setFileMode(FileMode.SYMLINK);
try (ObjectReader objectReader = db.newObjectReader()) {
+ Checkout checkout = new Checkout(db).setRecursiveDeletion(false);
+ checkout.checkout(dce,
+ new CheckoutMetadata(EolStreamType.DIRECT, null),
+ objectReader, null);
WorkingTreeOptions options = db.getConfig()
.get(WorkingTreeOptions.KEY);
- DirCacheCheckout.checkoutEntry(db, dce, objectReader, false, null,
- options);
-
FileTreeIterator fti = new FileTreeIterator(trash, db.getFS(),
options);
while (!fti.getEntryPathString().equals("symlink")) {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/ChangedPathTreeFilterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/ChangedPathTreeFilterTest.java
new file mode 100644
index 0000000000..88f6b75c6b
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/ChangedPathTreeFilterTest.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2025, Google LLC
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.treewalk.filter;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.stream.Collectors;
+
+import org.eclipse.jgit.internal.storage.commitgraph.ChangedPathFilter;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.filter.TreeFilter.MutableBoolean;
+import org.junit.Test;
+
+public class ChangedPathTreeFilterTest {
+
+ @Test
+ public void shouldTreeWalk_no_usingCpf() {
+ ChangedPathTreeFilter f = ChangedPathTreeFilter.create("a/b");
+ MutableBoolean cfpUsed = new MutableBoolean();
+
+ boolean result = f.shouldTreeWalk(FakeRevCommit.withCpfFor("c"), null,
+ cfpUsed);
+
+ assertFalse(result);
+ assertTrue(cfpUsed.get());
+ }
+
+ @Test
+ public void shouldTreeWalk_yes_usingCpf() {
+ ChangedPathTreeFilter f = ChangedPathTreeFilter.create("a/b");
+ MutableBoolean cfpUsed = new MutableBoolean();
+
+ boolean result = f.shouldTreeWalk(FakeRevCommit.withCpfFor("a/b"), null,
+ cfpUsed);
+
+ assertTrue(result);
+ assertTrue(cfpUsed.get());
+ }
+
+ @Test
+ public void shouldTreeWalk_yes_noCpf() {
+ ChangedPathTreeFilter f = ChangedPathTreeFilter.create("a/b");
+ MutableBoolean cfpUsed = new MutableBoolean();
+
+ boolean result = f.shouldTreeWalk(FakeRevCommit.noCpf(), null,
+ cfpUsed);
+
+ assertTrue(result);
+ assertFalse(cfpUsed.get());
+ }
+
+ @Test
+ public void shouldTreeWalk_no_usingCpf_noReport() {
+ ChangedPathTreeFilter f = ChangedPathTreeFilter.create("a/b");
+ boolean result = f.shouldTreeWalk(FakeRevCommit.withCpfFor("c"), null,
+ null);
+
+ assertFalse(result);
+ }
+
+ @Test
+ public void shouldTreeWalk_yes_usingCpf_noReport() {
+ ChangedPathTreeFilter f = ChangedPathTreeFilter.create("a/b");
+ boolean result = f.shouldTreeWalk(FakeRevCommit.withCpfFor("a/b"), null,
+ null);
+ assertTrue(result);
+ }
+
+ @Test
+ public void shouldTreeWalk_yes_noCpf_noReport() {
+ ChangedPathTreeFilter f = ChangedPathTreeFilter.create("a/b");
+ boolean result = f.shouldTreeWalk(FakeRevCommit.noCpf(), null,
+ null);
+
+ assertTrue(result);
+ }
+
+ private static class FakeRevCommit extends RevCommit {
+
+ static RevCommit withCpfFor(String... paths) {
+ return new FakeRevCommit(
+ ChangedPathFilter.fromPaths(Arrays.stream(paths)
+ .map(str -> ByteBuffer.wrap(str.getBytes(UTF_8)))
+ .collect(Collectors.toSet())));
+ }
+
+ static RevCommit noCpf() {
+ return new FakeRevCommit(null);
+ }
+
+ private final ChangedPathFilter cpf;
+
+ /**
+ * Create a new commit reference.
+ *
+ * @param cpf
+ * changedPathFilter
+ */
+ protected FakeRevCommit(ChangedPathFilter cpf) {
+ super(ObjectId.zeroId());
+ this.cpf = cpf;
+ }
+
+ @Override
+ public ChangedPathFilter getChangedPathFilter(RevWalk rw) {
+ return cpf;
+ }
+ }
+} \ No newline at end of file
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 32bd40312f..1bb4939c85 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
@@ -11,6 +11,7 @@
package org.eclipse.jgit.treewalk.filter;
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 static org.junit.Assert.fail;
@@ -21,7 +22,9 @@ import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.Set;
+import java.util.stream.Collectors;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheEditor;
@@ -30,6 +33,7 @@ import org.eclipse.jgit.dircache.DirCacheIterator;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.errors.StopWalkException;
+import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Sets;
@@ -143,6 +147,29 @@ public class PathFilterGroupTest {
}
@Test
+ public void testGetPathsBestEffort() {
+ String[] paths = { "path1", "path2", "path3" };
+ Set<byte[]> expected = Arrays.stream(paths).map(Constants::encode)
+ .collect(Collectors.toSet());
+ TreeFilter pathFilterGroup = PathFilterGroup.createFromStrings(paths);
+ Optional<Set<byte[]>> bestEffortPaths = pathFilterGroup
+ .getPathsBestEffort();
+ assertTrue(bestEffortPaths.isPresent());
+ Set<byte[]> actual = bestEffortPaths.get();
+ assertEquals(expected.size(), actual.size());
+ for (byte[] actualPath : actual) {
+ boolean findMatch = false;
+ for (byte[] expectedPath : expected) {
+ if (Arrays.equals(actualPath, expectedPath)) {
+ findMatch = true;
+ break;
+ }
+ }
+ assertTrue(findMatch);
+ }
+ }
+
+ @Test
public void testStopWalk() throws MissingObjectException,
IncorrectObjectTypeException, IOException {
// Obvious
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/PathSuffixFilterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/PathSuffixFilterTest.java
index 0b5a7356c8..d6f4f18f30 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/PathSuffixFilterTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/PathSuffixFilterTest.java
@@ -53,8 +53,8 @@ public class PathSuffixFilterTest extends RepositoryTestCase {
@Test
public void testEdgeCases() throws IOException {
ObjectId treeId = createTree("abc", "abcd", "bcd", "c");
- assertEquals(new ArrayList<String>(), getMatchingPaths("xbcd", treeId));
- assertEquals(new ArrayList<String>(), getMatchingPaths("abcx", treeId));
+ assertEquals(new ArrayList<>(), getMatchingPaths("xbcd", treeId));
+ assertEquals(new ArrayList<>(), getMatchingPaths("abcx", treeId));
assertEquals(Arrays.asList("abcd"), getMatchingPaths("abcd", treeId));
assertEquals(Arrays.asList("abcd", "bcd"), getMatchingPaths("bcd", treeId));
assertEquals(Arrays.asList("abc", "c"), getMatchingPaths("c", treeId));
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/BlockListTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/BlockListTest.java
index a270ca8861..b8b9cbe558 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/BlockListTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/BlockListTest.java
@@ -296,6 +296,7 @@ public class BlockListTest {
public void testAddRejectsBadIndexes() {
BlockList<Integer> list = new BlockList<>(4);
list.add(Integer.valueOf(41));
+ assertEquals(Integer.valueOf(41), list.get(0));
try {
list.add(-1, Integer.valueOf(42));
@@ -316,6 +317,7 @@ public class BlockListTest {
public void testRemoveRejectsBadIndexes() {
BlockList<Integer> list = new BlockList<>(4);
list.add(Integer.valueOf(41));
+ assertEquals(Integer.valueOf(41), list.get(0));
try {
list.remove(-1);
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 32652494d2..44e8632228 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
@@ -12,7 +12,9 @@ package org.eclipse.jgit.util;
import static org.junit.Assert.assertEquals;
-import java.util.concurrent.TimeUnit;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.ZoneId;
import org.eclipse.jgit.junit.MockSystemReader;
import org.eclipse.jgit.lib.ObjectId;
@@ -53,9 +55,9 @@ public class ChangeIdUtilTest {
MockSystemReader mockSystemReader = new MockSystemReader();
- final long when = mockSystemReader.getCurrentTime();
+ Instant when = mockSystemReader.now();
- final int tz = new MockSystemReader().getTimezone(when);
+ ZoneId tz = new MockSystemReader().getTimeZoneAt(when);
PersonIdent author = new PersonIdent("J. Author", "ja@example.com");
{
@@ -218,23 +220,23 @@ public class ChangeIdUtilTest {
@Test
public void testACommitWithSubjectBodyBugBrackersAndSob() throws Exception {
assertEquals(
- "a commit with subject body, bug. brackers and sob\n\nText\n\nBug: 33\nChange-Id: I90ecb589bef766302532c3e00915e10114b00f62\n[bracket]\nSigned-off-by: me@you.too\n",
- call("a commit with subject body, bug. brackers and sob\n\nText\n\nBug: 33\n[bracket]\nSigned-off-by: me@you.too\n\n"));
+ "a commit with subject body, bug, brackers and sob\n\nText\n\nBug: 33\n[bracket]\nChange-Id: I94dc6ed919a4baaa7c1bf8712717b888c6b90363\nSigned-off-by: me@you.too\n",
+ call("a commit with subject body, bug, brackers and sob\n\nText\n\nBug: 33\n[bracket]\nSigned-off-by: me@you.too\n\n"));
}
@Test
public void testACommitWithSubjectBodyBugLineWithASpaceAndSob()
throws Exception {
assertEquals(
- "a commit with subject body, bug. line with a space and sob\n\nText\n\nBug: 33\nChange-Id: I864e2218bdee033c8ce9a7f923af9e0d5dc16863\n \nSigned-off-by: me@you.too\n",
- call("a commit with subject body, bug. line with a space and sob\n\nText\n\nBug: 33\n \nSigned-off-by: me@you.too\n\n"));
+ "a commit with subject body, bug, line with a space and sob\n\nText\n\nBug: 33\n \nChange-Id: I126b472d2e0e64ad8187d61857f0169f9ccdae86\nSigned-off-by: me@you.too\n",
+ call("a commit with subject body, bug, line with a space and sob\n\nText\n\nBug: 33\n \nSigned-off-by: me@you.too\n\n"));
}
@Test
public void testACommitWithSubjectBodyBugEmptyLineAndSob() throws Exception {
assertEquals(
- "a commit with subject body, bug. empty line and sob\n\nText\n\nBug: 33\nChange-Id: I33f119f533313883e6ada3df600c4f0d4db23a76\n \nSigned-off-by: me@you.too\n",
- call("a commit with subject body, bug. empty line and sob\n\nText\n\nBug: 33\n \nSigned-off-by: me@you.too\n\n"));
+ "a commit with subject body, bug, empty line and sob\n\nText\n\nBug: 33\n\nChange-Id: Ic3b61b6e39a0815669b65302e9e75e6a5a019a26\nSigned-off-by: me@you.too\n",
+ call("a commit with subject body, bug, empty line and sob\n\nText\n\nBug: 33\n\nSigned-off-by: me@you.too\n\n"));
}
@Test
@@ -342,9 +344,7 @@ public class ChangeIdUtilTest {
/** Increment the {@link #author} and {@link #committer} times. */
protected void tick() {
- final long delta = TimeUnit.MILLISECONDS.convert(5 * 60,
- TimeUnit.SECONDS);
- final long now = author.getWhen().getTime() + delta;
+ Instant now = author.getWhenAsInstant().plus(Duration.ofMinutes(5));
author = new PersonIdent(author, now, tz);
committer = new PersonIdent(committer, now, tz);
@@ -528,7 +528,7 @@ public class ChangeIdUtilTest {
}
@Test
- public void testChangeIdAfterBugOrIssue() throws Exception {
+ public void testChangeIdAfterOtherFooters() throws Exception {
assertEquals("a\n" + //
"\n" + //
"Bug: 42\n" + //
@@ -541,6 +541,18 @@ public class ChangeIdUtilTest {
assertEquals("a\n" + //
"\n" + //
+ "Bug: 42\n" + //
+ " multi-line Bug footer\n" + //
+ "Change-Id: Icc953ef35f1a4ee5eb945132aefd603ae3d9dd9f\n" + //
+ SOB1,//
+ call("a\n" + //
+ "\n" + //
+ "Bug: 42\n" + //
+ " multi-line Bug footer\n" + //
+ SOB1));
+
+ assertEquals("a\n" + //
+ "\n" + //
"Issue: 42\n" + //
"Change-Id: Ie66e07d89ae5b114c0975b49cf326e90331dd822\n" + //
SOB1,//
@@ -548,6 +560,14 @@ public class ChangeIdUtilTest {
"\n" + //
"Issue: 42\n" + //
SOB1));
+
+ assertEquals("a\n" + //
+ "\n" + //
+ "Other: none\n" + //
+ "Change-Id: Ide70e625dea61854206378a377dd12e462ae720f\n",//
+ call("a\n" + //
+ "\n" + //
+ "Other: none\n"));
}
@Test
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FSTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FSTest.java
index 171d80c3da..3a7fa2388e 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FSTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FSTest.java
@@ -13,6 +13,7 @@ package org.eclipse.jgit.util;
import static java.time.Instant.EPOCH;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeNoException;
import static org.junit.Assume.assumeTrue;
@@ -28,8 +29,12 @@ import java.nio.file.attribute.PosixFilePermission;
import java.time.Duration;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Locale;
import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.errors.CommandFailedException;
@@ -195,6 +200,23 @@ public class FSTest {
}
@Test
+ @SuppressWarnings("boxing")
+ public void testConcurrentSymlinkSupport()
+ throws ExecutionException, InterruptedException {
+ Assume.assumeTrue(FS.DETECTED.supportsSymlinks());
+ int n = 3;
+ List<CompletableFuture<Boolean>> futures = new ArrayList<>();
+ for (int i = 0; i < n; i++) {
+ futures.add(CompletableFuture.supplyAsync(
+ () -> FS.DETECTED.newInstance().supportsSymlinks()));
+ }
+
+ for (int i = 0; i < n; i++) {
+ assertTrue(futures.get(i).get());
+ }
+ }
+
+ @Test
public void testFsTimestampResolution() throws Exception {
DateTimeFormatter formatter = DateTimeFormatter
.ofPattern("uuuu-MMM-dd HH:mm:ss.nnnnnnnnn", Locale.ENGLISH)
@@ -233,4 +255,26 @@ public class FSTest {
assertFalse(RepositoryCache.FileKey
.isGitRepository(new File("repo.git"), FS.DETECTED));
}
+
+ @Test
+ public void testSearchPath() throws IOException {
+ File f1 = new File(trash, "file1");
+ FileUtils.createNewFile(f1);
+ f1.setExecutable(true);
+ File f2 = new File(trash, "file2");
+ FileUtils.createNewFile(f2);
+ assertEquals(f1, FS.searchPath(trash.getAbsolutePath(), "file1"));
+ assertNull(FS.searchPath(trash.getAbsolutePath(), "file2"));
+ }
+
+ @Test
+ public void testSearchPathEmptyPath() {
+ assertNull(FS.searchPath("", "file1"));
+ assertNull(FS.searchPath(File.pathSeparator, "file1"));
+ assertNull(FS.searchPath(File.pathSeparator + File.pathSeparator,
+ "file1"));
+ assertNull(FS.searchPath(
+ " " + File.pathSeparator + " " + File.pathSeparator + " \t",
+ "file1"));
+ }
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FileUtilsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FileUtilsTest.java
index 2b1fb2ef04..5106540227 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FileUtilsTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FileUtilsTest.java
@@ -74,7 +74,9 @@ public class FileUtilsTest {
try {
FileUtils.delete(f, FileUtils.SKIP_MISSING);
} catch (IOException e) {
- fail("deletion of non-existing file must not fail with option SKIP_MISSING");
+ throw new AssertionError(
+ "deletion of non-existing file must not fail with option SKIP_MISSING",
+ e);
}
}
@@ -108,7 +110,9 @@ public class FileUtilsTest {
try {
FileUtils.delete(d, FileUtils.RECURSIVE | FileUtils.SKIP_MISSING);
} catch (IOException e) {
- fail("recursive deletion of non-existing directory must not fail with option SKIP_MISSING");
+ throw new AssertionError(
+ "recursive deletion of non-existing directory must not fail with option SKIP_MISSING",
+ e);
}
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitDateFormatterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitDateFormatterTest.java
index 0bd7e0bd61..7ef386f6ee 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitDateFormatterTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitDateFormatterTest.java
@@ -90,7 +90,8 @@ public class GitDateFormatterTest {
public void LOCALE() {
String date = new GitDateFormatter(Format.LOCALE).formatDate(ident);
assertTrue("Sep 20, 2011 7:09:25 PM -0400".equals(date)
- || "Sep 20, 2011, 7:09:25 PM -0400".equals(date)); // JDK-8206961
+ || "Sep 20, 2011, 7:09:25 PM -0400".equals(date) // JDK-8206961
+ || "Sep 20, 2011, 7:09:25\u202FPM -0400".equals(date)); // JDK-8304925
}
@Test
@@ -98,6 +99,7 @@ public class GitDateFormatterTest {
String date = new GitDateFormatter(Format.LOCALELOCAL)
.formatDate(ident);
assertTrue("Sep 20, 2011 7:39:25 PM".equals(date)
- || "Sep 20, 2011, 7:39:25 PM".equals(date)); // JDK-8206961
+ || "Sep 20, 2011, 7:39:25 PM".equals(date) // JDK-8206961
+ || "Sep 20, 2011, 7:39:25\u202FPM".equals(date)); // JDK-8304925
}
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitTimeParserBadlyFormattedTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitTimeParserBadlyFormattedTest.java
new file mode 100644
index 0000000000..a59d7bc7bb
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitTimeParserBadlyFormattedTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2012, Christian Halstrick and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.util;
+
+import static org.junit.Assert.assertThrows;
+
+import java.text.ParseException;
+
+import org.eclipse.jgit.junit.MockSystemReader;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests which assert that unparseable Strings lead to ParseExceptions
+ */
+@RunWith(Theories.class)
+public class GitTimeParserBadlyFormattedTest {
+ private String dateStr;
+
+ @Before
+ public void setUp() {
+ MockSystemReader mockSystemReader = new MockSystemReader();
+ SystemReader.setInstance(mockSystemReader);
+ }
+
+ @After
+ public void tearDown() {
+ SystemReader.setInstance(null);
+ }
+
+ public GitTimeParserBadlyFormattedTest(String dateStr) {
+ this.dateStr = dateStr;
+ }
+
+ @DataPoints
+ public static String[] getDataPoints() {
+ return new String[] { "", ".", "...", "1970", "3000.3000.3000", "3 yesterday ago",
+ "now yesterday ago", "yesterdays", "3.day. 2.week.ago",
+ "day ago", "Gra Feb 21 15:35:00 2007 +0100",
+ "Sun Feb 21 15:35:00 2007 +0100",
+ "Wed Feb 21 15:35:00 Grand +0100" };
+ }
+
+ @Theory
+ public void badlyFormattedWithoutRef() {
+ assertThrows(
+ "The expected ParseException while parsing '" + dateStr
+ + "' did not occur.",
+ ParseException.class, () -> GitTimeParser.parse(dateStr));
+ }
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitTimeParserTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitTimeParserTest.java
new file mode 100644
index 0000000000..0e5eb283a4
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitTimeParserTest.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2024, Christian Halstrick and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.text.ParseException;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.Period;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoField;
+import java.time.temporal.TemporalAccessor;
+
+import org.eclipse.jgit.junit.MockSystemReader;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class GitTimeParserTest {
+ MockSystemReader mockSystemReader;
+
+ @Before
+ public void setUp() {
+ mockSystemReader = new MockSystemReader();
+ SystemReader.setInstance(mockSystemReader);
+ }
+
+ @After
+ public void tearDown() {
+ SystemReader.setInstance(null);
+ }
+
+ @Test
+ public void yesterday() throws ParseException {
+ LocalDateTime parse = GitTimeParser.parse("yesterday");
+
+ LocalDateTime now = SystemReader.getInstance().civilNow();
+ assertEquals(Period.between(parse.toLocalDate(), now.toLocalDate()),
+ Period.ofDays(1));
+ }
+
+ @Test
+ public void never() throws ParseException {
+ LocalDateTime parse = GitTimeParser.parse("never");
+ assertEquals(LocalDateTime.MAX, parse);
+ }
+
+ @Test
+ public void now_pointInTime() throws ParseException {
+ LocalDateTime aTime = asLocalDateTime("2007-02-21 15:35:00 +0100");
+
+ LocalDateTime parsedNow = GitTimeParser.parse("now", aTime);
+
+ assertEquals(aTime, parsedNow);
+ }
+
+ @Test
+ public void now_systemTime() throws ParseException {
+ LocalDateTime firstNow = GitTimeParser.parse("now");
+ assertEquals(SystemReader.getInstance().civilNow(), firstNow);
+ mockSystemReader.tick(10);
+ LocalDateTime secondNow = GitTimeParser.parse("now");
+ assertTrue(secondNow.isAfter(firstNow));
+ }
+
+ @Test
+ public void weeksAgo() throws ParseException {
+ LocalDateTime aTime = asLocalDateTime("2007-02-21 15:35:00 +0100");
+
+ LocalDateTime parse = GitTimeParser.parse("2 weeks ago", aTime);
+ assertEquals(asLocalDateTime("2007-02-07 15:35:00 +0100"), parse);
+ }
+
+ @Test
+ public void daysAndWeeksAgo() throws ParseException {
+ LocalDateTime aTime = asLocalDateTime("2007-02-21 15:35:00 +0100");
+
+ LocalDateTime twoWeeksAgoActual = GitTimeParser.parse("2 weeks ago",
+ aTime);
+
+ LocalDateTime twoWeeksAgoExpected = asLocalDateTime(
+ "2007-02-07 15:35:00 +0100");
+ assertEquals(twoWeeksAgoExpected, twoWeeksAgoActual);
+
+ LocalDateTime combinedWhitespace = GitTimeParser
+ .parse("3 days 2 weeks ago", aTime);
+ LocalDateTime combinedWhitespaceExpected = asLocalDateTime(
+ "2007-02-04 15:35:00 +0100");
+ assertEquals(combinedWhitespaceExpected, combinedWhitespace);
+
+ LocalDateTime combinedDots = GitTimeParser.parse("3.day.2.week.ago",
+ aTime);
+ LocalDateTime combinedDotsExpected = asLocalDateTime(
+ "2007-02-04 15:35:00 +0100");
+ assertEquals(combinedDotsExpected, combinedDots);
+ }
+
+ @Test
+ public void hoursAgo() throws ParseException {
+ LocalDateTime aTime = asLocalDateTime("2007-02-21 17:35:00 +0100");
+
+ LocalDateTime twoHoursAgoActual = GitTimeParser.parse("2 hours ago",
+ aTime);
+
+ LocalDateTime twoHoursAgoExpected = asLocalDateTime(
+ "2007-02-21 15:35:00 +0100");
+ assertEquals(twoHoursAgoExpected, twoHoursAgoActual);
+ }
+
+ @Test
+ public void hoursAgo_acrossDay() throws ParseException {
+ LocalDateTime aTime = asLocalDateTime("2007-02-21 00:35:00 +0100");
+
+ LocalDateTime twoHoursAgoActual = GitTimeParser.parse("2 hours ago",
+ aTime);
+
+ LocalDateTime twoHoursAgoExpected = asLocalDateTime(
+ "2007-02-20 22:35:00 +0100");
+ assertEquals(twoHoursAgoExpected, twoHoursAgoActual);
+ }
+
+ @Test
+ public void minutesHoursAgoCombined() throws ParseException {
+ LocalDateTime aTime = asLocalDateTime("2007-02-04 15:35:00 +0100");
+
+ LocalDateTime combinedWhitespace = GitTimeParser
+ .parse("3 hours 2 minutes ago", aTime);
+ LocalDateTime combinedWhitespaceExpected = asLocalDateTime(
+ "2007-02-04 12:33:00 +0100");
+ assertEquals(combinedWhitespaceExpected, combinedWhitespace);
+
+ LocalDateTime combinedDots = GitTimeParser
+ .parse("3.hours.2.minutes.ago", aTime);
+ LocalDateTime combinedDotsExpected = asLocalDateTime(
+ "2007-02-04 12:33:00 +0100");
+ assertEquals(combinedDotsExpected, combinedDots);
+ }
+
+ @Test
+ public void minutesAgo() throws ParseException {
+ LocalDateTime aTime = asLocalDateTime("2007-02-21 17:35:10 +0100");
+
+ LocalDateTime twoMinutesAgo = GitTimeParser.parse("2 minutes ago",
+ aTime);
+
+ LocalDateTime twoMinutesAgoExpected = asLocalDateTime(
+ "2007-02-21 17:33:10 +0100");
+ assertEquals(twoMinutesAgoExpected, twoMinutesAgo);
+ }
+
+ @Test
+ public void minutesAgo_acrossDay() throws ParseException {
+ LocalDateTime aTime = asLocalDateTime("2007-02-21 00:35:10 +0100");
+
+ LocalDateTime minutesAgoActual = GitTimeParser.parse("40 minutes ago",
+ aTime);
+
+ LocalDateTime minutesAgoExpected = asLocalDateTime(
+ "2007-02-20 23:55:10 +0100");
+ assertEquals(minutesAgoExpected, minutesAgoActual);
+ }
+
+ @Test
+ public void iso() throws ParseException {
+ String dateStr = "2007-02-21 15:35:00 +0100";
+
+ LocalDateTime actual = GitTimeParser.parse(dateStr);
+
+ LocalDateTime expected = asLocalDateTime(dateStr);
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void rfc() throws ParseException {
+ String dateStr = "Wed, 21 Feb 2007 15:35:00 +0100";
+
+ LocalDateTime actual = GitTimeParser.parse(dateStr);
+
+ LocalDateTime expected = asLocalDateTime(dateStr,
+ "EEE, dd MMM yyyy HH:mm:ss Z");
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void shortFmt() throws ParseException {
+ assertParsing("2007-02-21", "yyyy-MM-dd");
+ }
+
+ @Test
+ public void shortWithDots() throws ParseException {
+ assertParsing("2007.02.21", "yyyy.MM.dd");
+ }
+
+ @Test
+ public void shortWithSlash() throws ParseException {
+ assertParsing("02/21/2007", "MM/dd/yyyy");
+ }
+
+ @Test
+ public void shortWithDotsReverse() throws ParseException {
+ assertParsing("21.02.2007", "dd.MM.yyyy");
+ }
+
+ @Test
+ public void defaultFmt() throws ParseException {
+ assertParsing("Wed Feb 21 15:35:00 2007 +0100",
+ "EEE MMM dd HH:mm:ss yyyy Z");
+ }
+
+ @Test
+ public void local() throws ParseException {
+ assertParsing("Wed Feb 21 15:35:00 2007", "EEE MMM dd HH:mm:ss yyyy");
+ }
+
+ private static void assertParsing(String dateStr, String format)
+ throws ParseException {
+ LocalDateTime actual = GitTimeParser.parse(dateStr);
+
+ LocalDateTime expected = asLocalDateTime(dateStr, format);
+ assertEquals(expected, actual);
+ }
+
+ private static LocalDateTime asLocalDateTime(String dateStr) {
+ return asLocalDateTime(dateStr, "yyyy-MM-dd HH:mm:ss Z");
+ }
+
+ private static LocalDateTime asLocalDateTime(String dateStr,
+ String pattern) {
+ DateTimeFormatter fmt = DateTimeFormatter.ofPattern(pattern);
+ TemporalAccessor ta = fmt
+ .withZone(SystemReader.getInstance().getTimeZoneId())
+ .withLocale(SystemReader.getInstance().getLocale())
+ .parse(dateStr);
+ return ta.isSupported(ChronoField.HOUR_OF_DAY) ? LocalDateTime.from(ta)
+ : LocalDate.from(ta).atStartOfDay();
+ }
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/HookTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/HookTest.java
index 1231aefee0..b7490f0b1f 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/HookTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/HookTest.java
@@ -157,7 +157,7 @@ public class HookTest extends RepositoryTestCase {
git.commit().setMessage("commit")
.setHookOutputStream(new PrintStream(out)).call();
} catch (AbortedByHookException e) {
- fail("unexpected hook failure");
+ throw new AssertionError("unexpected hook failure", e);
}
assertEquals("unexpected hook output",
"test pre-commit\ntest commit-msg .git/COMMIT_EDITMSG\ntest post-commit\n",
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/HttpSupportTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/HttpSupportTest.java
index cbe4eb2eb0..a3a5697ef4 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/HttpSupportTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/HttpSupportTest.java
@@ -47,18 +47,6 @@ public class HttpSupportTest {
}
@Test
- public void testMalformedUri() throws Exception {
- // Valid URL, but backslash is not allowed in a URI in the userinfo part
- // per RFC 3986: https://tools.ietf.org/html/rfc3986#section-3.2.1 .
- // Test that conversion to URI to call the ProxySelector does not throw
- // an exception.
- Proxy proxy = HttpSupport.proxyFor(new TestProxySelector(), new URL(
- "http://infor\\c.jones@somehost/somewhere/someproject.git"));
- assertNotNull(proxy);
- assertEquals(Proxy.Type.HTTP, proxy.type());
- }
-
- @Test
public void testCorrectUri() throws Exception {
// Backslash escaped as %5C is correct.
Proxy proxy = HttpSupport.proxyFor(new TestProxySelector(), new URL(
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RawParseUtilsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RawParseUtilsTest.java
index e80c07509a..e4ef302359 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RawParseUtilsTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RawParseUtilsTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2011, Leonard Broman <leonard.broman@gmail.com> and others
+ * Copyright (C) 2011, 2024 Leonard Broman <leonard.broman@gmail.com> and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -15,6 +15,7 @@ import static org.junit.Assert.fail;
import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
+import java.util.List;
import org.eclipse.jgit.lib.Constants;
import org.junit.Test;
@@ -24,7 +25,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
public class RawParseUtilsTest {
String commit = "tree e3a1035abd2b319bb01e57d69b0ba6cab289297e\n" +
"parent 54e895b87c0768d2317a2b17062e3ad9f76a8105\n" +
- "committer A U Thor <author@xample.com 1528968566 +0200\n" +
+ "committer A U Thor <author@example.com> 1528968566 +0200\n" +
"gpgsig -----BEGIN PGP SIGNATURE-----\n" +
" \n" +
" wsBcBAABCAAQBQJbGB4pCRBK7hj4Ov3rIwAAdHIIAENrvz23867ZgqrmyPemBEZP\n" +
@@ -67,21 +68,24 @@ public class RawParseUtilsTest {
public void testHeaderStart() {
byte[] headerName = "some".getBytes(UTF_8);
byte[] commitBytes = commit.getBytes(UTF_8);
- assertEquals(625, RawParseUtils.headerStart(headerName, commitBytes, 0));
- assertEquals(625, RawParseUtils.headerStart(headerName, commitBytes, 4));
+ assertEquals(627,
+ RawParseUtils.headerStart(headerName, commitBytes, 0));
+ assertEquals(627,
+ RawParseUtils.headerStart(headerName, commitBytes, 4));
byte[] missingHeaderName = "missing".getBytes(UTF_8);
assertEquals(-1, RawParseUtils.headerStart(missingHeaderName,
commitBytes, 0));
byte[] fauxHeaderName = "other".getBytes(UTF_8);
- assertEquals(-1, RawParseUtils.headerStart(fauxHeaderName, commitBytes, 625 + 4));
+ assertEquals(-1, RawParseUtils.headerStart(fauxHeaderName, commitBytes,
+ 627 + 4));
}
@Test
public void testHeaderEnd() {
byte[] commitBytes = commit.getBytes(UTF_8);
- int[] expected = new int[] {45, 93, 148, 619, 637};
+ int[] expected = new int[] { 45, 93, 150, 621, 639 };
int start = 0;
for (int i = 0; i < expected.length; i++) {
start = RawParseUtils.headerEnd(commitBytes, start);
@@ -89,4 +93,44 @@ public class RawParseUtilsTest {
start += 1;
}
}
+
+ @Test
+ public void testHeaderValue() {
+ byte[] commitBytes = commit.getBytes(UTF_8);
+ List<String> headers = List.of(
+ "e3a1035abd2b319bb01e57d69b0ba6cab289297e",
+ "54e895b87c0768d2317a2b17062e3ad9f76a8105",
+ "A U Thor <author@example.com> 1528968566 +0200",
+ "-----BEGIN PGP SIGNATURE-----\n"
+ + "\n"
+ + "wsBcBAABCAAQBQJbGB4pCRBK7hj4Ov3rIwAAdHIIAENrvz23867ZgqrmyPemBEZP\n"
+ + "U24B1Tlq/DWvce2buaxmbNQngKZ0pv2s8VMc11916WfTIC9EKvioatmpjduWvhqj\n"
+ + "znQTFyiMor30pyYsfrqFuQZvqBW01o8GEWqLg8zjf9Rf0R3LlOEw86aT8CdHRlm6\n"
+ + "wlb22xb8qoX4RB+LYfz7MhK5F+yLOPXZdJnAVbuyoMGRnDpwdzjL5Hj671+XJxN5\n"
+ + "SasRdhxkkfw/ZnHxaKEc4juMz8Nziz27elRwhOQqlTYoXNJnsV//wy5Losd7aKi1\n"
+ + "xXXyUpndEOmT0CIcKHrN/kbYoVL28OJaxoBuva3WYQaRrzEe3X02NMxZe9gkSqA=\n"
+ + "=TClh\n"
+ + "-----END PGP SIGNATURE-----",
+ "other header");
+ int start = 0;
+ for (String header : headers) {
+ int endOfTag = RawParseUtils.next(commitBytes, start, ' ');
+ int end = RawParseUtils.headerEnd(commitBytes, start);
+
+ assertEquals(header, new String(
+ RawParseUtils.headerValue(commitBytes, endOfTag, end),
+ UTF_8));
+ start = end + 1;
+ }
+ }
+
+ @Test
+ public void testLastHeaderEnd() {
+ byte[] raw = "headerA A header\nheaderB Another header".getBytes(UTF_8);
+ int bStart = RawParseUtils.headerStart("headerB".getBytes(UTF_8), raw,
+ 0);
+ assertEquals(25, bStart);
+ int bEnd = RawParseUtils.nextLfSkippingSplitLines(raw, bStart);
+ assertEquals(raw.length, bEnd);
+ }
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RawParseUtils_ParsePersonIdentTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RawParseUtils_ParsePersonIdentTest.java
index ee3ce8d98c..6d23db81d8 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RawParseUtils_ParsePersonIdentTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RawParseUtils_ParsePersonIdentTest.java
@@ -10,10 +10,13 @@
package org.eclipse.jgit.util;
+import static java.time.Instant.EPOCH;
+import static java.time.ZoneOffset.UTC;
import static org.junit.Assert.assertEquals;
-import java.util.Date;
-import java.util.TimeZone;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
import org.eclipse.jgit.lib.PersonIdent;
import org.junit.Test;
@@ -22,8 +25,8 @@ public class RawParseUtils_ParsePersonIdentTest {
@Test
public void testParsePersonIdent_legalCases() {
- final Date when = new Date(1234567890000l);
- final TimeZone tz = TimeZone.getTimeZone("GMT-7");
+ Instant when = Instant.ofEpochMilli(1234567890000L);
+ ZoneId tz = ZoneOffset.ofHours(-7);
assertPersonIdent("Me <me@example.com> 1234567890 -0700",
new PersonIdent("Me", "me@example.com", when, tz));
@@ -50,8 +53,8 @@ public class RawParseUtils_ParsePersonIdentTest {
@Test
public void testParsePersonIdent_fuzzyCases() {
- final Date when = new Date(1234567890000l);
- final TimeZone tz = TimeZone.getTimeZone("GMT-7");
+ Instant when = Instant.ofEpochMilli(1234567890000L);
+ ZoneId tz = ZoneOffset.ofHours(-7);
assertPersonIdent(
"A U Thor <author@example.com>, C O. Miter <comiter@example.com> 1234567890 -0700",
@@ -64,8 +67,8 @@ public class RawParseUtils_ParsePersonIdentTest {
@Test
public void testParsePersonIdent_incompleteCases() {
- final Date when = new Date(1234567890000l);
- final TimeZone tz = TimeZone.getTimeZone("GMT-7");
+ Instant when = Instant.ofEpochMilli(1234567890000L);
+ ZoneId tz = ZoneOffset.ofHours(-7);
assertPersonIdent("Me <> 1234567890 -0700", new PersonIdent("Me", "",
when, tz));
@@ -76,26 +79,26 @@ public class RawParseUtils_ParsePersonIdentTest {
assertPersonIdent(" <> 1234567890 -0700", new PersonIdent("", "", when,
tz));
- assertPersonIdent("<>", new PersonIdent("", "", 0, 0));
+ assertPersonIdent("<>", new PersonIdent("", "", EPOCH, UTC));
- assertPersonIdent(" <>", new PersonIdent("", "", 0, 0));
+ assertPersonIdent(" <>", new PersonIdent("", "", EPOCH, UTC));
assertPersonIdent("<me@example.com>", new PersonIdent("",
- "me@example.com", 0, 0));
+ "me@example.com", EPOCH, UTC));
assertPersonIdent(" <me@example.com>", new PersonIdent("",
- "me@example.com", 0, 0));
+ "me@example.com", EPOCH, UTC));
- assertPersonIdent("Me <>", new PersonIdent("Me", "", 0, 0));
+ assertPersonIdent("Me <>", new PersonIdent("Me", "", EPOCH, UTC));
assertPersonIdent("Me <me@example.com>", new PersonIdent("Me",
- "me@example.com", 0, 0));
+ "me@example.com", EPOCH, UTC));
assertPersonIdent("Me <me@example.com> 1234567890", new PersonIdent(
- "Me", "me@example.com", 0, 0));
+ "Me", "me@example.com", EPOCH, UTC));
assertPersonIdent("Me <me@example.com> 1234567890 ", new PersonIdent(
- "Me", "me@example.com", 0, 0));
+ "Me", "me@example.com", EPOCH, UTC));
}
@Test
@@ -104,6 +107,21 @@ public class RawParseUtils_ParsePersonIdentTest {
assertPersonIdent("Me <me@example.com 1234567890 -0700", null);
}
+ @Test
+ public void testParsePersonIdent_badTz() {
+ PersonIdent tooBig = RawParseUtils
+ .parsePersonIdent("Me <me@example.com> 1234567890 +8315");
+ assertEquals(tooBig.getZoneOffset().getTotalSeconds(), 0);
+
+ PersonIdent tooSmall = RawParseUtils
+ .parsePersonIdent("Me <me@example.com> 1234567890 -8315");
+ assertEquals(tooSmall.getZoneOffset().getTotalSeconds(), 0);
+
+ PersonIdent notATime = RawParseUtils
+ .parsePersonIdent("Me <me@example.com> 1234567890 -0370");
+ assertEquals(notATime.getZoneOffset().getTotalSeconds(), 0);
+ }
+
private static void assertPersonIdent(String line, PersonIdent expected) {
PersonIdent actual = RawParseUtils.parsePersonIdent(line);
assertEquals(expected, actual);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RefMapTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RefMapTest.java
index 627417d462..0c9cb2d2b0 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RefMapTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RefMapTest.java
@@ -268,6 +268,7 @@ public class RefMapTest {
assertFalse(itr.hasNext());
}
+ @SuppressWarnings("ModifiedButNotUsed")
@Test
public void testPut_KeyMustMatchName_NoPrefix() {
final Ref refA = newRef("refs/heads/A", ID_ONE);
@@ -280,6 +281,7 @@ public class RefMapTest {
}
}
+ @SuppressWarnings("ModifiedButNotUsed")
@Test
public void testPut_KeyMustMatchName_WithPrefix() {
final Ref refA = newRef("refs/heads/A", ID_ONE);
@@ -421,7 +423,7 @@ public class RefMapTest {
Map.Entry<String, Ref> ent_b = itr.next();
assertEquals(ent_a.hashCode(), "A".hashCode());
- assertEquals(ent_a, ent_a);
+ assertTrue(ent_a.equals(ent_a));
assertFalse(ent_a.equals(ent_b));
assertEquals(a.toString(), ent_a.toString());
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RelativeDateFormatterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RelativeDateFormatterTest.java
index 214bbca944..a927d8dbef 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RelativeDateFormatterTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RelativeDateFormatterTest.java
@@ -16,7 +16,7 @@ import static org.eclipse.jgit.util.RelativeDateFormatter.SECOND_IN_MILLIS;
import static org.eclipse.jgit.util.RelativeDateFormatter.YEAR_IN_MILLIS;
import static org.junit.Assert.assertEquals;
-import java.util.Date;
+import java.time.Instant;
import org.eclipse.jgit.junit.MockSystemReader;
import org.junit.After;
@@ -37,9 +37,9 @@ public class RelativeDateFormatterTest {
private static void assertFormat(long ageFromNow, long timeUnit,
String expectedFormat) {
- Date d = new Date(SystemReader.getInstance().getCurrentTime()
- - ageFromNow * timeUnit);
- String s = RelativeDateFormatter.format(d);
+ long millis = ageFromNow * timeUnit;
+ Instant aTime = SystemReader.getInstance().now().minusMillis(millis);
+ String s = RelativeDateFormatter.format(aTime);
assertEquals(expectedFormat, s);
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/StringUtilsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/StringUtilsTest.java
index aa7247e105..9a1c710752 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/StringUtilsTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/StringUtilsTest.java
@@ -12,6 +12,7 @@ package org.eclipse.jgit.util;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
@@ -153,4 +154,41 @@ public class StringUtilsTest {
() -> StringUtils.parseLongWithSuffix("8000000000000000000G",
false));
}
+
+ @Test
+ public void testCommonPrefix() {
+ assertEquals("", StringUtils.commonPrefix((String[]) null));
+ assertEquals("", StringUtils.commonPrefix(new String[] {}));
+ assertEquals("", StringUtils.commonPrefix(new String[] { null }));
+ assertEquals("", StringUtils.commonPrefix(null, null));
+ assertEquals("", StringUtils.commonPrefix("", ""));
+ assertEquals("", StringUtils.commonPrefix(null, ""));
+ assertEquals("", StringUtils.commonPrefix("abcd", null, null));
+ assertEquals("", StringUtils.commonPrefix(null, null, "abcd"));
+ assertEquals("", StringUtils.commonPrefix("", "abcd"));
+ assertEquals("", StringUtils.commonPrefix("abcd", "efgh"));
+ assertEquals("abcd", StringUtils.commonPrefix("abcd"));
+ assertEquals("ab", StringUtils.commonPrefix("abcd", "ab"));
+ assertEquals("abcd", StringUtils.commonPrefix("abcd", "abcdefgh"));
+ assertEquals("foo bar ",
+ StringUtils.commonPrefix("foo bar 42", "foo bar 24"));
+ }
+
+ @Test
+ public void testTrim() {
+ assertEquals("a", StringUtils.trim("a", '/'));
+ assertEquals("aaaa", StringUtils.trim("aaaa", '/'));
+ assertEquals("aaa", StringUtils.trim("/aaa", '/'));
+ assertEquals("aaa", StringUtils.trim("aaa/", '/'));
+ assertEquals("aaa", StringUtils.trim("/aaa/", '/'));
+ assertEquals("aa/aa", StringUtils.trim("/aa/aa/", '/'));
+ assertEquals("aa/aa", StringUtils.trim("aa/aa", '/'));
+
+ assertEquals("", StringUtils.trim("", '/'));
+ assertEquals("", StringUtils.trim("/", '/'));
+ assertEquals("", StringUtils.trim("//", '/'));
+ assertEquals("", StringUtils.trim("///", '/'));
+
+ assertNull(StringUtils.trim(null, '/'));
+ }
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/http/HttpCookiesMatcher.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/http/HttpCookiesMatcher.java
index 5a2bd976c7..c1c72804da 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/http/HttpCookiesMatcher.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/http/HttpCookiesMatcher.java
@@ -10,7 +10,7 @@
package org.eclipse.jgit.util.http;
import java.net.HttpCookie;
-import java.util.LinkedList;
+import java.util.ArrayList;
import java.util.List;
import org.hamcrest.Description;
@@ -26,7 +26,7 @@ public final class HttpCookiesMatcher {
public static Matcher<Iterable<? extends HttpCookie>> containsInOrder(
Iterable<HttpCookie> expectedCookies, int allowedMaxAgeDelta) {
- final List<Matcher<? super HttpCookie>> cookieMatchers = new LinkedList<>();
+ final List<Matcher<? super HttpCookie>> cookieMatchers = new ArrayList<>();
for (HttpCookie cookie : expectedCookies) {
cookieMatchers
.add(new HttpCookieMatcher(cookie, allowedMaxAgeDelta));
@@ -92,7 +92,7 @@ public final class HttpCookiesMatcher {
}
@SuppressWarnings("boxing")
- protected static void describeCookie(Description description,
+ private static void describeCookie(Description description,
HttpCookie cookie) {
description.appendText("HttpCookie[");
description.appendText("name: ").appendValue(cookie.getName())
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/io/ByteBufferInputStreamTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/io/ByteBufferInputStreamTest.java
new file mode 100644
index 0000000000..ec9f96ed96
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/io/ByteBufferInputStreamTest.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2023, SAP SE and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.util.io;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+import org.eclipse.jgit.internal.JGitText;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ByteBufferInputStreamTest {
+
+ private static final byte data[] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,
+ 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F };
+
+ private ByteBuffer buf;
+
+ private ByteBufferInputStream is;
+
+ @Before
+ public void setup() {
+ buf = ByteBuffer.wrap(data);
+ is = new ByteBufferInputStream(buf);
+ }
+
+ @After
+ public void tearDown() {
+ is.close();
+ }
+
+ @Test
+ public void testRead() throws IOException {
+ assertEquals(0x00, is.read());
+ assertEquals(0x01, is.read());
+ assertEquals(0x02, is.read());
+ assertEquals(0x03, is.read());
+ assertEquals(0x04, is.read());
+ assertEquals(0x05, is.read());
+ assertEquals(0x06, is.read());
+ assertEquals(0x07, is.read());
+ assertEquals(0x08, is.read());
+ assertEquals(0x09, is.read());
+ assertEquals(0x0A, is.read());
+ assertEquals(0x0B, is.read());
+ assertEquals(0x0C, is.read());
+ assertEquals(0x0D, is.read());
+ assertEquals(0x0E, is.read());
+ assertEquals(0x0F, is.read());
+ assertEquals(-1, is.read());
+ }
+
+ @Test
+ public void testReadMultiple() throws IOException {
+ byte[] x = new byte[5];
+ int n = is.read(x);
+ assertEquals(5, n);
+ assertArrayEquals(new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04 }, x);
+ }
+
+ @Test
+ public void testReadMultipleOffset() throws IOException {
+ byte[] x = new byte[7];
+ int n = is.read(x, 4, 3);
+ assertEquals(3, n);
+ assertArrayEquals(
+ new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02 },
+ x);
+ }
+
+ @Test
+ public void testReadAll() throws IOException {
+ byte[] x = is.readAllBytes();
+ assertEquals(16, x.length);
+ assertArrayEquals(data, x);
+ }
+
+ @Test
+ public void testMarkReset() throws IOException {
+ byte[] x = new byte[5];
+ int n = is.read(x);
+ assertEquals(11, is.available());
+ assertTrue(is.markSupported());
+ is.mark(is.available());
+ is.reset();
+ byte[] y = new byte[5];
+ int m = is.read(y);
+ assertEquals(n, m);
+ assertArrayEquals(new byte[] { 0x05, 0x06, 0x07, 0x08, 0x09 }, y);
+ }
+
+ @Test
+ public void testClosed() {
+ is.close();
+ Exception e = assertThrows(IOException.class, () -> is.read());
+ assertEquals(JGitText.get().inputStreamClosed, e.getMessage());
+ }
+
+ @Test
+ public void testReadNBytes() throws IOException {
+ byte[] x = is.readNBytes(4);
+ assertArrayEquals(new byte[] { 0x00, 0x01, 0x02, 0x03 }, x);
+ }
+
+ @Test
+ public void testReadNBytesOffset() throws IOException {
+ byte[] x = new byte[10];
+ Arrays.fill(x, (byte) 0x0F);
+ is.readNBytes(x, 3, 4);
+ assertArrayEquals(new byte[] { 0x0F, 0x0F, 0x0F, 0x00, 0x01, 0x02, 0x03,
+ 0x0F, 0x0F, 0x0F }, x);
+ }
+
+ @Test
+ public void testRead0() throws IOException {
+ byte[] x = new byte[7];
+ int n = is.read(x, 4, 0);
+ assertEquals(0, n);
+
+ is.readAllBytes();
+ n = is.read(x, 4, 3);
+ assertEquals(-1, n);
+ }
+
+ @Test
+ public void testSkip() throws IOException {
+ assertEquals(15, is.skip(15));
+ assertEquals(0x0F, is.read());
+ assertEquals(-1, is.read());
+ }
+
+ @Test
+ public void testSkip0() throws IOException {
+ assertEquals(0, is.skip(0));
+ assertEquals(0x00, is.read());
+ }
+}