diff options
| author | Gavin Mak <gavinmak@google.com> | 2026-02-06 14:55:34 -0800 |
|---|---|---|
| committer | LUCI <gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com> | 2026-03-17 14:28:39 -0700 |
| commit | 67881c0c3b27d850c57070919f4476b370de7768 (patch) | |
| tree | 677767437a0246e0ae6a7d6e3ca84adfc4cf72c6 /git_refs.py | |
| parent | 551087cd980976775470cde5f7ede264a90b5349 (diff) | |
| download | git-repo-67881c0c3b27d850c57070919f4476b370de7768.tar.gz | |
git_refs: read refs via git plumbing for files/reftable
Replace direct `packed-refs` file parsing with `git for-each-ref`
plumbing to support both `files` and `reftable` backends.
Bug: 476209856
Change-Id: I2ad8ff8f3382426600f15370c997f9bc17165485
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/550401
Commit-Queue: Gavin Mak <gavinmak@google.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Gavin Mak <gavinmak@google.com>
Diffstat (limited to 'git_refs.py')
| -rw-r--r-- | git_refs.py | 141 |
1 files changed, 86 insertions, 55 deletions
diff --git a/git_refs.py b/git_refs.py index 237c15ae2..e3da103a0 100644 --- a/git_refs.py +++ b/git_refs.py | |||
| @@ -14,6 +14,7 @@ | |||
| 14 | 14 | ||
| 15 | import os | 15 | import os |
| 16 | 16 | ||
| 17 | from git_command import GitCommand | ||
| 17 | import platform_utils | 18 | import platform_utils |
| 18 | from repo_trace import Trace | 19 | from repo_trace import Trace |
| 19 | 20 | ||
| @@ -86,9 +87,8 @@ class GitRefs: | |||
| 86 | self._symref = {} | 87 | self._symref = {} |
| 87 | self._mtime = {} | 88 | self._mtime = {} |
| 88 | 89 | ||
| 89 | self._ReadPackedRefs() | 90 | self._ReadRefs() |
| 90 | self._ReadLoose("refs/") | 91 | self._ReadSymbolicRef(HEAD) |
| 91 | self._ReadLoose1(os.path.join(self._gitdir, HEAD), HEAD) | ||
| 92 | 92 | ||
| 93 | scan = self._symref | 93 | scan = self._symref |
| 94 | attempts = 0 | 94 | attempts = 0 |
| @@ -102,64 +102,95 @@ class GitRefs: | |||
| 102 | scan = scan_next | 102 | scan = scan_next |
| 103 | attempts += 1 | 103 | attempts += 1 |
| 104 | 104 | ||
| 105 | def _ReadPackedRefs(self): | 105 | self._TrackMtime(HEAD) |
| 106 | path = os.path.join(self._gitdir, "packed-refs") | 106 | self._TrackMtime("config") |
| 107 | try: | 107 | self._TrackMtime("packed-refs") |
| 108 | fd = open(path) | 108 | self._TrackTreeMtimes("refs") |
| 109 | mtime = os.path.getmtime(path) | 109 | self._TrackTreeMtimes("reftable") |
| 110 | except OSError: | 110 | |
| 111 | @staticmethod | ||
| 112 | def _IsNullRef(ref_id: str) -> bool: | ||
| 113 | """Check if a ref_id is a null object ID.""" | ||
| 114 | return ref_id and all(ch == "0" for ch in ref_id) | ||
| 115 | |||
| 116 | def _ReadRefs(self) -> None: | ||
| 117 | """Read all references using git for-each-ref.""" | ||
| 118 | p = GitCommand( | ||
| 119 | None, | ||
| 120 | ["for-each-ref", "--format=%(objectname)%00%(refname)%00%(symref)"], | ||
| 121 | capture_stdout=True, | ||
| 122 | capture_stderr=True, | ||
| 123 | bare=True, | ||
| 124 | gitdir=self._gitdir, | ||
| 125 | ) | ||
| 126 | if p.Wait() != 0: | ||
| 111 | return | 127 | return |
| 112 | try: | ||
| 113 | for line in fd: | ||
| 114 | line = str(line) | ||
| 115 | if line[0] == "#": | ||
| 116 | continue | ||
| 117 | if line[0] == "^": | ||
| 118 | continue | ||
| 119 | |||
| 120 | line = line[:-1] | ||
| 121 | p = line.split(" ") | ||
| 122 | ref_id = p[0] | ||
| 123 | name = p[1] | ||
| 124 | 128 | ||
| 129 | for line in p.stdout.splitlines(): | ||
| 130 | ref_id, name, symref = line.split("\0") | ||
| 131 | if symref: | ||
| 132 | self._symref[name] = symref | ||
| 133 | elif ref_id and not self._IsNullRef(ref_id): | ||
| 134 | self._phyref[name] = ref_id | ||
| 135 | |||
| 136 | def _ReadSymbolicRef(self, name: str) -> None: | ||
| 137 | """Read a symbolic reference.""" | ||
| 138 | p = GitCommand( | ||
| 139 | None, | ||
| 140 | ["symbolic-ref", "-q", name], | ||
| 141 | capture_stdout=True, | ||
| 142 | capture_stderr=True, | ||
| 143 | bare=True, | ||
| 144 | gitdir=self._gitdir, | ||
| 145 | ) | ||
| 146 | if p.Wait() == 0: | ||
| 147 | ref = p.stdout.strip() | ||
| 148 | if ref: | ||
| 149 | self._symref[name] = ref | ||
| 150 | return | ||
| 151 | |||
| 152 | p = GitCommand( | ||
| 153 | None, | ||
| 154 | ["rev-parse", "--verify", "-q", name], | ||
| 155 | capture_stdout=True, | ||
| 156 | capture_stderr=True, | ||
| 157 | bare=True, | ||
| 158 | gitdir=self._gitdir, | ||
| 159 | ) | ||
| 160 | if p.Wait() == 0: | ||
| 161 | ref_id = p.stdout.strip() | ||
| 162 | if ref_id: | ||
| 125 | self._phyref[name] = ref_id | 163 | self._phyref[name] = ref_id |
| 126 | finally: | 164 | |
| 127 | fd.close() | 165 | def _TrackMtime(self, name: str) -> None: |
| 128 | self._mtime["packed-refs"] = mtime | 166 | """Track the modification time of a specific gitdir path.""" |
| 129 | 167 | path = os.path.join(self._gitdir, name) | |
| 130 | def _ReadLoose(self, prefix): | ||
| 131 | base = os.path.join(self._gitdir, prefix) | ||
| 132 | for name in platform_utils.listdir(base): | ||
| 133 | p = os.path.join(base, name) | ||
| 134 | # We don't implement the full ref validation algorithm, just the | ||
| 135 | # simple rules that would show up in local filesystems. | ||
| 136 | # https://git-scm.com/docs/git-check-ref-format | ||
| 137 | if name.startswith(".") or name.endswith(".lock"): | ||
| 138 | pass | ||
| 139 | elif platform_utils.isdir(p): | ||
| 140 | self._mtime[prefix] = os.path.getmtime(base) | ||
| 141 | self._ReadLoose(prefix + name + "/") | ||
| 142 | else: | ||
| 143 | self._ReadLoose1(p, prefix + name) | ||
| 144 | |||
| 145 | def _ReadLoose1(self, path, name): | ||
| 146 | try: | 168 | try: |
| 147 | with open(path) as fd: | 169 | self._mtime[name] = os.path.getmtime(path) |
| 148 | mtime = os.path.getmtime(path) | 170 | except OSError: |
| 149 | ref_id = fd.readline() | ||
| 150 | except (OSError, UnicodeError): | ||
| 151 | return | 171 | return |
| 152 | 172 | ||
| 173 | def _TrackTreeMtimes(self, root: str) -> None: | ||
| 174 | """Recursively track modification times for a directory tree.""" | ||
| 175 | root_path = os.path.join(self._gitdir, root) | ||
| 153 | try: | 176 | try: |
| 154 | ref_id = ref_id.decode() | 177 | if not platform_utils.isdir(root_path): |
| 155 | except AttributeError: | 178 | return |
| 156 | pass | 179 | except OSError: |
| 157 | if not ref_id: | ||
| 158 | return | 180 | return |
| 159 | ref_id = ref_id[:-1] | ||
| 160 | 181 | ||
| 161 | if ref_id.startswith("ref: "): | 182 | to_scan = [root] |
| 162 | self._symref[name] = ref_id[5:] | 183 | while to_scan: |
| 163 | else: | 184 | name = to_scan.pop() |
| 164 | self._phyref[name] = ref_id | 185 | self._TrackMtime(name) |
| 165 | self._mtime[name] = mtime | 186 | path = os.path.join(self._gitdir, name) |
| 187 | if not platform_utils.isdir(path): | ||
| 188 | continue | ||
| 189 | |||
| 190 | for child in platform_utils.listdir(path): | ||
| 191 | child_name = os.path.join(name, child) | ||
| 192 | child_path = os.path.join(self._gitdir, child_name) | ||
| 193 | if platform_utils.isdir(child_path): | ||
| 194 | to_scan.append(child_name) | ||
| 195 | else: | ||
| 196 | self._TrackMtime(child_name) | ||
