##// END OF EJS Templates
git: pass `id` attribute of `pygit2.Tree` object...
Connor Sheehan -
r46089:73a5aa5e default
parent child Browse files
Show More
@@ -1,369 +1,370 b''
1 1 from __future__ import absolute_import
2 2
3 3 from mercurial import (
4 4 match as matchmod,
5 5 pathutil,
6 6 pycompat,
7 7 util,
8 8 )
9 9 from mercurial.interfaces import (
10 10 repository,
11 11 util as interfaceutil,
12 12 )
13 13 from . import gitutil
14 14
15 15
16 16 pygit2 = gitutil.get_pygit2()
17 17
18 18
19 19 @interfaceutil.implementer(repository.imanifestdict)
20 20 class gittreemanifest(object):
21 21 """Expose git trees (and optionally a builder's overlay) as a manifestdict.
22 22
23 23 Very similar to mercurial.manifest.treemanifest.
24 24 """
25 25
26 26 def __init__(self, git_repo, root_tree, pending_changes):
27 27 """Initializer.
28 28
29 29 Args:
30 30 git_repo: The git_repo we're walking (required to look up child
31 31 trees).
32 32 root_tree: The root Git tree object for this manifest.
33 33 pending_changes: A dict in which pending changes will be
34 34 tracked. The enclosing memgittreemanifestctx will use this to
35 35 construct any required Tree objects in Git during it's
36 36 `write()` method.
37 37 """
38 38 self._git_repo = git_repo
39 39 self._tree = root_tree
40 40 if pending_changes is None:
41 41 pending_changes = {}
42 42 # dict of path: Optional[Tuple(node, flags)]
43 43 self._pending_changes = pending_changes
44 44
45 45 def _resolve_entry(self, path):
46 46 """Given a path, load its node and flags, or raise KeyError if missing.
47 47
48 48 This takes into account any pending writes in the builder.
49 49 """
50 50 upath = pycompat.fsdecode(path)
51 51 ent = None
52 52 if path in self._pending_changes:
53 53 val = self._pending_changes[path]
54 54 if val is None:
55 55 raise KeyError
56 56 return val
57 57 t = self._tree
58 58 comps = upath.split('/')
59 59 te = self._tree
60 60 for comp in comps[:-1]:
61 61 te = te[comp]
62 62 t = self._git_repo[te.id]
63 63 ent = t[comps[-1]]
64 64 if ent.filemode == pygit2.GIT_FILEMODE_BLOB:
65 65 flags = b''
66 66 elif ent.filemode == pygit2.GIT_FILEMODE_BLOB_EXECUTABLE:
67 67 flags = b'x'
68 68 elif ent.filemode == pygit2.GIT_FILEMODE_LINK:
69 69 flags = b'l'
70 70 else:
71 71 raise ValueError('unsupported mode %s' % oct(ent.filemode))
72 72 return ent.id.raw, flags
73 73
74 74 def __getitem__(self, path):
75 75 return self._resolve_entry(path)[0]
76 76
77 77 def find(self, path):
78 78 return self._resolve_entry(path)
79 79
80 80 def __len__(self):
81 81 return len(list(self.walk(matchmod.always())))
82 82
83 83 def __nonzero__(self):
84 84 try:
85 85 next(iter(self))
86 86 return True
87 87 except StopIteration:
88 88 return False
89 89
90 90 __bool__ = __nonzero__
91 91
92 92 def __contains__(self, path):
93 93 try:
94 94 self._resolve_entry(path)
95 95 return True
96 96 except KeyError:
97 97 return False
98 98
99 99 def iterkeys(self):
100 100 return self.walk(matchmod.always())
101 101
102 102 def keys(self):
103 103 return list(self.iterkeys())
104 104
105 105 def __iter__(self):
106 106 return self.iterkeys()
107 107
108 108 def __setitem__(self, path, node):
109 109 self._pending_changes[path] = node, self.flags(path)
110 110
111 111 def __delitem__(self, path):
112 112 # TODO: should probably KeyError for already-deleted files?
113 113 self._pending_changes[path] = None
114 114
115 115 def filesnotin(self, other, match=None):
116 116 if match is not None:
117 117 match = matchmod.badmatch(match, lambda path, msg: None)
118 118 sm2 = set(other.walk(match))
119 119 return {f for f in self.walk(match) if f not in sm2}
120 120 return {f for f in self if f not in other}
121 121
122 122 @util.propertycache
123 123 def _dirs(self):
124 124 return pathutil.dirs(self)
125 125
126 126 def hasdir(self, dir):
127 127 return dir in self._dirs
128 128
129 129 def diff(self, other, match=lambda x: True, clean=False):
130 130 '''Finds changes between the current manifest and m2.
131 131
132 132 The result is returned as a dict with filename as key and
133 133 values of the form ((n1,fl1),(n2,fl2)), where n1/n2 is the
134 134 nodeid in the current/other manifest and fl1/fl2 is the flag
135 135 in the current/other manifest. Where the file does not exist,
136 136 the nodeid will be None and the flags will be the empty
137 137 string.
138 138 '''
139 139 result = {}
140 140
141 141 def _iterativediff(t1, t2, subdir):
142 142 """compares two trees and appends new tree nodes to examine to
143 143 the stack"""
144 144 if t1 is None:
145 145 t1 = {}
146 146 if t2 is None:
147 147 t2 = {}
148 148
149 149 for e1 in t1:
150 150 realname = subdir + pycompat.fsencode(e1.name)
151 151
152 152 if e1.type == pygit2.GIT_OBJ_TREE:
153 153 try:
154 154 e2 = t2[e1.name]
155 155 if e2.type != pygit2.GIT_OBJ_TREE:
156 156 e2 = None
157 157 except KeyError:
158 158 e2 = None
159 159
160 160 stack.append((realname + b'/', e1, e2))
161 161 else:
162 162 n1, fl1 = self.find(realname)
163 163
164 164 try:
165 165 e2 = t2[e1.name]
166 166 n2, fl2 = other.find(realname)
167 167 except KeyError:
168 168 e2 = None
169 169 n2, fl2 = (None, b'')
170 170
171 171 if e2 is not None and e2.type == pygit2.GIT_OBJ_TREE:
172 172 stack.append((realname + b'/', None, e2))
173 173
174 174 if not match(realname):
175 175 continue
176 176
177 177 if n1 != n2 or fl1 != fl2:
178 178 result[realname] = ((n1, fl1), (n2, fl2))
179 179 elif clean:
180 180 result[realname] = None
181 181
182 182 for e2 in t2:
183 183 if e2.name in t1:
184 184 continue
185 185
186 186 realname = subdir + pycompat.fsencode(e2.name)
187 187
188 188 if e2.type == pygit2.GIT_OBJ_TREE:
189 189 stack.append((realname + b'/', None, e2))
190 190 elif match(realname):
191 191 n2, fl2 = other.find(realname)
192 192 result[realname] = ((None, b''), (n2, fl2))
193 193
194 194 stack = []
195 195 _iterativediff(self._tree, other._tree, b'')
196 196 while stack:
197 197 subdir, t1, t2 = stack.pop()
198 198 # stack is populated in the function call
199 199 _iterativediff(t1, t2, subdir)
200 200
201 201 return result
202 202
203 203 def setflag(self, path, flag):
204 204 node, unused_flag = self._resolve_entry(path)
205 205 self._pending_changes[path] = node, flag
206 206
207 207 def get(self, path, default=None):
208 208 try:
209 209 return self._resolve_entry(path)[0]
210 210 except KeyError:
211 211 return default
212 212
213 213 def flags(self, path):
214 214 try:
215 215 return self._resolve_entry(path)[1]
216 216 except KeyError:
217 217 return b''
218 218
219 219 def copy(self):
220 220 return gittreemanifest(
221 221 self._git_repo, self._tree, dict(self._pending_changes)
222 222 )
223 223
224 224 def items(self):
225 225 for f in self:
226 226 # TODO: build a proper iterator version of this
227 227 yield self[f]
228 228
229 229 def iteritems(self):
230 230 return self.items()
231 231
232 232 def iterentries(self):
233 233 for f in self:
234 234 # TODO: build a proper iterator version of this
235 235 yield self._resolve_entry(f)
236 236
237 237 def text(self):
238 238 assert False # TODO can this method move out of the manifest iface?
239 239
240 240 def _walkonetree(self, tree, match, subdir):
241 241 for te in tree:
242 242 # TODO: can we prune dir walks with the matcher?
243 243 realname = subdir + pycompat.fsencode(te.name)
244 244 if te.type == pygit2.GIT_OBJ_TREE:
245 245 for inner in self._walkonetree(
246 246 self._git_repo[te.id], match, realname + b'/'
247 247 ):
248 248 yield inner
249 249 elif match(realname):
250 250 yield pycompat.fsencode(realname)
251 251
252 252 def walk(self, match):
253 253 # TODO: this is a very lazy way to merge in the pending
254 254 # changes. There is absolutely room for optimization here by
255 255 # being clever about walking over the sets...
256 256 baseline = set(self._walkonetree(self._tree, match, b''))
257 257 deleted = {p for p, v in self._pending_changes.items() if v is None}
258 258 pend = {p for p in self._pending_changes if match(p)}
259 259 return iter(sorted((baseline | pend) - deleted))
260 260
261 261
262 262 @interfaceutil.implementer(repository.imanifestrevisionstored)
263 263 class gittreemanifestctx(object):
264 264 def __init__(self, repo, gittree):
265 265 self._repo = repo
266 266 self._tree = gittree
267 267
268 268 def read(self):
269 269 return gittreemanifest(self._repo, self._tree, None)
270 270
271 271 def readfast(self, shallow=False):
272 272 return self.read()
273 273
274 274 def copy(self):
275 275 # NB: it's important that we return a memgittreemanifestctx
276 276 # because the caller expects a mutable manifest.
277 277 return memgittreemanifestctx(self._repo, self._tree)
278 278
279 279 def find(self, path):
280 280 return self.read()[path]
281 281
282 282
283 283 @interfaceutil.implementer(repository.imanifestrevisionwritable)
284 284 class memgittreemanifestctx(object):
285 285 def __init__(self, repo, tree):
286 286 self._repo = repo
287 287 self._tree = tree
288 288 # dict of path: Optional[Tuple(node, flags)]
289 289 self._pending_changes = {}
290 290
291 291 def read(self):
292 292 return gittreemanifest(self._repo, self._tree, self._pending_changes)
293 293
294 294 def copy(self):
295 295 # TODO: if we have a builder in play, what should happen here?
296 296 # Maybe we can shuffle copy() into the immutable interface.
297 297 return memgittreemanifestctx(self._repo, self._tree)
298 298
299 299 def write(self, transaction, link, p1, p2, added, removed, match=None):
300 300 # We're not (for now, anyway) going to audit filenames, so we
301 301 # can ignore added and removed.
302 302
303 303 # TODO what does this match argument get used for? hopefully
304 304 # just narrow?
305 305 assert not match or isinstance(match, matchmod.alwaysmatcher)
306 306
307 307 touched_dirs = pathutil.dirs(list(self._pending_changes))
308 308 trees = {
309 309 b'': self._tree,
310 310 }
311 311 # path: treebuilder
312 312 builders = {
313 313 b'': self._repo.TreeBuilder(self._tree),
314 314 }
315 315 # get a TreeBuilder for every tree in the touched_dirs set
316 316 for d in sorted(touched_dirs, key=lambda x: (len(x), x)):
317 317 if d == b'':
318 318 # loaded root tree above
319 319 continue
320 320 comps = d.split(b'/')
321 321 full = b''
322 322 for part in comps:
323 323 parent = trees[full]
324 324 try:
325 new = self._repo[parent[pycompat.fsdecode(part)]]
325 parent_tree_id = parent[pycompat.fsdecode(part)].id
326 new = self._repo[parent_tree_id]
326 327 except KeyError:
327 328 # new directory
328 329 new = None
329 330 full += b'/' + part
330 331 if new is not None:
331 332 # existing directory
332 333 trees[full] = new
333 334 builders[full] = self._repo.TreeBuilder(new)
334 335 else:
335 336 # new directory, use an empty dict to easily
336 337 # generate KeyError as any nested new dirs get
337 338 # created.
338 339 trees[full] = {}
339 340 builders[full] = self._repo.TreeBuilder()
340 341 for f, info in self._pending_changes.items():
341 342 if b'/' not in f:
342 343 dirname = b''
343 344 basename = f
344 345 else:
345 346 dirname, basename = f.rsplit(b'/', 1)
346 347 dirname = b'/' + dirname
347 348 if info is None:
348 349 builders[dirname].remove(pycompat.fsdecode(basename))
349 350 else:
350 351 n, fl = info
351 352 mode = {
352 353 b'': pygit2.GIT_FILEMODE_BLOB,
353 354 b'x': pygit2.GIT_FILEMODE_BLOB_EXECUTABLE,
354 355 b'l': pygit2.GIT_FILEMODE_LINK,
355 356 }[fl]
356 357 builders[dirname].insert(
357 358 pycompat.fsdecode(basename), gitutil.togitnode(n), mode
358 359 )
359 360 # This visits the buffered TreeBuilders in deepest-first
360 361 # order, bubbling up the edits.
361 362 for b in sorted(builders, key=len, reverse=True):
362 363 if b == b'':
363 364 break
364 365 cb = builders[b]
365 366 dn, bn = b.rsplit(b'/', 1)
366 367 builders[dn].insert(
367 368 pycompat.fsdecode(bn), cb.write(), pygit2.GIT_FILEMODE_TREE
368 369 )
369 370 return builders[b''].write().raw
General Comments 0
You need to be logged in to leave comments. Login now