summaryrefslogtreecommitdiffstats
path: root/project.py
diff options
context:
space:
mode:
authorGavin Mak <gavinmak@google.com>2026-02-06 14:18:00 -0800
committerLUCI <gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com>2026-03-17 14:29:38 -0700
commitf14c577fce155bc4955e83b1d9444dc67f1511aa (patch)
tree9ddceaa0b2e7d1062b65404249c8c6ac3534ff4f /project.py
parent67881c0c3b27d850c57070919f4476b370de7768 (diff)
downloadgit-repo-f14c577fce155bc4955e83b1d9444dc67f1511aa.tar.gz
project: avoid direct packed-refs writes during fetch
Replace raw file manipulation with native `git update-ref` commands inside a try/finally block to ensure temp refs are created/cleaned up regardless of storage format. Bug: 476209856 Change-Id: I228e81d3d3b323328260f6672075193421c8dc47 Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/550421 Tested-by: Gavin Mak <gavinmak@google.com> Commit-Queue: Gavin Mak <gavinmak@google.com> Reviewed-by: Mike Frysinger <vapier@google.com>
Diffstat (limited to 'project.py')
-rw-r--r--project.py311
1 files changed, 161 insertions, 150 deletions
diff --git a/project.py b/project.py
index f3ae9ab1d..b3c906b18 100644
--- a/project.py
+++ b/project.py
@@ -2516,18 +2516,20 @@ class Project:
2516 if not remote.PreConnectFetch(ssh_proxy): 2516 if not remote.PreConnectFetch(ssh_proxy):
2517 ssh_proxy = None 2517 ssh_proxy = None
2518 2518
2519 alt_tmp_refs = []
2519 if initial: 2520 if initial:
2520 if alt_dir and "objects" == os.path.basename(alt_dir): 2521 if alt_dir and "objects" == os.path.basename(alt_dir):
2521 ref_dir = os.path.dirname(alt_dir) 2522 ref_dir = os.path.dirname(alt_dir)
2522 packed_refs = os.path.join(self.gitdir, "packed-refs")
2523 2523
2524 all_refs = self.bare_ref.all 2524 all_refs = self.bare_ref.all
2525 ids = set(all_refs.values()) 2525 ids = set(all_refs.values())
2526 tmp = set() 2526
2527 update_ref_cmds = []
2527 2528
2528 for r, ref_id in GitRefs(ref_dir).all.items(): 2529 for r, ref_id in GitRefs(ref_dir).all.items():
2529 if r not in all_refs: 2530 if r not in all_refs:
2530 if r.startswith(R_TAGS) or remote.WritesTo(r): 2531 if r.startswith(R_TAGS) or remote.WritesTo(r):
2532 update_ref_cmds.append(f"create {r} {ref_id}\n")
2531 all_refs[r] = ref_id 2533 all_refs[r] = ref_id
2532 ids.add(ref_id) 2534 ids.add(ref_id)
2533 continue 2535 continue
@@ -2536,22 +2538,18 @@ class Project:
2536 continue 2538 continue
2537 2539
2538 r = "refs/_alt/%s" % ref_id 2540 r = "refs/_alt/%s" % ref_id
2541 update_ref_cmds.append(f"create {r} {ref_id}\n")
2539 all_refs[r] = ref_id 2542 all_refs[r] = ref_id
2540 ids.add(ref_id) 2543 ids.add(ref_id)
2541 tmp.add(r) 2544 alt_tmp_refs.append(r)
2542 2545
2543 tmp_packed_lines = [] 2546 if update_ref_cmds:
2544 old_packed_lines = [] 2547 GitCommand(
2545 2548 self,
2546 for r in sorted(all_refs): 2549 ["update-ref", "--no-deref", "--stdin"],
2547 line = f"{all_refs[r]} {r}\n" 2550 bare=True,
2548 tmp_packed_lines.append(line) 2551 input="".join(update_ref_cmds),
2549 if r not in tmp: 2552 ).Wait()
2550 old_packed_lines.append(line)
2551
2552 tmp_packed = "".join(tmp_packed_lines)
2553 old_packed = "".join(old_packed_lines)
2554 _lwrite(packed_refs, tmp_packed)
2555 else: 2553 else:
2556 alt_dir = None 2554 alt_dir = None
2557 2555
@@ -2652,147 +2650,160 @@ class Project:
2652 retry_fetches = max(retry_fetches, 2) 2650 retry_fetches = max(retry_fetches, 2)
2653 retry_cur_sleep = retry_sleep_initial_sec 2651 retry_cur_sleep = retry_sleep_initial_sec
2654 ok = prune_tried = False 2652 ok = prune_tried = False
2655 for try_n in range(retry_fetches): 2653 try:
2656 verify_command = try_n == retry_fetches - 1 2654 for try_n in range(retry_fetches):
2657 gitcmd = GitCommand( 2655 verify_command = try_n == retry_fetches - 1
2658 self, 2656 gitcmd = GitCommand(
2659 cmd,
2660 bare=True,
2661 objdir=os.path.join(self.objdir, "objects"),
2662 ssh_proxy=ssh_proxy,
2663 merge_output=True,
2664 capture_stdout=quiet or bool(output_redir),
2665 verify_command=verify_command,
2666 )
2667 if gitcmd.stdout and not quiet and output_redir:
2668 output_redir.write(gitcmd.stdout)
2669 ret = gitcmd.Wait()
2670 if ret == 0:
2671 ok = True
2672 break
2673
2674 # Retry later due to HTTP 429 Too Many Requests.
2675 elif (
2676 gitcmd.stdout
2677 and "error:" in gitcmd.stdout
2678 and "HTTP 429" in gitcmd.stdout
2679 ):
2680 # Fallthru to sleep+retry logic at the bottom.
2681 pass
2682
2683 # TODO(b/360889369#comment24): git may gc commits incorrectly.
2684 # Until the root cause is fixed, retry fetch with --refetch which
2685 # will bring the repository into a good state.
2686 elif gitcmd.stdout and (
2687 "could not parse commit" in gitcmd.stdout
2688 or "unable to parse commit" in gitcmd.stdout
2689 ):
2690 cmd.insert(1, "--refetch")
2691 print(
2692 "could not parse commit error, retrying with refetch",
2693 file=output_redir,
2694 )
2695 continue
2696
2697 # Try to prune remote branches once in case there are conflicts.
2698 # For example, if the remote had refs/heads/upstream, but deleted
2699 # that and now has refs/heads/upstream/foo.
2700 elif (
2701 gitcmd.stdout
2702 and "error:" in gitcmd.stdout
2703 and "git remote prune" in gitcmd.stdout
2704 and not prune_tried
2705 ):
2706 prune_tried = True
2707 prunecmd = GitCommand(
2708 self, 2657 self,
2709 ["remote", "prune", name], 2658 cmd,
2710 bare=True, 2659 bare=True,
2660 objdir=os.path.join(self.objdir, "objects"),
2711 ssh_proxy=ssh_proxy, 2661 ssh_proxy=ssh_proxy,
2662 merge_output=True,
2663 capture_stdout=quiet or bool(output_redir),
2664 verify_command=verify_command,
2712 ) 2665 )
2713 ret = prunecmd.Wait() 2666 if gitcmd.stdout and not quiet and output_redir:
2714 if ret: 2667 output_redir.write(gitcmd.stdout)
2668 ret = gitcmd.Wait()
2669 if ret == 0:
2670 ok = True
2715 break 2671 break
2716 print(
2717 "retrying fetch after pruning remote branches",
2718 file=output_redir,
2719 )
2720 # Continue right away so we don't sleep as we shouldn't need to.
2721 continue
2722 elif (
2723 ret == 128
2724 and gitcmd.stdout
2725 and "fatal: could not read Username" in gitcmd.stdout
2726 ):
2727 # User needs to be authenticated, and Git wants to prompt for
2728 # username and password.
2729 print(
2730 "git requires authentication, but repo cannot perform "
2731 "interactive authentication. Check git credentials.",
2732 file=output_redir,
2733 )
2734 break
2735 elif (
2736 ret == 128
2737 and gitcmd.stdout
2738 and "remote helper 'sso' aborted session" in gitcmd.stdout
2739 ):
2740 # User needs to be authenticated, and Git wants to prompt for
2741 # username and password.
2742 print(
2743 "git requires authentication, but repo cannot perform "
2744 "interactive authentication.",
2745 file=output_redir,
2746 )
2747 raise GitAuthError(gitcmd.stdout)
2748 break
2749 elif current_branch_only and is_sha1 and ret == 128:
2750 # Exit code 128 means "couldn't find the ref you asked for"; if
2751 # we're in sha1 mode, we just tried sync'ing from the upstream
2752 # field; it doesn't exist, thus abort the optimization attempt
2753 # and do a full sync.
2754 break
2755 elif depth and is_sha1 and ret == 1:
2756 # In sha1 mode, when depth is enabled, syncing the revision
2757 # from upstream may not work because some servers only allow
2758 # fetching named refs. Fetching a specific sha1 may result
2759 # in an error like 'server does not allow request for
2760 # unadvertised object'. In this case, attempt a full sync
2761 # without depth.
2762 break
2763 elif ret < 0:
2764 # Git died with a signal, exit immediately.
2765 break
2766 2672
2767 # Figure out how long to sleep before the next attempt, if there is 2673 # Retry later due to HTTP 429 Too Many Requests.
2768 # one. 2674 elif (
2769 if not verbose and gitcmd.stdout: 2675 gitcmd.stdout
2770 print( 2676 and "error:" in gitcmd.stdout
2771 f"\n{self.name}:\n{gitcmd.stdout}", 2677 and "HTTP 429" in gitcmd.stdout
2772 end="", 2678 ):
2773 file=output_redir, 2679 # Fallthru to sleep+retry logic at the bottom.
2774 ) 2680 pass
2775 if try_n < retry_fetches - 1:
2776 print(
2777 "%s: sleeping %s seconds before retrying"
2778 % (self.name, retry_cur_sleep),
2779 file=output_redir,
2780 )
2781 time.sleep(retry_cur_sleep)
2782 retry_cur_sleep = min(
2783 retry_exp_factor * retry_cur_sleep, MAXIMUM_RETRY_SLEEP_SEC
2784 )
2785 retry_cur_sleep *= 1 - random.uniform(
2786 -RETRY_JITTER_PERCENT, RETRY_JITTER_PERCENT
2787 )
2788 2681
2789 if initial: 2682 # TODO(b/360889369#comment24): git may gc commits incorrectly.
2790 if alt_dir: 2683 # Until the root cause is fixed, retry fetch with --refetch
2791 if old_packed != "": 2684 # which will bring the repository into a good state.
2792 _lwrite(packed_refs, old_packed) 2685 elif gitcmd.stdout and (
2793 else: 2686 "could not parse commit" in gitcmd.stdout
2794 platform_utils.remove(packed_refs) 2687 or "unable to parse commit" in gitcmd.stdout
2795 self.bare_git.pack_refs("--all", "--prune") 2688 ):
2689 cmd.insert(1, "--refetch")
2690 print(
2691 "could not parse commit error, retrying with refetch",
2692 file=output_redir,
2693 )
2694 continue
2695
2696 # Try to prune remote branches once in case there are conflicts.
2697 # For example, if the remote had refs/heads/upstream, but
2698 # deleted that and now has refs/heads/upstream/foo.
2699 elif (
2700 gitcmd.stdout
2701 and "error:" in gitcmd.stdout
2702 and "git remote prune" in gitcmd.stdout
2703 and not prune_tried
2704 ):
2705 prune_tried = True
2706 prunecmd = GitCommand(
2707 self,
2708 ["remote", "prune", name],
2709 bare=True,
2710 ssh_proxy=ssh_proxy,
2711 )
2712 ret = prunecmd.Wait()
2713 if ret:
2714 break
2715 print(
2716 "retrying fetch after pruning remote branches",
2717 file=output_redir,
2718 )
2719 # Continue right away so we don't sleep as we shouldn't
2720 # need to.
2721 continue
2722 elif (
2723 ret == 128
2724 and gitcmd.stdout
2725 and "fatal: could not read Username" in gitcmd.stdout
2726 ):
2727 # User needs to be authenticated, and Git wants to prompt
2728 # for username and password.
2729 print(
2730 "git requires authentication, but repo cannot perform "
2731 "interactive authentication. Check git credentials.",
2732 file=output_redir,
2733 )
2734 break
2735 elif (
2736 ret == 128
2737 and gitcmd.stdout
2738 and "remote helper 'sso' aborted session" in gitcmd.stdout
2739 ):
2740 # User needs to be authenticated, and Git wants to prompt
2741 # for username and password.
2742 print(
2743 "git requires authentication, but repo cannot perform "
2744 "interactive authentication.",
2745 file=output_redir,
2746 )
2747 raise GitAuthError(gitcmd.stdout)
2748 break
2749 elif current_branch_only and is_sha1 and ret == 128:
2750 # Exit code 128 means "couldn't find the ref you asked for";
2751 # if we're in sha1 mode, we just tried sync'ing from the
2752 # upstream field; it doesn't exist, thus abort the
2753 # optimization attempt and do a full sync.
2754 break
2755 elif depth and is_sha1 and ret == 1:
2756 # In sha1 mode, when depth is enabled, syncing the revision
2757 # from upstream may not work because some servers only allow
2758 # fetching named refs. Fetching a specific sha1 may result
2759 # in an error like 'server does not allow request for
2760 # unadvertised object'. In this case, attempt a full sync
2761 # without depth.
2762 break
2763 elif ret < 0:
2764 # Git died with a signal, exit immediately.
2765 break
2766
2767 # Figure out how long to sleep before the next attempt, if
2768 # there is one.
2769 if not verbose and gitcmd.stdout:
2770 print(
2771 f"\n{self.name}:\n{gitcmd.stdout}",
2772 end="",
2773 file=output_redir,
2774 )
2775 if try_n < retry_fetches - 1:
2776 print(
2777 "%s: sleeping %s seconds before retrying"
2778 % (self.name, retry_cur_sleep),
2779 file=output_redir,
2780 )
2781 time.sleep(retry_cur_sleep)
2782 retry_cur_sleep = min(
2783 retry_exp_factor * retry_cur_sleep,
2784 MAXIMUM_RETRY_SLEEP_SEC,
2785 )
2786 retry_cur_sleep *= 1 - random.uniform(
2787 -RETRY_JITTER_PERCENT, RETRY_JITTER_PERCENT
2788 )
2789 finally:
2790 if initial:
2791 if alt_tmp_refs:
2792 delete_cmds = "".join(
2793 f"delete {ref}\n" for ref in alt_tmp_refs
2794 )
2795 GitCommand(
2796 self,
2797 ["update-ref", "--no-deref", "--stdin"],
2798 bare=True,
2799 input=delete_cmds,
2800 log_as_error=False,
2801 ).Wait()
2802
2803 for ref in alt_tmp_refs:
2804 self.bare_ref.deleted(ref)
2805
2806 self.bare_git.pack_refs("--all", "--prune")
2796 2807
2797 if is_sha1 and current_branch_only: 2808 if is_sha1 and current_branch_only:
2798 # We just synced the upstream given branch; verify we 2809 # We just synced the upstream given branch; verify we