You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

PackWriterTest.java 33KB

Shallow fetch: Respect "shallow" lines When fetching from a shallow clone, the client sends "have" lines to tell the server about objects it already has and "shallow" lines to tell where its local history terminates. In some circumstances, the server fails to honor the shallow lines and fails to return objects that the client needs. UploadPack passes the "have" lines to PackWriter so PackWriter can omit them from the generated pack. UploadPack processes "shallow" lines by calling RevWalk.assumeShallow() with the set of shallow commits. RevWalk creates and caches RevCommits for these shallow commits, clearing out their parents. That way, walks correctly terminate at the shallow commits instead of assuming the client has history going back behind them. UploadPack converts its RevWalk to an ObjectWalk, maintaining the cached RevCommits, and passes it to PackWriter. Unfortunately, to support shallow fetches the PackWriter does the following: if (shallowPack && !(walk instanceof DepthWalk.ObjectWalk)) walk = new DepthWalk.ObjectWalk(reader, depth); That is, when the client sends a "deepen" line (fetch --depth=<n>) and the caller has not passed in a DepthWalk.ObjectWalk, PackWriter throws away the RevWalk that was passed in and makes a new one. The cleared parent lists prepared by RevWalk.assumeShallow() are lost. Fortunately UploadPack intends to pass in a DepthWalk.ObjectWalk. It tries to create it by calling toObjectWalkWithSameObjects() on a DepthWalk.RevWalk. But it doesn't work: because DepthWalk.RevWalk does not override the standard RevWalk#toObjectWalkWithSameObjects implementation, the result is a plain ObjectWalk instead of an instance of DepthWalk.ObjectWalk. The result is that the "shallow" information is thrown away and objects reachable from the shallow commits can be omitted from the pack sent when fetching with --depth from a shallow clone. Multiple factors collude to limit the circumstances under which this bug can be observed: 1. Commits with depth != 0 don't enter DepthGenerator's pending queue. That means a "have" cannot have any effect on DepthGenerator unless it is also a "want". 2. DepthGenerator#next() doesn't call carryFlagsImpl(), so the uninteresting flag is not propagated to ancestors there even if a "have" is also a "want". 3. JGit treats a depth of 1 as "1 past the wants". Because of (2), the only place the UNINTERESTING flag can leak to a shallow commit's parents is in the carryFlags() call from markUninteresting(). carryFlags() only traverses commits that have already been parsed: commits yet to be parsed are supposed to inherit correct flags from their parent in PendingGenerator#next (which doesn't happen here --- that is (2)). So the list of commits that have already been parsed becomes relevant. When we hit the markUninteresting() call, all "want"s, "have"s, and commits to be unshallowed have been parsed. carryFlags() only affects the parsed commits. If the "want" is a direct parent of a "have", then it carryFlags() marks it as uninteresting. If the "have" was also a "shallow", then its parent pointer should have been null and the "want" shouldn't have been marked, so we see the bug. If the "want" is a more distant ancestor then (2) keeps the uninteresting state from propagating to the "want" and we don't see the bug. If the "shallow" is not also a "have" then the shallow commit isn't parsed so (2) keeps the uninteresting state from propagating to the "want so we don't see the bug. Here is a reproduction case (time flowing left to right, arrows pointing to parents). "C" must be a commit that the client reports as a "have" during negotiation. That can only happen if the server reports it as an existing branch or tag in the first round of negotiation: A <-- B <-- C <-- D First do git clone --depth 1 <repo> which yields D as a "have" and C as a "shallow" commit. Then try git fetch --depth 1 <repo> B:refs/heads/B Negotiation sets up: have D, shallow C, have C, want B. But due to this bug B is marked as uninteresting and is not sent. Change-Id: I6e14b57b2f85e52d28cdcf356df647870f475440 Signed-off-by: Terry Parker <tparker@google.com>
7 years ago
Fix missing deltas near type boundaries Delta search was discarding discovered deltas if an object appeared near a type boundary in the delta search window. This has caused JGit to produce larger pack files than other implementations of the packing algorithm. Delta search works by pushing prior objects into a search window, an ordered list of objects to attempt to delta compress the next object against. (The window size is bounded, avoiding O(N^2) behavior.) For implementation reasons multiple object types can appear in the input list, and the window. PackWriter commonly passes both trees and blobs in the input list handed to the DeltaWindow algorithm. The pack file format requires an object to only delta compress against the same type, so the DeltaWindow algorithm must stop doing comparisions if a blob would be compared to a tree. Because the input list is sorted by object type and the window is recently considered prior objects, once a wrong type is discovered in the window the search algorithm stops and uses the current result. Unfortunately the termination condition was discarding any found delta by setting deltaBase and deltaBuf to null when it was trying to break the window search. When this bug occurs, the state of the DeltaWindow looks like this: current | \ / input list: tree0 tree1 blob1 blob2 window: blob1 tree1 tree0 / \ | res.prev As the loop iterates to the right across the window, it first finds that blob1 is a suitable delta base for blob2, and temporarily holds this in the bestDelta/deltaBuf fields. It then considers tree1, but tree1 has the wrong type (blob != tree), so the window loop must give up and fall through the remaining code. Moving the condition up and discarding the window contents allows the bestDelta/deltaBuf to be kept, letting the final file delta compress blob1 against blob0. The impact of this bug (and its fix) on real world repositories is likely minimal. The boundary from blob to tree happens approximately once in the search, as the input list is sorted by type. Only the first window size worth of blobs (e.g. 10 or 250) were failing to produce a delta in the final file. This bug fix does produce significantly different results for small test repositories created in the unit test suite, such as when a pack may contains 6 objects (2 commits, 2 trees, 2 blobs). Packing test cases can now better sample different output pack file sizes depending on delta compression and object reuse flags in PackConfig. Change-Id: Ibec09398d0305d4dbc0c66fce1daaf38eb71148f
7 years ago
Fix missing deltas near type boundaries Delta search was discarding discovered deltas if an object appeared near a type boundary in the delta search window. This has caused JGit to produce larger pack files than other implementations of the packing algorithm. Delta search works by pushing prior objects into a search window, an ordered list of objects to attempt to delta compress the next object against. (The window size is bounded, avoiding O(N^2) behavior.) For implementation reasons multiple object types can appear in the input list, and the window. PackWriter commonly passes both trees and blobs in the input list handed to the DeltaWindow algorithm. The pack file format requires an object to only delta compress against the same type, so the DeltaWindow algorithm must stop doing comparisions if a blob would be compared to a tree. Because the input list is sorted by object type and the window is recently considered prior objects, once a wrong type is discovered in the window the search algorithm stops and uses the current result. Unfortunately the termination condition was discarding any found delta by setting deltaBase and deltaBuf to null when it was trying to break the window search. When this bug occurs, the state of the DeltaWindow looks like this: current | \ / input list: tree0 tree1 blob1 blob2 window: blob1 tree1 tree0 / \ | res.prev As the loop iterates to the right across the window, it first finds that blob1 is a suitable delta base for blob2, and temporarily holds this in the bestDelta/deltaBuf fields. It then considers tree1, but tree1 has the wrong type (blob != tree), so the window loop must give up and fall through the remaining code. Moving the condition up and discarding the window contents allows the bestDelta/deltaBuf to be kept, letting the final file delta compress blob1 against blob0. The impact of this bug (and its fix) on real world repositories is likely minimal. The boundary from blob to tree happens approximately once in the search, as the input list is sorted by type. Only the first window size worth of blobs (e.g. 10 or 250) were failing to produce a delta in the final file. This bug fix does produce significantly different results for small test repositories created in the unit test suite, such as when a pack may contains 6 objects (2 commits, 2 trees, 2 blobs). Packing test cases can now better sample different output pack file sizes depending on delta compression and object reuse flags in PackConfig. Change-Id: Ibec09398d0305d4dbc0c66fce1daaf38eb71148f
7 years ago
Shallow fetch: Respect "shallow" lines When fetching from a shallow clone, the client sends "have" lines to tell the server about objects it already has and "shallow" lines to tell where its local history terminates. In some circumstances, the server fails to honor the shallow lines and fails to return objects that the client needs. UploadPack passes the "have" lines to PackWriter so PackWriter can omit them from the generated pack. UploadPack processes "shallow" lines by calling RevWalk.assumeShallow() with the set of shallow commits. RevWalk creates and caches RevCommits for these shallow commits, clearing out their parents. That way, walks correctly terminate at the shallow commits instead of assuming the client has history going back behind them. UploadPack converts its RevWalk to an ObjectWalk, maintaining the cached RevCommits, and passes it to PackWriter. Unfortunately, to support shallow fetches the PackWriter does the following: if (shallowPack && !(walk instanceof DepthWalk.ObjectWalk)) walk = new DepthWalk.ObjectWalk(reader, depth); That is, when the client sends a "deepen" line (fetch --depth=<n>) and the caller has not passed in a DepthWalk.ObjectWalk, PackWriter throws away the RevWalk that was passed in and makes a new one. The cleared parent lists prepared by RevWalk.assumeShallow() are lost. Fortunately UploadPack intends to pass in a DepthWalk.ObjectWalk. It tries to create it by calling toObjectWalkWithSameObjects() on a DepthWalk.RevWalk. But it doesn't work: because DepthWalk.RevWalk does not override the standard RevWalk#toObjectWalkWithSameObjects implementation, the result is a plain ObjectWalk instead of an instance of DepthWalk.ObjectWalk. The result is that the "shallow" information is thrown away and objects reachable from the shallow commits can be omitted from the pack sent when fetching with --depth from a shallow clone. Multiple factors collude to limit the circumstances under which this bug can be observed: 1. Commits with depth != 0 don't enter DepthGenerator's pending queue. That means a "have" cannot have any effect on DepthGenerator unless it is also a "want". 2. DepthGenerator#next() doesn't call carryFlagsImpl(), so the uninteresting flag is not propagated to ancestors there even if a "have" is also a "want". 3. JGit treats a depth of 1 as "1 past the wants". Because of (2), the only place the UNINTERESTING flag can leak to a shallow commit's parents is in the carryFlags() call from markUninteresting(). carryFlags() only traverses commits that have already been parsed: commits yet to be parsed are supposed to inherit correct flags from their parent in PendingGenerator#next (which doesn't happen here --- that is (2)). So the list of commits that have already been parsed becomes relevant. When we hit the markUninteresting() call, all "want"s, "have"s, and commits to be unshallowed have been parsed. carryFlags() only affects the parsed commits. If the "want" is a direct parent of a "have", then it carryFlags() marks it as uninteresting. If the "have" was also a "shallow", then its parent pointer should have been null and the "want" shouldn't have been marked, so we see the bug. If the "want" is a more distant ancestor then (2) keeps the uninteresting state from propagating to the "want" and we don't see the bug. If the "shallow" is not also a "have" then the shallow commit isn't parsed so (2) keeps the uninteresting state from propagating to the "want so we don't see the bug. Here is a reproduction case (time flowing left to right, arrows pointing to parents). "C" must be a commit that the client reports as a "have" during negotiation. That can only happen if the server reports it as an existing branch or tag in the first round of negotiation: A <-- B <-- C <-- D First do git clone --depth 1 <repo> which yields D as a "have" and C as a "shallow" commit. Then try git fetch --depth 1 <repo> B:refs/heads/B Negotiation sets up: have D, shallow C, have C, want B. But due to this bug B is marked as uninteresting and is not sent. Change-Id: I6e14b57b2f85e52d28cdcf356df647870f475440 Signed-off-by: Terry Parker <tparker@google.com>
7 years ago
Shallow fetch: Respect "shallow" lines When fetching from a shallow clone, the client sends "have" lines to tell the server about objects it already has and "shallow" lines to tell where its local history terminates. In some circumstances, the server fails to honor the shallow lines and fails to return objects that the client needs. UploadPack passes the "have" lines to PackWriter so PackWriter can omit them from the generated pack. UploadPack processes "shallow" lines by calling RevWalk.assumeShallow() with the set of shallow commits. RevWalk creates and caches RevCommits for these shallow commits, clearing out their parents. That way, walks correctly terminate at the shallow commits instead of assuming the client has history going back behind them. UploadPack converts its RevWalk to an ObjectWalk, maintaining the cached RevCommits, and passes it to PackWriter. Unfortunately, to support shallow fetches the PackWriter does the following: if (shallowPack && !(walk instanceof DepthWalk.ObjectWalk)) walk = new DepthWalk.ObjectWalk(reader, depth); That is, when the client sends a "deepen" line (fetch --depth=<n>) and the caller has not passed in a DepthWalk.ObjectWalk, PackWriter throws away the RevWalk that was passed in and makes a new one. The cleared parent lists prepared by RevWalk.assumeShallow() are lost. Fortunately UploadPack intends to pass in a DepthWalk.ObjectWalk. It tries to create it by calling toObjectWalkWithSameObjects() on a DepthWalk.RevWalk. But it doesn't work: because DepthWalk.RevWalk does not override the standard RevWalk#toObjectWalkWithSameObjects implementation, the result is a plain ObjectWalk instead of an instance of DepthWalk.ObjectWalk. The result is that the "shallow" information is thrown away and objects reachable from the shallow commits can be omitted from the pack sent when fetching with --depth from a shallow clone. Multiple factors collude to limit the circumstances under which this bug can be observed: 1. Commits with depth != 0 don't enter DepthGenerator's pending queue. That means a "have" cannot have any effect on DepthGenerator unless it is also a "want". 2. DepthGenerator#next() doesn't call carryFlagsImpl(), so the uninteresting flag is not propagated to ancestors there even if a "have" is also a "want". 3. JGit treats a depth of 1 as "1 past the wants". Because of (2), the only place the UNINTERESTING flag can leak to a shallow commit's parents is in the carryFlags() call from markUninteresting(). carryFlags() only traverses commits that have already been parsed: commits yet to be parsed are supposed to inherit correct flags from their parent in PendingGenerator#next (which doesn't happen here --- that is (2)). So the list of commits that have already been parsed becomes relevant. When we hit the markUninteresting() call, all "want"s, "have"s, and commits to be unshallowed have been parsed. carryFlags() only affects the parsed commits. If the "want" is a direct parent of a "have", then it carryFlags() marks it as uninteresting. If the "have" was also a "shallow", then its parent pointer should have been null and the "want" shouldn't have been marked, so we see the bug. If the "want" is a more distant ancestor then (2) keeps the uninteresting state from propagating to the "want" and we don't see the bug. If the "shallow" is not also a "have" then the shallow commit isn't parsed so (2) keeps the uninteresting state from propagating to the "want so we don't see the bug. Here is a reproduction case (time flowing left to right, arrows pointing to parents). "C" must be a commit that the client reports as a "have" during negotiation. That can only happen if the server reports it as an existing branch or tag in the first round of negotiation: A <-- B <-- C <-- D First do git clone --depth 1 <repo> which yields D as a "have" and C as a "shallow" commit. Then try git fetch --depth 1 <repo> B:refs/heads/B Negotiation sets up: have D, shallow C, have C, want B. But due to this bug B is marked as uninteresting and is not sent. Change-Id: I6e14b57b2f85e52d28cdcf356df647870f475440 Signed-off-by: Terry Parker <tparker@google.com>
7 years ago
Shallow fetch: Respect "shallow" lines When fetching from a shallow clone, the client sends "have" lines to tell the server about objects it already has and "shallow" lines to tell where its local history terminates. In some circumstances, the server fails to honor the shallow lines and fails to return objects that the client needs. UploadPack passes the "have" lines to PackWriter so PackWriter can omit them from the generated pack. UploadPack processes "shallow" lines by calling RevWalk.assumeShallow() with the set of shallow commits. RevWalk creates and caches RevCommits for these shallow commits, clearing out their parents. That way, walks correctly terminate at the shallow commits instead of assuming the client has history going back behind them. UploadPack converts its RevWalk to an ObjectWalk, maintaining the cached RevCommits, and passes it to PackWriter. Unfortunately, to support shallow fetches the PackWriter does the following: if (shallowPack && !(walk instanceof DepthWalk.ObjectWalk)) walk = new DepthWalk.ObjectWalk(reader, depth); That is, when the client sends a "deepen" line (fetch --depth=<n>) and the caller has not passed in a DepthWalk.ObjectWalk, PackWriter throws away the RevWalk that was passed in and makes a new one. The cleared parent lists prepared by RevWalk.assumeShallow() are lost. Fortunately UploadPack intends to pass in a DepthWalk.ObjectWalk. It tries to create it by calling toObjectWalkWithSameObjects() on a DepthWalk.RevWalk. But it doesn't work: because DepthWalk.RevWalk does not override the standard RevWalk#toObjectWalkWithSameObjects implementation, the result is a plain ObjectWalk instead of an instance of DepthWalk.ObjectWalk. The result is that the "shallow" information is thrown away and objects reachable from the shallow commits can be omitted from the pack sent when fetching with --depth from a shallow clone. Multiple factors collude to limit the circumstances under which this bug can be observed: 1. Commits with depth != 0 don't enter DepthGenerator's pending queue. That means a "have" cannot have any effect on DepthGenerator unless it is also a "want". 2. DepthGenerator#next() doesn't call carryFlagsImpl(), so the uninteresting flag is not propagated to ancestors there even if a "have" is also a "want". 3. JGit treats a depth of 1 as "1 past the wants". Because of (2), the only place the UNINTERESTING flag can leak to a shallow commit's parents is in the carryFlags() call from markUninteresting(). carryFlags() only traverses commits that have already been parsed: commits yet to be parsed are supposed to inherit correct flags from their parent in PendingGenerator#next (which doesn't happen here --- that is (2)). So the list of commits that have already been parsed becomes relevant. When we hit the markUninteresting() call, all "want"s, "have"s, and commits to be unshallowed have been parsed. carryFlags() only affects the parsed commits. If the "want" is a direct parent of a "have", then it carryFlags() marks it as uninteresting. If the "have" was also a "shallow", then its parent pointer should have been null and the "want" shouldn't have been marked, so we see the bug. If the "want" is a more distant ancestor then (2) keeps the uninteresting state from propagating to the "want" and we don't see the bug. If the "shallow" is not also a "have" then the shallow commit isn't parsed so (2) keeps the uninteresting state from propagating to the "want so we don't see the bug. Here is a reproduction case (time flowing left to right, arrows pointing to parents). "C" must be a commit that the client reports as a "have" during negotiation. That can only happen if the server reports it as an existing branch or tag in the first round of negotiation: A <-- B <-- C <-- D First do git clone --depth 1 <repo> which yields D as a "have" and C as a "shallow" commit. Then try git fetch --depth 1 <repo> B:refs/heads/B Negotiation sets up: have D, shallow C, have C, want B. But due to this bug B is marked as uninteresting and is not sent. Change-Id: I6e14b57b2f85e52d28cdcf356df647870f475440 Signed-off-by: Terry Parker <tparker@google.com>
7 years ago
Shallow fetch: Respect "shallow" lines When fetching from a shallow clone, the client sends "have" lines to tell the server about objects it already has and "shallow" lines to tell where its local history terminates. In some circumstances, the server fails to honor the shallow lines and fails to return objects that the client needs. UploadPack passes the "have" lines to PackWriter so PackWriter can omit them from the generated pack. UploadPack processes "shallow" lines by calling RevWalk.assumeShallow() with the set of shallow commits. RevWalk creates and caches RevCommits for these shallow commits, clearing out their parents. That way, walks correctly terminate at the shallow commits instead of assuming the client has history going back behind them. UploadPack converts its RevWalk to an ObjectWalk, maintaining the cached RevCommits, and passes it to PackWriter. Unfortunately, to support shallow fetches the PackWriter does the following: if (shallowPack && !(walk instanceof DepthWalk.ObjectWalk)) walk = new DepthWalk.ObjectWalk(reader, depth); That is, when the client sends a "deepen" line (fetch --depth=<n>) and the caller has not passed in a DepthWalk.ObjectWalk, PackWriter throws away the RevWalk that was passed in and makes a new one. The cleared parent lists prepared by RevWalk.assumeShallow() are lost. Fortunately UploadPack intends to pass in a DepthWalk.ObjectWalk. It tries to create it by calling toObjectWalkWithSameObjects() on a DepthWalk.RevWalk. But it doesn't work: because DepthWalk.RevWalk does not override the standard RevWalk#toObjectWalkWithSameObjects implementation, the result is a plain ObjectWalk instead of an instance of DepthWalk.ObjectWalk. The result is that the "shallow" information is thrown away and objects reachable from the shallow commits can be omitted from the pack sent when fetching with --depth from a shallow clone. Multiple factors collude to limit the circumstances under which this bug can be observed: 1. Commits with depth != 0 don't enter DepthGenerator's pending queue. That means a "have" cannot have any effect on DepthGenerator unless it is also a "want". 2. DepthGenerator#next() doesn't call carryFlagsImpl(), so the uninteresting flag is not propagated to ancestors there even if a "have" is also a "want". 3. JGit treats a depth of 1 as "1 past the wants". Because of (2), the only place the UNINTERESTING flag can leak to a shallow commit's parents is in the carryFlags() call from markUninteresting(). carryFlags() only traverses commits that have already been parsed: commits yet to be parsed are supposed to inherit correct flags from their parent in PendingGenerator#next (which doesn't happen here --- that is (2)). So the list of commits that have already been parsed becomes relevant. When we hit the markUninteresting() call, all "want"s, "have"s, and commits to be unshallowed have been parsed. carryFlags() only affects the parsed commits. If the "want" is a direct parent of a "have", then it carryFlags() marks it as uninteresting. If the "have" was also a "shallow", then its parent pointer should have been null and the "want" shouldn't have been marked, so we see the bug. If the "want" is a more distant ancestor then (2) keeps the uninteresting state from propagating to the "want" and we don't see the bug. If the "shallow" is not also a "have" then the shallow commit isn't parsed so (2) keeps the uninteresting state from propagating to the "want so we don't see the bug. Here is a reproduction case (time flowing left to right, arrows pointing to parents). "C" must be a commit that the client reports as a "have" during negotiation. That can only happen if the server reports it as an existing branch or tag in the first round of negotiation: A <-- B <-- C <-- D First do git clone --depth 1 <repo> which yields D as a "have" and C as a "shallow" commit. Then try git fetch --depth 1 <repo> B:refs/heads/B Negotiation sets up: have D, shallow C, have C, want B. But due to this bug B is marked as uninteresting and is not sent. Change-Id: I6e14b57b2f85e52d28cdcf356df647870f475440 Signed-off-by: Terry Parker <tparker@google.com>
7 years ago
Shallow fetch: Respect "shallow" lines When fetching from a shallow clone, the client sends "have" lines to tell the server about objects it already has and "shallow" lines to tell where its local history terminates. In some circumstances, the server fails to honor the shallow lines and fails to return objects that the client needs. UploadPack passes the "have" lines to PackWriter so PackWriter can omit them from the generated pack. UploadPack processes "shallow" lines by calling RevWalk.assumeShallow() with the set of shallow commits. RevWalk creates and caches RevCommits for these shallow commits, clearing out their parents. That way, walks correctly terminate at the shallow commits instead of assuming the client has history going back behind them. UploadPack converts its RevWalk to an ObjectWalk, maintaining the cached RevCommits, and passes it to PackWriter. Unfortunately, to support shallow fetches the PackWriter does the following: if (shallowPack && !(walk instanceof DepthWalk.ObjectWalk)) walk = new DepthWalk.ObjectWalk(reader, depth); That is, when the client sends a "deepen" line (fetch --depth=<n>) and the caller has not passed in a DepthWalk.ObjectWalk, PackWriter throws away the RevWalk that was passed in and makes a new one. The cleared parent lists prepared by RevWalk.assumeShallow() are lost. Fortunately UploadPack intends to pass in a DepthWalk.ObjectWalk. It tries to create it by calling toObjectWalkWithSameObjects() on a DepthWalk.RevWalk. But it doesn't work: because DepthWalk.RevWalk does not override the standard RevWalk#toObjectWalkWithSameObjects implementation, the result is a plain ObjectWalk instead of an instance of DepthWalk.ObjectWalk. The result is that the "shallow" information is thrown away and objects reachable from the shallow commits can be omitted from the pack sent when fetching with --depth from a shallow clone. Multiple factors collude to limit the circumstances under which this bug can be observed: 1. Commits with depth != 0 don't enter DepthGenerator's pending queue. That means a "have" cannot have any effect on DepthGenerator unless it is also a "want". 2. DepthGenerator#next() doesn't call carryFlagsImpl(), so the uninteresting flag is not propagated to ancestors there even if a "have" is also a "want". 3. JGit treats a depth of 1 as "1 past the wants". Because of (2), the only place the UNINTERESTING flag can leak to a shallow commit's parents is in the carryFlags() call from markUninteresting(). carryFlags() only traverses commits that have already been parsed: commits yet to be parsed are supposed to inherit correct flags from their parent in PendingGenerator#next (which doesn't happen here --- that is (2)). So the list of commits that have already been parsed becomes relevant. When we hit the markUninteresting() call, all "want"s, "have"s, and commits to be unshallowed have been parsed. carryFlags() only affects the parsed commits. If the "want" is a direct parent of a "have", then it carryFlags() marks it as uninteresting. If the "have" was also a "shallow", then its parent pointer should have been null and the "want" shouldn't have been marked, so we see the bug. If the "want" is a more distant ancestor then (2) keeps the uninteresting state from propagating to the "want" and we don't see the bug. If the "shallow" is not also a "have" then the shallow commit isn't parsed so (2) keeps the uninteresting state from propagating to the "want so we don't see the bug. Here is a reproduction case (time flowing left to right, arrows pointing to parents). "C" must be a commit that the client reports as a "have" during negotiation. That can only happen if the server reports it as an existing branch or tag in the first round of negotiation: A <-- B <-- C <-- D First do git clone --depth 1 <repo> which yields D as a "have" and C as a "shallow" commit. Then try git fetch --depth 1 <repo> B:refs/heads/B Negotiation sets up: have D, shallow C, have C, want B. But due to this bug B is marked as uninteresting and is not sent. Change-Id: I6e14b57b2f85e52d28cdcf356df647870f475440 Signed-off-by: Terry Parker <tparker@google.com>
7 years ago
Fix missing deltas near type boundaries Delta search was discarding discovered deltas if an object appeared near a type boundary in the delta search window. This has caused JGit to produce larger pack files than other implementations of the packing algorithm. Delta search works by pushing prior objects into a search window, an ordered list of objects to attempt to delta compress the next object against. (The window size is bounded, avoiding O(N^2) behavior.) For implementation reasons multiple object types can appear in the input list, and the window. PackWriter commonly passes both trees and blobs in the input list handed to the DeltaWindow algorithm. The pack file format requires an object to only delta compress against the same type, so the DeltaWindow algorithm must stop doing comparisions if a blob would be compared to a tree. Because the input list is sorted by object type and the window is recently considered prior objects, once a wrong type is discovered in the window the search algorithm stops and uses the current result. Unfortunately the termination condition was discarding any found delta by setting deltaBase and deltaBuf to null when it was trying to break the window search. When this bug occurs, the state of the DeltaWindow looks like this: current | \ / input list: tree0 tree1 blob1 blob2 window: blob1 tree1 tree0 / \ | res.prev As the loop iterates to the right across the window, it first finds that blob1 is a suitable delta base for blob2, and temporarily holds this in the bestDelta/deltaBuf fields. It then considers tree1, but tree1 has the wrong type (blob != tree), so the window loop must give up and fall through the remaining code. Moving the condition up and discarding the window contents allows the bestDelta/deltaBuf to be kept, letting the final file delta compress blob1 against blob0. The impact of this bug (and its fix) on real world repositories is likely minimal. The boundary from blob to tree happens approximately once in the search, as the input list is sorted by type. Only the first window size worth of blobs (e.g. 10 or 250) were failing to produce a delta in the final file. This bug fix does produce significantly different results for small test repositories created in the unit test suite, such as when a pack may contains 6 objects (2 commits, 2 trees, 2 blobs). Packing test cases can now better sample different output pack file sizes depending on delta compression and object reuse flags in PackConfig. Change-Id: Ibec09398d0305d4dbc0c66fce1daaf38eb71148f
7 years ago
Fix missing deltas near type boundaries Delta search was discarding discovered deltas if an object appeared near a type boundary in the delta search window. This has caused JGit to produce larger pack files than other implementations of the packing algorithm. Delta search works by pushing prior objects into a search window, an ordered list of objects to attempt to delta compress the next object against. (The window size is bounded, avoiding O(N^2) behavior.) For implementation reasons multiple object types can appear in the input list, and the window. PackWriter commonly passes both trees and blobs in the input list handed to the DeltaWindow algorithm. The pack file format requires an object to only delta compress against the same type, so the DeltaWindow algorithm must stop doing comparisions if a blob would be compared to a tree. Because the input list is sorted by object type and the window is recently considered prior objects, once a wrong type is discovered in the window the search algorithm stops and uses the current result. Unfortunately the termination condition was discarding any found delta by setting deltaBase and deltaBuf to null when it was trying to break the window search. When this bug occurs, the state of the DeltaWindow looks like this: current | \ / input list: tree0 tree1 blob1 blob2 window: blob1 tree1 tree0 / \ | res.prev As the loop iterates to the right across the window, it first finds that blob1 is a suitable delta base for blob2, and temporarily holds this in the bestDelta/deltaBuf fields. It then considers tree1, but tree1 has the wrong type (blob != tree), so the window loop must give up and fall through the remaining code. Moving the condition up and discarding the window contents allows the bestDelta/deltaBuf to be kept, letting the final file delta compress blob1 against blob0. The impact of this bug (and its fix) on real world repositories is likely minimal. The boundary from blob to tree happens approximately once in the search, as the input list is sorted by type. Only the first window size worth of blobs (e.g. 10 or 250) were failing to produce a delta in the final file. This bug fix does produce significantly different results for small test repositories created in the unit test suite, such as when a pack may contains 6 objects (2 commits, 2 trees, 2 blobs). Packing test cases can now better sample different output pack file sizes depending on delta compression and object reuse flags in PackConfig. Change-Id: Ibec09398d0305d4dbc0c66fce1daaf38eb71148f
7 years ago
Fix missing deltas near type boundaries Delta search was discarding discovered deltas if an object appeared near a type boundary in the delta search window. This has caused JGit to produce larger pack files than other implementations of the packing algorithm. Delta search works by pushing prior objects into a search window, an ordered list of objects to attempt to delta compress the next object against. (The window size is bounded, avoiding O(N^2) behavior.) For implementation reasons multiple object types can appear in the input list, and the window. PackWriter commonly passes both trees and blobs in the input list handed to the DeltaWindow algorithm. The pack file format requires an object to only delta compress against the same type, so the DeltaWindow algorithm must stop doing comparisions if a blob would be compared to a tree. Because the input list is sorted by object type and the window is recently considered prior objects, once a wrong type is discovered in the window the search algorithm stops and uses the current result. Unfortunately the termination condition was discarding any found delta by setting deltaBase and deltaBuf to null when it was trying to break the window search. When this bug occurs, the state of the DeltaWindow looks like this: current | \ / input list: tree0 tree1 blob1 blob2 window: blob1 tree1 tree0 / \ | res.prev As the loop iterates to the right across the window, it first finds that blob1 is a suitable delta base for blob2, and temporarily holds this in the bestDelta/deltaBuf fields. It then considers tree1, but tree1 has the wrong type (blob != tree), so the window loop must give up and fall through the remaining code. Moving the condition up and discarding the window contents allows the bestDelta/deltaBuf to be kept, letting the final file delta compress blob1 against blob0. The impact of this bug (and its fix) on real world repositories is likely minimal. The boundary from blob to tree happens approximately once in the search, as the input list is sorted by type. Only the first window size worth of blobs (e.g. 10 or 250) were failing to produce a delta in the final file. This bug fix does produce significantly different results for small test repositories created in the unit test suite, such as when a pack may contains 6 objects (2 commits, 2 trees, 2 blobs). Packing test cases can now better sample different output pack file sizes depending on delta compression and object reuse flags in PackConfig. Change-Id: Ibec09398d0305d4dbc0c66fce1daaf38eb71148f
7 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011
  1. /*
  2. * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> and others
  3. *
  4. * This program and the accompanying materials are made available under the
  5. * terms of the Eclipse Distribution License v. 1.0 which is available at
  6. * https://www.eclipse.org/org/documents/edl-v10.php.
  7. *
  8. * SPDX-License-Identifier: BSD-3-Clause
  9. */
  10. package org.eclipse.jgit.internal.storage.file;
  11. import static org.eclipse.jgit.internal.storage.pack.PackWriter.NONE;
  12. import static org.eclipse.jgit.lib.Constants.INFO_ALTERNATES;
  13. import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
  14. import static org.junit.Assert.assertEquals;
  15. import static org.junit.Assert.assertFalse;
  16. import static org.junit.Assert.assertNotNull;
  17. import static org.junit.Assert.assertTrue;
  18. import static org.junit.Assert.fail;
  19. import static org.mockito.ArgumentMatchers.any;
  20. import static org.mockito.Mockito.doNothing;
  21. import static org.mockito.Mockito.times;
  22. import static org.mockito.Mockito.verify;
  23. import java.io.ByteArrayInputStream;
  24. import java.io.ByteArrayOutputStream;
  25. import java.io.File;
  26. import java.io.FileOutputStream;
  27. import java.io.IOException;
  28. import java.text.ParseException;
  29. import java.time.Duration;
  30. import java.util.ArrayList;
  31. import java.util.Arrays;
  32. import java.util.Collections;
  33. import java.util.HashSet;
  34. import java.util.List;
  35. import java.util.Set;
  36. import org.eclipse.jgit.api.Git;
  37. import org.eclipse.jgit.errors.MissingObjectException;
  38. import org.eclipse.jgit.internal.storage.file.PackIndex.MutableEntry;
  39. import org.eclipse.jgit.internal.storage.pack.PackExt;
  40. import org.eclipse.jgit.internal.storage.pack.PackWriter;
  41. import org.eclipse.jgit.junit.JGitTestUtil;
  42. import org.eclipse.jgit.junit.TestRepository;
  43. import org.eclipse.jgit.junit.TestRepository.BranchBuilder;
  44. import org.eclipse.jgit.lib.NullProgressMonitor;
  45. import org.eclipse.jgit.lib.ObjectId;
  46. import org.eclipse.jgit.lib.ObjectIdSet;
  47. import org.eclipse.jgit.lib.ObjectInserter;
  48. import org.eclipse.jgit.lib.Ref;
  49. import org.eclipse.jgit.lib.Repository;
  50. import org.eclipse.jgit.lib.Sets;
  51. import org.eclipse.jgit.revwalk.DepthWalk;
  52. import org.eclipse.jgit.revwalk.ObjectWalk;
  53. import org.eclipse.jgit.revwalk.RevBlob;
  54. import org.eclipse.jgit.revwalk.RevCommit;
  55. import org.eclipse.jgit.revwalk.RevObject;
  56. import org.eclipse.jgit.revwalk.RevWalk;
  57. import org.eclipse.jgit.storage.pack.PackConfig;
  58. import org.eclipse.jgit.storage.pack.PackStatistics;
  59. import org.eclipse.jgit.test.resources.SampleDataRepositoryTestCase;
  60. import org.eclipse.jgit.transport.PackParser;
  61. import org.junit.After;
  62. import org.junit.Before;
  63. import org.junit.Test;
  64. import org.mockito.Mockito;
  65. public class PackWriterTest extends SampleDataRepositoryTestCase {
  66. private static final List<RevObject> EMPTY_LIST_REVS = Collections
  67. .<RevObject> emptyList();
  68. private static final Set<ObjectIdSet> EMPTY_ID_SET = Collections
  69. .<ObjectIdSet> emptySet();
  70. private PackConfig config;
  71. private PackWriter writer;
  72. private ByteArrayOutputStream os;
  73. private Pack pack;
  74. private ObjectInserter inserter;
  75. private FileRepository dst;
  76. private RevBlob contentA;
  77. private RevBlob contentB;
  78. private RevBlob contentC;
  79. private RevBlob contentD;
  80. private RevBlob contentE;
  81. private RevCommit c1;
  82. private RevCommit c2;
  83. private RevCommit c3;
  84. private RevCommit c4;
  85. private RevCommit c5;
  86. @Override
  87. @Before
  88. public void setUp() throws Exception {
  89. super.setUp();
  90. os = new ByteArrayOutputStream();
  91. config = new PackConfig(db);
  92. dst = createBareRepository();
  93. File alt = new File(dst.getObjectDatabase().getDirectory(), INFO_ALTERNATES);
  94. alt.getParentFile().mkdirs();
  95. write(alt, db.getObjectDatabase().getDirectory().getAbsolutePath() + "\n");
  96. }
  97. @Override
  98. @After
  99. public void tearDown() throws Exception {
  100. if (writer != null) {
  101. writer.close();
  102. writer = null;
  103. }
  104. if (inserter != null) {
  105. inserter.close();
  106. inserter = null;
  107. }
  108. super.tearDown();
  109. }
  110. /**
  111. * Test constructor for exceptions, default settings, initialization.
  112. *
  113. * @throws IOException
  114. */
  115. @Test
  116. public void testContructor() throws IOException {
  117. writer = new PackWriter(config, db.newObjectReader());
  118. assertFalse(writer.isDeltaBaseAsOffset());
  119. assertTrue(config.isReuseDeltas());
  120. assertTrue(config.isReuseObjects());
  121. assertEquals(0, writer.getObjectCount());
  122. }
  123. /**
  124. * Change default settings and verify them.
  125. */
  126. @Test
  127. public void testModifySettings() {
  128. config.setReuseDeltas(false);
  129. config.setReuseObjects(false);
  130. config.setDeltaBaseAsOffset(false);
  131. assertFalse(config.isReuseDeltas());
  132. assertFalse(config.isReuseObjects());
  133. assertFalse(config.isDeltaBaseAsOffset());
  134. writer = new PackWriter(config, db.newObjectReader());
  135. writer.setDeltaBaseAsOffset(true);
  136. assertTrue(writer.isDeltaBaseAsOffset());
  137. assertFalse(config.isDeltaBaseAsOffset());
  138. }
  139. /**
  140. * Write empty pack by providing empty sets of interesting/uninteresting
  141. * objects and check for correct format.
  142. *
  143. * @throws IOException
  144. */
  145. @Test
  146. public void testWriteEmptyPack1() throws IOException {
  147. createVerifyOpenPack(NONE, NONE, false, false);
  148. assertEquals(0, writer.getObjectCount());
  149. assertEquals(0, pack.getObjectCount());
  150. assertEquals("da39a3ee5e6b4b0d3255bfef95601890afd80709", writer
  151. .computeName().name());
  152. }
  153. /**
  154. * Write empty pack by providing empty iterator of objects to write and
  155. * check for correct format.
  156. *
  157. * @throws IOException
  158. */
  159. @Test
  160. public void testWriteEmptyPack2() throws IOException {
  161. createVerifyOpenPack(EMPTY_LIST_REVS);
  162. assertEquals(0, writer.getObjectCount());
  163. assertEquals(0, pack.getObjectCount());
  164. }
  165. /**
  166. * Try to pass non-existing object as uninteresting, with non-ignoring
  167. * setting.
  168. *
  169. * @throws IOException
  170. */
  171. @Test
  172. public void testNotIgnoreNonExistingObjects() throws IOException {
  173. final ObjectId nonExisting = ObjectId
  174. .fromString("0000000000000000000000000000000000000001");
  175. try {
  176. createVerifyOpenPack(NONE, haves(nonExisting), false, false);
  177. fail("Should have thrown MissingObjectException");
  178. } catch (MissingObjectException x) {
  179. // expected
  180. }
  181. }
  182. /**
  183. * Try to pass non-existing object as uninteresting, with ignoring setting.
  184. *
  185. * @throws IOException
  186. */
  187. @Test
  188. public void testIgnoreNonExistingObjects() throws IOException {
  189. final ObjectId nonExisting = ObjectId
  190. .fromString("0000000000000000000000000000000000000001");
  191. createVerifyOpenPack(NONE, haves(nonExisting), false, true);
  192. // shouldn't throw anything
  193. }
  194. /**
  195. * Try to pass non-existing object as uninteresting, with ignoring setting.
  196. * Use a repo with bitmap indexes because then PackWriter will use
  197. * PackWriterBitmapWalker which had problems with this situation.
  198. *
  199. * @throws IOException
  200. * @throws ParseException
  201. */
  202. @Test
  203. public void testIgnoreNonExistingObjectsWithBitmaps() throws IOException,
  204. ParseException {
  205. final ObjectId nonExisting = ObjectId
  206. .fromString("0000000000000000000000000000000000000001");
  207. new GC(db).gc();
  208. createVerifyOpenPack(NONE, haves(nonExisting), false, true, true);
  209. // shouldn't throw anything
  210. }
  211. /**
  212. * Create pack basing on only interesting objects, then precisely verify
  213. * content. No delta reuse here.
  214. *
  215. * @throws IOException
  216. */
  217. @Test
  218. public void testWritePack1() throws IOException {
  219. config.setReuseDeltas(false);
  220. writeVerifyPack1();
  221. }
  222. /**
  223. * Test writing pack without object reuse. Pack content/preparation as in
  224. * {@link #testWritePack1()}.
  225. *
  226. * @throws IOException
  227. */
  228. @Test
  229. public void testWritePack1NoObjectReuse() throws IOException {
  230. config.setReuseDeltas(false);
  231. config.setReuseObjects(false);
  232. writeVerifyPack1();
  233. }
  234. /**
  235. * Create pack basing on both interesting and uninteresting objects, then
  236. * precisely verify content. No delta reuse here.
  237. *
  238. * @throws IOException
  239. */
  240. @Test
  241. public void testWritePack2() throws IOException {
  242. writeVerifyPack2(false);
  243. }
  244. /**
  245. * Test pack writing with deltas reuse, delta-base first rule. Pack
  246. * content/preparation as in {@link #testWritePack2()}.
  247. *
  248. * @throws IOException
  249. */
  250. @Test
  251. public void testWritePack2DeltasReuseRefs() throws IOException {
  252. writeVerifyPack2(true);
  253. }
  254. /**
  255. * Test pack writing with delta reuse. Delta bases referred as offsets. Pack
  256. * configuration as in {@link #testWritePack2DeltasReuseRefs()}.
  257. *
  258. * @throws IOException
  259. */
  260. @Test
  261. public void testWritePack2DeltasReuseOffsets() throws IOException {
  262. config.setDeltaBaseAsOffset(true);
  263. writeVerifyPack2(true);
  264. }
  265. /**
  266. * Test pack writing with delta reuse. Raw-data copy (reuse) is made on a
  267. * pack with CRC32 index. Pack configuration as in
  268. * {@link #testWritePack2DeltasReuseRefs()}.
  269. *
  270. * @throws IOException
  271. */
  272. @Test
  273. public void testWritePack2DeltasCRC32Copy() throws IOException {
  274. final File packDir = db.getObjectDatabase().getPackDirectory();
  275. final PackFile crc32Pack = new PackFile(packDir,
  276. "pack-34be9032ac282b11fa9babdc2b2a93ca996c9c2f.pack");
  277. final PackFile crc32Idx = new PackFile(packDir,
  278. "pack-34be9032ac282b11fa9babdc2b2a93ca996c9c2f.idx");
  279. copyFile(JGitTestUtil.getTestResourceFile(
  280. "pack-34be9032ac282b11fa9babdc2b2a93ca996c9c2f.idxV2"),
  281. crc32Idx);
  282. db.openPack(crc32Pack);
  283. writeVerifyPack2(true);
  284. }
  285. /**
  286. * Create pack basing on fixed objects list, then precisely verify content.
  287. * No delta reuse here.
  288. *
  289. * @throws IOException
  290. * @throws MissingObjectException
  291. *
  292. */
  293. @Test
  294. public void testWritePack3() throws MissingObjectException, IOException {
  295. config.setReuseDeltas(false);
  296. final ObjectId forcedOrder[] = new ObjectId[] {
  297. ObjectId.fromString("82c6b885ff600be425b4ea96dee75dca255b69e7"),
  298. ObjectId.fromString("c59759f143fb1fe21c197981df75a7ee00290799"),
  299. ObjectId.fromString("aabf2ffaec9b497f0950352b3e582d73035c2035"),
  300. ObjectId.fromString("902d5476fa249b7abc9d84c611577a81381f0327"),
  301. ObjectId.fromString("6ff87c4664981e4397625791c8ea3bbb5f2279a3") ,
  302. ObjectId.fromString("5b6e7c66c276e7610d4a73c70ec1a1f7c1003259") };
  303. try (RevWalk parser = new RevWalk(db)) {
  304. final RevObject forcedOrderRevs[] = new RevObject[forcedOrder.length];
  305. for (int i = 0; i < forcedOrder.length; i++)
  306. forcedOrderRevs[i] = parser.parseAny(forcedOrder[i]);
  307. createVerifyOpenPack(Arrays.asList(forcedOrderRevs));
  308. }
  309. assertEquals(forcedOrder.length, writer.getObjectCount());
  310. verifyObjectsOrder(forcedOrder);
  311. assertEquals("ed3f96b8327c7c66b0f8f70056129f0769323d86", writer
  312. .computeName().name());
  313. }
  314. /**
  315. * Another pack creation: basing on both interesting and uninteresting
  316. * objects. No delta reuse possible here, as this is a specific case when we
  317. * write only 1 commit, associated with 1 tree, 1 blob.
  318. *
  319. * @throws IOException
  320. */
  321. @Test
  322. public void testWritePack4() throws IOException {
  323. writeVerifyPack4(false);
  324. }
  325. /**
  326. * Test thin pack writing: 1 blob delta base is on objects edge. Pack
  327. * configuration as in {@link #testWritePack4()}.
  328. *
  329. * @throws IOException
  330. */
  331. @Test
  332. public void testWritePack4ThinPack() throws IOException {
  333. writeVerifyPack4(true);
  334. }
  335. /**
  336. * Compare sizes of packs created using {@link #testWritePack2()} and
  337. * {@link #testWritePack2DeltasReuseRefs()}. The pack using deltas should
  338. * be smaller.
  339. *
  340. * @throws Exception
  341. */
  342. @Test
  343. public void testWritePack2SizeDeltasVsNoDeltas() throws Exception {
  344. config.setReuseDeltas(false);
  345. config.setDeltaCompress(false);
  346. testWritePack2();
  347. final long sizePack2NoDeltas = os.size();
  348. tearDown();
  349. setUp();
  350. testWritePack2DeltasReuseRefs();
  351. final long sizePack2DeltasRefs = os.size();
  352. assertTrue(sizePack2NoDeltas > sizePack2DeltasRefs);
  353. }
  354. /**
  355. * Compare sizes of packs created using
  356. * {@link #testWritePack2DeltasReuseRefs()} and
  357. * {@link #testWritePack2DeltasReuseOffsets()}. The pack with delta bases
  358. * written as offsets should be smaller.
  359. *
  360. * @throws Exception
  361. */
  362. @Test
  363. public void testWritePack2SizeOffsetsVsRefs() throws Exception {
  364. testWritePack2DeltasReuseRefs();
  365. final long sizePack2DeltasRefs = os.size();
  366. tearDown();
  367. setUp();
  368. testWritePack2DeltasReuseOffsets();
  369. final long sizePack2DeltasOffsets = os.size();
  370. assertTrue(sizePack2DeltasRefs > sizePack2DeltasOffsets);
  371. }
  372. /**
  373. * Compare sizes of packs created using {@link #testWritePack4()} and
  374. * {@link #testWritePack4ThinPack()}. Obviously, the thin pack should be
  375. * smaller.
  376. *
  377. * @throws Exception
  378. */
  379. @Test
  380. public void testWritePack4SizeThinVsNoThin() throws Exception {
  381. testWritePack4();
  382. final long sizePack4 = os.size();
  383. tearDown();
  384. setUp();
  385. testWritePack4ThinPack();
  386. final long sizePack4Thin = os.size();
  387. assertTrue(sizePack4 > sizePack4Thin);
  388. }
  389. @Test
  390. public void testDeltaStatistics() throws Exception {
  391. config.setDeltaCompress(true);
  392. // TestRepository will close repo
  393. FileRepository repo = createBareRepository();
  394. ArrayList<RevObject> blobs = new ArrayList<>();
  395. try (TestRepository<FileRepository> testRepo = new TestRepository<>(
  396. repo)) {
  397. blobs.add(testRepo.blob(genDeltableData(1000)));
  398. blobs.add(testRepo.blob(genDeltableData(1005)));
  399. try (PackWriter pw = new PackWriter(repo)) {
  400. NullProgressMonitor m = NullProgressMonitor.INSTANCE;
  401. pw.preparePack(blobs.iterator());
  402. pw.writePack(m, m, os);
  403. PackStatistics stats = pw.getStatistics();
  404. assertEquals(1, stats.getTotalDeltas());
  405. assertTrue("Delta bytes not set.",
  406. stats.byObjectType(OBJ_BLOB).getDeltaBytes() > 0);
  407. }
  408. }
  409. }
  410. // Generate consistent junk data for building files that delta well
  411. private String genDeltableData(int length) {
  412. assertTrue("Generated data must have a length > 0", length > 0);
  413. char[] data = {'a', 'b', 'c', '\n'};
  414. StringBuilder builder = new StringBuilder(length);
  415. for (int i = 0; i < length; i++) {
  416. builder.append(data[i % 4]);
  417. }
  418. return builder.toString();
  419. }
  420. @Test
  421. public void testWriteIndex() throws Exception {
  422. config.setIndexVersion(2);
  423. writeVerifyPack4(false);
  424. PackFile packFile = pack.getPackFile();
  425. PackFile indexFile = packFile.create(PackExt.INDEX);
  426. // Validate that IndexPack came up with the right CRC32 value.
  427. final PackIndex idx1 = PackIndex.open(indexFile);
  428. assertTrue(idx1 instanceof PackIndexV2);
  429. assertEquals(0x4743F1E4L, idx1.findCRC32(ObjectId
  430. .fromString("82c6b885ff600be425b4ea96dee75dca255b69e7")));
  431. // Validate that an index written by PackWriter is the same.
  432. final File idx2File = new File(indexFile.getAbsolutePath() + ".2");
  433. try (FileOutputStream is = new FileOutputStream(idx2File)) {
  434. writer.writeIndex(is);
  435. }
  436. final PackIndex idx2 = PackIndex.open(idx2File);
  437. assertTrue(idx2 instanceof PackIndexV2);
  438. assertEquals(idx1.getObjectCount(), idx2.getObjectCount());
  439. assertEquals(idx1.getOffset64Count(), idx2.getOffset64Count());
  440. for (int i = 0; i < idx1.getObjectCount(); i++) {
  441. final ObjectId id = idx1.getObjectId(i);
  442. assertEquals(id, idx2.getObjectId(i));
  443. assertEquals(idx1.findOffset(id), idx2.findOffset(id));
  444. assertEquals(idx1.findCRC32(id), idx2.findCRC32(id));
  445. }
  446. }
  447. @Test
  448. public void testExclude() throws Exception {
  449. // TestRepository closes repo
  450. FileRepository repo = createBareRepository();
  451. try (TestRepository<FileRepository> testRepo = new TestRepository<>(
  452. repo)) {
  453. BranchBuilder bb = testRepo.branch("refs/heads/master");
  454. contentA = testRepo.blob("A");
  455. c1 = bb.commit().add("f", contentA).create();
  456. testRepo.getRevWalk().parseHeaders(c1);
  457. PackIndex pf1 = writePack(repo, wants(c1), EMPTY_ID_SET);
  458. assertContent(pf1, Arrays.asList(c1.getId(), c1.getTree().getId(),
  459. contentA.getId()));
  460. contentB = testRepo.blob("B");
  461. c2 = bb.commit().add("f", contentB).create();
  462. testRepo.getRevWalk().parseHeaders(c2);
  463. PackIndex pf2 = writePack(repo, wants(c2),
  464. Sets.of((ObjectIdSet) pf1));
  465. assertContent(pf2, Arrays.asList(c2.getId(), c2.getTree().getId(),
  466. contentB.getId()));
  467. }
  468. }
  469. private static void assertContent(PackIndex pi, List<ObjectId> expected) {
  470. assertEquals("Pack index has wrong size.", expected.size(),
  471. pi.getObjectCount());
  472. for (int i = 0; i < pi.getObjectCount(); i++)
  473. assertTrue(
  474. "Pack index didn't contain the expected id "
  475. + pi.getObjectId(i),
  476. expected.contains(pi.getObjectId(i)));
  477. }
  478. @Test
  479. public void testShallowIsMinimalDepth1() throws Exception {
  480. try (FileRepository repo = setupRepoForShallowFetch()) {
  481. PackIndex idx = writeShallowPack(repo, 1, wants(c2), NONE, NONE);
  482. assertContent(idx, Arrays.asList(c2.getId(), c2.getTree().getId(),
  483. contentA.getId(), contentB.getId()));
  484. // Client already has blobs A and B, verify those are not packed.
  485. idx = writeShallowPack(repo, 1, wants(c5), haves(c2), shallows(c2));
  486. assertContent(idx, Arrays.asList(c5.getId(), c5.getTree().getId(),
  487. contentC.getId(), contentD.getId(), contentE.getId()));
  488. }
  489. }
  490. @Test
  491. public void testShallowIsMinimalDepth2() throws Exception {
  492. try (FileRepository repo = setupRepoForShallowFetch()) {
  493. PackIndex idx = writeShallowPack(repo, 2, wants(c2), NONE, NONE);
  494. assertContent(idx,
  495. Arrays.asList(c1.getId(), c2.getId(), c1.getTree().getId(),
  496. c2.getTree().getId(), contentA.getId(),
  497. contentB.getId()));
  498. // Client already has blobs A and B, verify those are not packed.
  499. idx = writeShallowPack(repo, 2, wants(c5), haves(c1, c2),
  500. shallows(c1));
  501. assertContent(idx,
  502. Arrays.asList(c4.getId(), c5.getId(), c4.getTree().getId(),
  503. c5.getTree().getId(), contentC.getId(),
  504. contentD.getId(), contentE.getId()));
  505. }
  506. }
  507. @Test
  508. public void testShallowFetchShallowParentDepth1() throws Exception {
  509. try (FileRepository repo = setupRepoForShallowFetch()) {
  510. PackIndex idx = writeShallowPack(repo, 1, wants(c5), NONE, NONE);
  511. assertContent(idx, Arrays.asList(c5.getId(), c5.getTree().getId(),
  512. contentA.getId(), contentB.getId(), contentC.getId(),
  513. contentD.getId(), contentE.getId()));
  514. idx = writeShallowPack(repo, 1, wants(c4), haves(c5), shallows(c5));
  515. assertContent(idx, Arrays.asList(c4.getId(), c4.getTree().getId()));
  516. }
  517. }
  518. @Test
  519. public void testShallowFetchShallowParentDepth2() throws Exception {
  520. try (FileRepository repo = setupRepoForShallowFetch()) {
  521. PackIndex idx = writeShallowPack(repo, 2, wants(c5), NONE, NONE);
  522. assertContent(idx,
  523. Arrays.asList(c4.getId(), c5.getId(), c4.getTree().getId(),
  524. c5.getTree().getId(), contentA.getId(),
  525. contentB.getId(), contentC.getId(),
  526. contentD.getId(), contentE.getId()));
  527. idx = writeShallowPack(repo, 2, wants(c3), haves(c4, c5),
  528. shallows(c4));
  529. assertContent(idx, Arrays.asList(c2.getId(), c3.getId(),
  530. c2.getTree().getId(), c3.getTree().getId()));
  531. }
  532. }
  533. @Test
  534. public void testShallowFetchShallowAncestorDepth1() throws Exception {
  535. try (FileRepository repo = setupRepoForShallowFetch()) {
  536. PackIndex idx = writeShallowPack(repo, 1, wants(c5), NONE, NONE);
  537. assertContent(idx, Arrays.asList(c5.getId(), c5.getTree().getId(),
  538. contentA.getId(), contentB.getId(), contentC.getId(),
  539. contentD.getId(), contentE.getId()));
  540. idx = writeShallowPack(repo, 1, wants(c3), haves(c5), shallows(c5));
  541. assertContent(idx, Arrays.asList(c3.getId(), c3.getTree().getId()));
  542. }
  543. }
  544. @Test
  545. public void testShallowFetchShallowAncestorDepth2() throws Exception {
  546. try (FileRepository repo = setupRepoForShallowFetch()) {
  547. PackIndex idx = writeShallowPack(repo, 2, wants(c5), NONE, NONE);
  548. assertContent(idx,
  549. Arrays.asList(c4.getId(), c5.getId(), c4.getTree().getId(),
  550. c5.getTree().getId(), contentA.getId(),
  551. contentB.getId(), contentC.getId(),
  552. contentD.getId(), contentE.getId()));
  553. idx = writeShallowPack(repo, 2, wants(c2), haves(c4, c5),
  554. shallows(c4));
  555. assertContent(idx, Arrays.asList(c1.getId(), c2.getId(),
  556. c1.getTree().getId(), c2.getTree().getId()));
  557. }
  558. }
  559. @Test
  560. public void testTotalPackFilesScanWhenSearchForReuseTimeoutNotSet()
  561. throws Exception {
  562. FileRepository fileRepository = setUpRepoWithMultiplePackfiles();
  563. PackWriter mockedPackWriter = Mockito
  564. .spy(new PackWriter(config, fileRepository.newObjectReader()));
  565. doNothing().when(mockedPackWriter).select(any(), any());
  566. try (FileOutputStream packOS = new FileOutputStream(
  567. getPackFileToWrite(fileRepository, mockedPackWriter))) {
  568. mockedPackWriter.writePack(NullProgressMonitor.INSTANCE,
  569. NullProgressMonitor.INSTANCE, packOS);
  570. }
  571. long numberOfPackFiles = new GC(fileRepository)
  572. .getStatistics().numberOfPackFiles;
  573. int expectedSelectCalls =
  574. // Objects contained in multiple packfiles * number of packfiles
  575. 2 * (int) numberOfPackFiles +
  576. // Objects in single packfile
  577. 1;
  578. verify(mockedPackWriter, times(expectedSelectCalls)).select(any(),
  579. any());
  580. }
  581. @Test
  582. public void testTotalPackFilesScanWhenSkippingSearchForReuseTimeoutCheck()
  583. throws Exception {
  584. FileRepository fileRepository = setUpRepoWithMultiplePackfiles();
  585. PackConfig packConfig = new PackConfig();
  586. packConfig.setSearchForReuseTimeout(Duration.ofSeconds(-1));
  587. PackWriter mockedPackWriter = Mockito.spy(
  588. new PackWriter(packConfig, fileRepository.newObjectReader()));
  589. doNothing().when(mockedPackWriter).select(any(), any());
  590. try (FileOutputStream packOS = new FileOutputStream(
  591. getPackFileToWrite(fileRepository, mockedPackWriter))) {
  592. mockedPackWriter.writePack(NullProgressMonitor.INSTANCE,
  593. NullProgressMonitor.INSTANCE, packOS);
  594. }
  595. long numberOfPackFiles = new GC(fileRepository)
  596. .getStatistics().numberOfPackFiles;
  597. int expectedSelectCalls =
  598. // Objects contained in multiple packfiles * number of packfiles
  599. 2 * (int) numberOfPackFiles +
  600. // Objects contained in single packfile
  601. 1;
  602. verify(mockedPackWriter, times(expectedSelectCalls)).select(any(),
  603. any());
  604. }
  605. @Test
  606. public void testPartialPackFilesScanWhenDoingSearchForReuseTimeoutCheck()
  607. throws Exception {
  608. FileRepository fileRepository = setUpRepoWithMultiplePackfiles();
  609. PackConfig packConfig = new PackConfig();
  610. packConfig.setSearchForReuseTimeout(Duration.ofSeconds(-1));
  611. PackWriter mockedPackWriter = Mockito.spy(
  612. new PackWriter(packConfig, fileRepository.newObjectReader()));
  613. mockedPackWriter.enableSearchForReuseTimeout();
  614. doNothing().when(mockedPackWriter).select(any(), any());
  615. try (FileOutputStream packOS = new FileOutputStream(
  616. getPackFileToWrite(fileRepository, mockedPackWriter))) {
  617. mockedPackWriter.writePack(NullProgressMonitor.INSTANCE,
  618. NullProgressMonitor.INSTANCE, packOS);
  619. }
  620. int expectedSelectCalls = 3; // Objects in packfiles
  621. verify(mockedPackWriter, times(expectedSelectCalls)).select(any(),
  622. any());
  623. }
  624. /**
  625. * Creates objects and packfiles in the following order:
  626. * <ul>
  627. * <li>Creates 2 objects (C1 = commit, T1 = tree)
  628. * <li>Creates packfile P1 (containing C1, T1)
  629. * <li>Creates 1 object (C2 commit)
  630. * <li>Creates packfile P2 (containing C1, T1, C2)
  631. * <li>Create 1 object (C3 commit)
  632. * </ul>
  633. *
  634. * @throws Exception
  635. */
  636. private FileRepository setUpRepoWithMultiplePackfiles() throws Exception {
  637. FileRepository fileRepository = createWorkRepository();
  638. try (Git git = new Git(fileRepository)) {
  639. // Creates 2 objects (C1 = commit, T1 = tree)
  640. git.commit().setMessage("First commit").call();
  641. GC gc = new GC(fileRepository);
  642. gc.setPackExpireAgeMillis(Long.MAX_VALUE);
  643. gc.setExpireAgeMillis(Long.MAX_VALUE);
  644. // Creates packfile P1 (containing C1, T1)
  645. gc.gc();
  646. // Creates 1 object (C2 commit)
  647. git.commit().setMessage("Second commit").call();
  648. // Creates packfile P2 (containing C1, T1, C2)
  649. gc.gc();
  650. // Create 1 object (C3 commit)
  651. git.commit().setMessage("Third commit").call();
  652. }
  653. return fileRepository;
  654. }
  655. private PackFile getPackFileToWrite(FileRepository fileRepository,
  656. PackWriter mockedPackWriter) throws IOException {
  657. File packdir = fileRepository.getObjectDatabase().getPackDirectory();
  658. PackFile packFile = new PackFile(packdir,
  659. mockedPackWriter.computeName(), PackExt.PACK);
  660. Set<ObjectId> all = new HashSet<>();
  661. for (Ref r : fileRepository.getRefDatabase().getRefs()) {
  662. all.add(r.getObjectId());
  663. }
  664. mockedPackWriter.preparePack(NullProgressMonitor.INSTANCE, all,
  665. PackWriter.NONE);
  666. return packFile;
  667. }
  668. private FileRepository setupRepoForShallowFetch() throws Exception {
  669. FileRepository repo = createBareRepository();
  670. // TestRepository will close the repo, but we need to return an open
  671. // one!
  672. repo.incrementOpen();
  673. try (TestRepository<Repository> r = new TestRepository<>(repo)) {
  674. BranchBuilder bb = r.branch("refs/heads/master");
  675. contentA = r.blob("A");
  676. contentB = r.blob("B");
  677. contentC = r.blob("C");
  678. contentD = r.blob("D");
  679. contentE = r.blob("E");
  680. c1 = bb.commit().add("a", contentA).create();
  681. c2 = bb.commit().add("b", contentB).create();
  682. c3 = bb.commit().add("c", contentC).create();
  683. c4 = bb.commit().add("d", contentD).create();
  684. c5 = bb.commit().add("e", contentE).create();
  685. r.getRevWalk().parseHeaders(c5); // fully initialize the tip RevCommit
  686. return repo;
  687. }
  688. }
  689. private static PackIndex writePack(FileRepository repo,
  690. Set<? extends ObjectId> want, Set<ObjectIdSet> excludeObjects)
  691. throws IOException {
  692. try (RevWalk walk = new RevWalk(repo)) {
  693. return writePack(repo, walk, 0, want, NONE, excludeObjects);
  694. }
  695. }
  696. private static PackIndex writeShallowPack(FileRepository repo, int depth,
  697. Set<? extends ObjectId> want, Set<? extends ObjectId> have,
  698. Set<? extends ObjectId> shallow) throws IOException {
  699. // During negotiation, UploadPack would have set up a DepthWalk and
  700. // marked the client's "shallow" commits. Emulate that here.
  701. try (DepthWalk.RevWalk walk = new DepthWalk.RevWalk(repo, depth - 1)) {
  702. walk.assumeShallow(shallow);
  703. return writePack(repo, walk, depth, want, have, EMPTY_ID_SET);
  704. }
  705. }
  706. private static PackIndex writePack(FileRepository repo, RevWalk walk,
  707. int depth, Set<? extends ObjectId> want,
  708. Set<? extends ObjectId> have, Set<ObjectIdSet> excludeObjects)
  709. throws IOException {
  710. try (PackWriter pw = new PackWriter(repo)) {
  711. pw.setDeltaBaseAsOffset(true);
  712. pw.setReuseDeltaCommits(false);
  713. for (ObjectIdSet idx : excludeObjects) {
  714. pw.excludeObjects(idx);
  715. }
  716. if (depth > 0) {
  717. pw.setShallowPack(depth, null);
  718. }
  719. // ow doesn't need to be closed; caller closes walk.
  720. ObjectWalk ow = walk.toObjectWalkWithSameObjects();
  721. pw.preparePack(NullProgressMonitor.INSTANCE, ow, want, have, NONE);
  722. File packdir = repo.getObjectDatabase().getPackDirectory();
  723. PackFile packFile = new PackFile(packdir, pw.computeName(),
  724. PackExt.PACK);
  725. try (FileOutputStream packOS = new FileOutputStream(packFile)) {
  726. pw.writePack(NullProgressMonitor.INSTANCE,
  727. NullProgressMonitor.INSTANCE, packOS);
  728. }
  729. PackFile idxFile = packFile.create(PackExt.INDEX);
  730. try (FileOutputStream idxOS = new FileOutputStream(idxFile)) {
  731. pw.writeIndex(idxOS);
  732. }
  733. return PackIndex.open(idxFile);
  734. }
  735. }
  736. // TODO: testWritePackDeltasCycle()
  737. // TODO: testWritePackDeltasDepth()
  738. private void writeVerifyPack1() throws IOException {
  739. final HashSet<ObjectId> interestings = new HashSet<>();
  740. interestings.add(ObjectId
  741. .fromString("82c6b885ff600be425b4ea96dee75dca255b69e7"));
  742. createVerifyOpenPack(interestings, NONE, false, false);
  743. final ObjectId expectedOrder[] = new ObjectId[] {
  744. ObjectId.fromString("82c6b885ff600be425b4ea96dee75dca255b69e7"),
  745. ObjectId.fromString("c59759f143fb1fe21c197981df75a7ee00290799"),
  746. ObjectId.fromString("540a36d136cf413e4b064c2b0e0a4db60f77feab"),
  747. ObjectId.fromString("aabf2ffaec9b497f0950352b3e582d73035c2035"),
  748. ObjectId.fromString("902d5476fa249b7abc9d84c611577a81381f0327"),
  749. ObjectId.fromString("4b825dc642cb6eb9a060e54bf8d69288fbee4904"),
  750. ObjectId.fromString("6ff87c4664981e4397625791c8ea3bbb5f2279a3"),
  751. ObjectId.fromString("5b6e7c66c276e7610d4a73c70ec1a1f7c1003259") };
  752. assertEquals(expectedOrder.length, writer.getObjectCount());
  753. verifyObjectsOrder(expectedOrder);
  754. assertEquals("34be9032ac282b11fa9babdc2b2a93ca996c9c2f", writer
  755. .computeName().name());
  756. }
  757. private void writeVerifyPack2(boolean deltaReuse) throws IOException {
  758. config.setReuseDeltas(deltaReuse);
  759. final HashSet<ObjectId> interestings = new HashSet<>();
  760. interestings.add(ObjectId
  761. .fromString("82c6b885ff600be425b4ea96dee75dca255b69e7"));
  762. final HashSet<ObjectId> uninterestings = new HashSet<>();
  763. uninterestings.add(ObjectId
  764. .fromString("540a36d136cf413e4b064c2b0e0a4db60f77feab"));
  765. createVerifyOpenPack(interestings, uninterestings, false, false);
  766. final ObjectId expectedOrder[] = new ObjectId[] {
  767. ObjectId.fromString("82c6b885ff600be425b4ea96dee75dca255b69e7"),
  768. ObjectId.fromString("c59759f143fb1fe21c197981df75a7ee00290799"),
  769. ObjectId.fromString("aabf2ffaec9b497f0950352b3e582d73035c2035"),
  770. ObjectId.fromString("902d5476fa249b7abc9d84c611577a81381f0327"),
  771. ObjectId.fromString("6ff87c4664981e4397625791c8ea3bbb5f2279a3") ,
  772. ObjectId.fromString("5b6e7c66c276e7610d4a73c70ec1a1f7c1003259") };
  773. if (!config.isReuseDeltas() && !config.isDeltaCompress()) {
  774. // If no deltas are in the file the final two entries swap places.
  775. swap(expectedOrder, 4, 5);
  776. }
  777. assertEquals(expectedOrder.length, writer.getObjectCount());
  778. verifyObjectsOrder(expectedOrder);
  779. assertEquals("ed3f96b8327c7c66b0f8f70056129f0769323d86", writer
  780. .computeName().name());
  781. }
  782. private static void swap(ObjectId[] arr, int a, int b) {
  783. ObjectId tmp = arr[a];
  784. arr[a] = arr[b];
  785. arr[b] = tmp;
  786. }
  787. private void writeVerifyPack4(final boolean thin) throws IOException {
  788. final HashSet<ObjectId> interestings = new HashSet<>();
  789. interestings.add(ObjectId
  790. .fromString("82c6b885ff600be425b4ea96dee75dca255b69e7"));
  791. final HashSet<ObjectId> uninterestings = new HashSet<>();
  792. uninterestings.add(ObjectId
  793. .fromString("c59759f143fb1fe21c197981df75a7ee00290799"));
  794. createVerifyOpenPack(interestings, uninterestings, thin, false);
  795. final ObjectId writtenObjects[] = new ObjectId[] {
  796. ObjectId.fromString("82c6b885ff600be425b4ea96dee75dca255b69e7"),
  797. ObjectId.fromString("aabf2ffaec9b497f0950352b3e582d73035c2035"),
  798. ObjectId.fromString("5b6e7c66c276e7610d4a73c70ec1a1f7c1003259") };
  799. assertEquals(writtenObjects.length, writer.getObjectCount());
  800. ObjectId expectedObjects[];
  801. if (thin) {
  802. expectedObjects = new ObjectId[4];
  803. System.arraycopy(writtenObjects, 0, expectedObjects, 0,
  804. writtenObjects.length);
  805. expectedObjects[3] = ObjectId
  806. .fromString("6ff87c4664981e4397625791c8ea3bbb5f2279a3");
  807. } else {
  808. expectedObjects = writtenObjects;
  809. }
  810. verifyObjectsOrder(expectedObjects);
  811. assertEquals("cded4b74176b4456afa456768b2b5aafb41c44fc", writer
  812. .computeName().name());
  813. }
  814. private void createVerifyOpenPack(final Set<ObjectId> interestings,
  815. final Set<ObjectId> uninterestings, final boolean thin,
  816. final boolean ignoreMissingUninteresting)
  817. throws MissingObjectException, IOException {
  818. createVerifyOpenPack(interestings, uninterestings, thin,
  819. ignoreMissingUninteresting, false);
  820. }
  821. private void createVerifyOpenPack(final Set<ObjectId> interestings,
  822. final Set<ObjectId> uninterestings, final boolean thin,
  823. final boolean ignoreMissingUninteresting, boolean useBitmaps)
  824. throws MissingObjectException, IOException {
  825. NullProgressMonitor m = NullProgressMonitor.INSTANCE;
  826. writer = new PackWriter(config, db.newObjectReader());
  827. writer.setUseBitmaps(useBitmaps);
  828. writer.setThin(thin);
  829. writer.setIgnoreMissingUninteresting(ignoreMissingUninteresting);
  830. writer.preparePack(m, interestings, uninterestings);
  831. writer.writePack(m, m, os);
  832. writer.close();
  833. verifyOpenPack(thin);
  834. }
  835. private void createVerifyOpenPack(List<RevObject> objectSource)
  836. throws MissingObjectException, IOException {
  837. NullProgressMonitor m = NullProgressMonitor.INSTANCE;
  838. writer = new PackWriter(config, db.newObjectReader());
  839. writer.preparePack(objectSource.iterator());
  840. assertEquals(objectSource.size(), writer.getObjectCount());
  841. writer.writePack(m, m, os);
  842. writer.close();
  843. verifyOpenPack(false);
  844. }
  845. private void verifyOpenPack(boolean thin) throws IOException {
  846. final byte[] packData = os.toByteArray();
  847. if (thin) {
  848. PackParser p = index(packData);
  849. try {
  850. p.parse(NullProgressMonitor.INSTANCE);
  851. fail("indexer should grumble about missing object");
  852. } catch (IOException x) {
  853. // expected
  854. }
  855. }
  856. ObjectDirectoryPackParser p = (ObjectDirectoryPackParser) index(packData);
  857. p.setKeepEmpty(true);
  858. p.setAllowThin(thin);
  859. p.setIndexVersion(2);
  860. p.parse(NullProgressMonitor.INSTANCE);
  861. pack = p.getPack();
  862. assertNotNull("have PackFile after parsing", pack);
  863. }
  864. private PackParser index(byte[] packData) throws IOException {
  865. if (inserter == null)
  866. inserter = dst.newObjectInserter();
  867. return inserter.newPackParser(new ByteArrayInputStream(packData));
  868. }
  869. private void verifyObjectsOrder(ObjectId objectsOrder[]) {
  870. final List<PackIndex.MutableEntry> entries = new ArrayList<>();
  871. for (MutableEntry me : pack) {
  872. entries.add(me.cloneEntry());
  873. }
  874. Collections.sort(entries, (MutableEntry o1, MutableEntry o2) -> Long
  875. .signum(o1.getOffset() - o2.getOffset()));
  876. int i = 0;
  877. for (MutableEntry me : entries) {
  878. assertEquals(objectsOrder[i++].toObjectId(), me.toObjectId());
  879. }
  880. }
  881. private static Set<ObjectId> haves(ObjectId... objects) {
  882. return Sets.of(objects);
  883. }
  884. private static Set<ObjectId> wants(ObjectId... objects) {
  885. return Sets.of(objects);
  886. }
  887. private static Set<ObjectId> shallows(ObjectId... objects) {
  888. return Sets.of(objects);
  889. }
  890. }