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