chenwei791129 opened a new pull request, #9190:
URL: https://github.com/apache/devlake/pull/9190

   ### Summary
   
   Fixes a silent, permanent data gap in `gitextractor` incremental collection. 
Commits that are clearly inside the collection window can be dropped when a 
merge request brings in a branch whose head is older than the previous run. 
After that, no later incremental run picks them up. Details and a standalone 
reproduction are in #9189.
   
   #### What goes wrong today
   
   History on the default branch; the previous run started at 12:00, so this 
run fetches with `since = 12:00`:
   
   ```
               12:00 = since (previous run start)
                    |
     R ------------ | -- Q ------ P ------ M      main
     (01-01)        |   13:00    14:00    16:00 (merge commit)
      \             |                     /
       F ---------- | --------------------+       feature (head F at 10:00,
      10:00         |                              collected by the previous 
run)
   ```
   
   This run should store `Q`, `P` and `M`. What actually happens:
   
   1. `git fetch --shallow-since=12:00` stops at `M`. Git decides "shallow or 
not" per commit: since one parent of `M` (`F`) is older than 12:00, git cuts 
`M` off from **all** its parents, so `P` and `Q` are not fetched even though 
they are newer than 12:00.
   2. The single `git fetch --deepen=1` adds one generation: `P` arrives, but 
its parent `Q` does not.
   
      ```
      local clone:   P (parent Q missing) -- M        R -- F
      ```
   
   3. `CollectCommits()` skips any commit whose first parent is not in the 
local clone (`skip commit ... because it has no parent commit`), so `P` is not 
stored. `Q` was never fetched.
   4. The next run starts from this run's start time, so `P` and `Q` are now 
"old" and are never fetched again.
   
   When a lost commit is a deployment head, `refdiff` compares the next 
deployment against an incomplete graph and old PRs inflate DORA Lead Time for 
Changes.
   
   #### The change
   
   After the existing `--deepen=1`, keep fetching more history for as long as 
the newest shallow boundary commit is still newer than `since`. Each extra 
round deepens by twice as many generations as the previous one (1, 2, 4, ...), 
with a cap of 10 extra rounds.
   
   ```
   after --deepen=1:     P -- M              boundary P (14:00) is newer than 
since
                                             -> deepen by 1, then 2, 4, ... 
generations
   after extra rounds:   R -- Q -- P -- M    newest boundary commit is older 
than since -> stop
   ```
   
   When the loop ends, every boundary commit is older than `since`, so it was 
either stored by an earlier run or is outside the collection window (first sync 
with `timeAfter`). The existing "no parent commit" skip then only affects those 
commits, which is the case #7720 introduced it for. The first sync with 
`timeAfter` goes through the same path and benefits as well.
   
   Details:
   
   - The boundary is read from the bare clone's `shallow` file. Git can list 
boundary commits there that it did not fetch, and `git log` fails on those 
(`--ignore-missing` does not help for full object ids), so the commits are 
first filtered with `git cat-file --batch-check`.
   - Failures in the new step are logged as warnings and never fail the 
subtask, so the worst case is the current behaviour.
   - Cost: normally zero or a few extra fetches. The depth doubles each round 
and the cap is 10 rounds (up to 1023 extra generations), so the cost is 
bounded. Each round deepens all boundary commits, including old branch tips, so 
busy repositories fetch some extra old history. If the cap is hit, a warning is 
logged and the behaviour is the same as before this change.
   - Partial clones: with `SkipCommitStat`, the clone uses 
`--filter=blob:none`. In such a clone, looking up a missing object makes git 
fetch it from origin. The boundary lookup sets `GIT_NO_LAZY_FETCH=1` to prevent 
that where git supports it (git 2.45+ and some backports, for example Debian's 
2.39.5). With older git (such as 2.30.2 in the CI builder image) the lookup may 
trigger that fetch, which uses the same auth and proxy settings as the other 
fetches.
   
   #### Not changed
   
   - Commits that already went missing are not backfilled; a full sync (Full 
Refresh) recovers them.
   - Commits whose committer date is older than the previous run but that were 
pushed after it (for example, pushed hours after being committed) are still 
subject to the time-based `--shallow-since` window. That is a separate 
limitation.
   - Data sources with `NoShallowClone` are not covered. `doubleClone` removes 
the intermediate clone, which is the fetch remote, before the deepen steps run. 
This is pre-existing and also affects the existing `--deepen=1`.
   
   ### Does this close any open issues?
   
   Closes #9189
   
   ### Screenshots
   
   N/A. New unit tests (they need the `git` CLI, which the CI builder image 
has):
   
   - `TestIncrementalCloneKeepsCommitsBehindMergeBoundary` builds the history 
above and checks that `Q`, `P`, `M` and `Q`'s parent are all fetched. It fails 
without the fix (`Q should be fetched`).
   - `TestNewestShallowCommitTimeIgnoresMissingCommits` covers a shallow file 
that lists a commit that was not fetched, and a repo without a `shallow` file 
(like a full clone).
   
   ### Other Information
   
   - The same issue affects the go-git and libgit2 collectors, because both 
skip commits without their first parent; the fix is in the shared clone step.
   - The bug reproduces with git 2.39.5 (the git in the 
`apache/devlake:v1.0.3-beta15` image) and 2.56.0. The new tests pass with git 
2.30.2 (CI builder image) and 2.39.5 (with `-race` on 2.39.5).
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to