summaryrefslogtreecommitdiffstats
path: root/git_refs.py
diff options
context:
space:
mode:
authorGavin Mak <gavinmak@google.com>2026-02-06 14:55:34 -0800
committerLUCI <gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com>2026-03-17 14:28:39 -0700
commit67881c0c3b27d850c57070919f4476b370de7768 (patch)
tree677767437a0246e0ae6a7d6e3ca84adfc4cf72c6 /git_refs.py
parent551087cd980976775470cde5f7ede264a90b5349 (diff)
downloadgit-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.py141
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
15import os 15import os
16 16
17from git_command import GitCommand
17import platform_utils 18import platform_utils
18from repo_trace import Trace 19from 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)