##// END OF EJS Templates
git: implement diff manifest method...
Josef 'Jeff' Sipek -
r45450:f294b4e1 default
parent child Browse files
Show More
@@ -1,297 +1,367
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=None, clean=False):
129 def diff(self, other, match=lambda x: True, clean=False):
130 # TODO
130 '''Finds changes between the current manifest and m2.
131 assert False
131
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
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,
136 the nodeid will be None and the flags will be the empty
137 string.
138 '''
139 result = {}
140
141 def _iterativediff(t1, t2, subdir):
142 """compares two trees and appends new tree nodes to examine to
143 the stack"""
144 if t1 is None:
145 t1 = {}
146 if t2 is None:
147 t2 = {}
148
149 for e1 in t1:
150 realname = subdir + pycompat.fsencode(e1.name)
151
152 if e1.type == pygit2.GIT_OBJ_TREE:
153 try:
154 e2 = t2[e1.name]
155 if e2.type != pygit2.GIT_OBJ_TREE:
156 e2 = None
157 except KeyError:
158 e2 = None
159
160 stack.append((realname + b'/', e1, e2))
161 else:
162 n1, fl1 = self.find(realname)
163
164 try:
165 e2 = t2[e1.name]
166 n2, fl2 = other.find(realname)
167 except KeyError:
168 e2 = None
169 n2, fl2 = (None, b'')
170
171 if e2 is not None and e2.type == pygit2.GIT_OBJ_TREE:
172 stack.append((realname + b'/', None, e2))
173
174 if not match(realname):
175 continue
176
177 if n1 != n2 or fl1 != fl2:
178 result[realname] = ((n1, fl1), (n2, fl2))
179 elif clean:
180 result[realname] = None
181
182 for e2 in t2:
183 if e2.name in t1:
184 continue
185
186 realname = subdir + pycompat.fsencode(e2.name)
187
188 if e2.type == pygit2.GIT_OBJ_TREE:
189 stack.append((realname + b'/', None, e2))
190 elif match(realname):
191 n2, fl2 = other.find(realname)
192 result[realname] = ((None, b''), (n2, fl2))
193
194 stack = []
195 _iterativediff(self._tree, other._tree, b'')
196 while stack:
197 subdir, t1, t2 = stack.pop()
198 # stack is populated in the function call
199 _iterativediff(t1, t2, subdir)
200
201 return result
132
202
133 def setflag(self, path, flag):
203 def setflag(self, path, flag):
134 node, unused_flag = self._resolve_entry(path)
204 node, unused_flag = self._resolve_entry(path)
135 self._pending_changes[path] = node, flag
205 self._pending_changes[path] = node, flag
136
206
137 def get(self, path, default=None):
207 def get(self, path, default=None):
138 try:
208 try:
139 return self._resolve_entry(path)[0]
209 return self._resolve_entry(path)[0]
140 except KeyError:
210 except KeyError:
141 return default
211 return default
142
212
143 def flags(self, path):
213 def flags(self, path):
144 try:
214 try:
145 return self._resolve_entry(path)[1]
215 return self._resolve_entry(path)[1]
146 except KeyError:
216 except KeyError:
147 return b''
217 return b''
148
218
149 def copy(self):
219 def copy(self):
150 pass
220 pass
151
221
152 def items(self):
222 def items(self):
153 for f in self:
223 for f in self:
154 # TODO: build a proper iterator version of this
224 # TODO: build a proper iterator version of this
155 yield self[f]
225 yield self[f]
156
226
157 def iteritems(self):
227 def iteritems(self):
158 return self.items()
228 return self.items()
159
229
160 def iterentries(self):
230 def iterentries(self):
161 for f in self:
231 for f in self:
162 # TODO: build a proper iterator version of this
232 # TODO: build a proper iterator version of this
163 yield self._resolve_entry(f)
233 yield self._resolve_entry(f)
164
234
165 def text(self):
235 def text(self):
166 assert False # TODO can this method move out of the manifest iface?
236 assert False # TODO can this method move out of the manifest iface?
167
237
168 def _walkonetree(self, tree, match, subdir):
238 def _walkonetree(self, tree, match, subdir):
169 for te in tree:
239 for te in tree:
170 # TODO: can we prune dir walks with the matcher?
240 # TODO: can we prune dir walks with the matcher?
171 realname = subdir + pycompat.fsencode(te.name)
241 realname = subdir + pycompat.fsencode(te.name)
172 if te.type == pygit2.GIT_OBJ_TREE:
242 if te.type == pygit2.GIT_OBJ_TREE:
173 for inner in self._walkonetree(
243 for inner in self._walkonetree(
174 self._git_repo[te.id], match, realname + b'/'
244 self._git_repo[te.id], match, realname + b'/'
175 ):
245 ):
176 yield inner
246 yield inner
177 elif match(realname):
247 elif match(realname):
178 yield pycompat.fsencode(realname)
248 yield pycompat.fsencode(realname)
179
249
180 def walk(self, match):
250 def walk(self, match):
181 # TODO: this is a very lazy way to merge in the pending
251 # TODO: this is a very lazy way to merge in the pending
182 # changes. There is absolutely room for optimization here by
252 # changes. There is absolutely room for optimization here by
183 # being clever about walking over the sets...
253 # being clever about walking over the sets...
184 baseline = set(self._walkonetree(self._tree, match, b''))
254 baseline = set(self._walkonetree(self._tree, match, b''))
185 deleted = {p for p, v in self._pending_changes.items() if v is None}
255 deleted = {p for p, v in self._pending_changes.items() if v is None}
186 pend = {p for p in self._pending_changes if match(p)}
256 pend = {p for p in self._pending_changes if match(p)}
187 return iter(sorted((baseline | pend) - deleted))
257 return iter(sorted((baseline | pend) - deleted))
188
258
189
259
190 @interfaceutil.implementer(repository.imanifestrevisionstored)
260 @interfaceutil.implementer(repository.imanifestrevisionstored)
191 class gittreemanifestctx(object):
261 class gittreemanifestctx(object):
192 def __init__(self, repo, gittree):
262 def __init__(self, repo, gittree):
193 self._repo = repo
263 self._repo = repo
194 self._tree = gittree
264 self._tree = gittree
195
265
196 def read(self):
266 def read(self):
197 return gittreemanifest(self._repo, self._tree, None)
267 return gittreemanifest(self._repo, self._tree, None)
198
268
199 def readfast(self, shallow=False):
269 def readfast(self, shallow=False):
200 return self.read()
270 return self.read()
201
271
202 def copy(self):
272 def copy(self):
203 # NB: it's important that we return a memgittreemanifestctx
273 # NB: it's important that we return a memgittreemanifestctx
204 # because the caller expects a mutable manifest.
274 # because the caller expects a mutable manifest.
205 return memgittreemanifestctx(self._repo, self._tree)
275 return memgittreemanifestctx(self._repo, self._tree)
206
276
207 def find(self, path):
277 def find(self, path):
208 return self.read()[path]
278 return self.read()[path]
209
279
210
280
211 @interfaceutil.implementer(repository.imanifestrevisionwritable)
281 @interfaceutil.implementer(repository.imanifestrevisionwritable)
212 class memgittreemanifestctx(object):
282 class memgittreemanifestctx(object):
213 def __init__(self, repo, tree):
283 def __init__(self, repo, tree):
214 self._repo = repo
284 self._repo = repo
215 self._tree = tree
285 self._tree = tree
216 # dict of path: Optional[Tuple(node, flags)]
286 # dict of path: Optional[Tuple(node, flags)]
217 self._pending_changes = {}
287 self._pending_changes = {}
218
288
219 def read(self):
289 def read(self):
220 return gittreemanifest(self._repo, self._tree, self._pending_changes)
290 return gittreemanifest(self._repo, self._tree, self._pending_changes)
221
291
222 def copy(self):
292 def copy(self):
223 # TODO: if we have a builder in play, what should happen here?
293 # TODO: if we have a builder in play, what should happen here?
224 # Maybe we can shuffle copy() into the immutable interface.
294 # Maybe we can shuffle copy() into the immutable interface.
225 return memgittreemanifestctx(self._repo, self._tree)
295 return memgittreemanifestctx(self._repo, self._tree)
226
296
227 def write(self, transaction, link, p1, p2, added, removed, match=None):
297 def write(self, transaction, link, p1, p2, added, removed, match=None):
228 # We're not (for now, anyway) going to audit filenames, so we
298 # We're not (for now, anyway) going to audit filenames, so we
229 # can ignore added and removed.
299 # can ignore added and removed.
230
300
231 # TODO what does this match argument get used for? hopefully
301 # TODO what does this match argument get used for? hopefully
232 # just narrow?
302 # just narrow?
233 assert not match or isinstance(match, matchmod.alwaysmatcher)
303 assert not match or isinstance(match, matchmod.alwaysmatcher)
234
304
235 touched_dirs = pathutil.dirs(list(self._pending_changes))
305 touched_dirs = pathutil.dirs(list(self._pending_changes))
236 trees = {
306 trees = {
237 b'': self._tree,
307 b'': self._tree,
238 }
308 }
239 # path: treebuilder
309 # path: treebuilder
240 builders = {
310 builders = {
241 b'': self._repo.TreeBuilder(self._tree),
311 b'': self._repo.TreeBuilder(self._tree),
242 }
312 }
243 # get a TreeBuilder for every tree in the touched_dirs set
313 # get a TreeBuilder for every tree in the touched_dirs set
244 for d in sorted(touched_dirs, key=lambda x: (len(x), x)):
314 for d in sorted(touched_dirs, key=lambda x: (len(x), x)):
245 if d == b'':
315 if d == b'':
246 # loaded root tree above
316 # loaded root tree above
247 continue
317 continue
248 comps = d.split(b'/')
318 comps = d.split(b'/')
249 full = b''
319 full = b''
250 for part in comps:
320 for part in comps:
251 parent = trees[full]
321 parent = trees[full]
252 try:
322 try:
253 new = self._repo[parent[pycompat.fsdecode(part)]]
323 new = self._repo[parent[pycompat.fsdecode(part)]]
254 except KeyError:
324 except KeyError:
255 # new directory
325 # new directory
256 new = None
326 new = None
257 full += b'/' + part
327 full += b'/' + part
258 if new is not None:
328 if new is not None:
259 # existing directory
329 # existing directory
260 trees[full] = new
330 trees[full] = new
261 builders[full] = self._repo.TreeBuilder(new)
331 builders[full] = self._repo.TreeBuilder(new)
262 else:
332 else:
263 # new directory, use an empty dict to easily
333 # new directory, use an empty dict to easily
264 # generate KeyError as any nested new dirs get
334 # generate KeyError as any nested new dirs get
265 # created.
335 # created.
266 trees[full] = {}
336 trees[full] = {}
267 builders[full] = self._repo.TreeBuilder()
337 builders[full] = self._repo.TreeBuilder()
268 for f, info in self._pending_changes.items():
338 for f, info in self._pending_changes.items():
269 if b'/' not in f:
339 if b'/' not in f:
270 dirname = b''
340 dirname = b''
271 basename = f
341 basename = f
272 else:
342 else:
273 dirname, basename = f.rsplit(b'/', 1)
343 dirname, basename = f.rsplit(b'/', 1)
274 dirname = b'/' + dirname
344 dirname = b'/' + dirname
275 if info is None:
345 if info is None:
276 builders[dirname].remove(pycompat.fsdecode(basename))
346 builders[dirname].remove(pycompat.fsdecode(basename))
277 else:
347 else:
278 n, fl = info
348 n, fl = info
279 mode = {
349 mode = {
280 b'': pygit2.GIT_FILEMODE_BLOB,
350 b'': pygit2.GIT_FILEMODE_BLOB,
281 b'x': pygit2.GIT_FILEMODE_BLOB_EXECUTABLE,
351 b'x': pygit2.GIT_FILEMODE_BLOB_EXECUTABLE,
282 b'l': pygit2.GIT_FILEMODE_LINK,
352 b'l': pygit2.GIT_FILEMODE_LINK,
283 }[fl]
353 }[fl]
284 builders[dirname].insert(
354 builders[dirname].insert(
285 pycompat.fsdecode(basename), gitutil.togitnode(n), mode
355 pycompat.fsdecode(basename), gitutil.togitnode(n), mode
286 )
356 )
287 # This visits the buffered TreeBuilders in deepest-first
357 # This visits the buffered TreeBuilders in deepest-first
288 # order, bubbling up the edits.
358 # order, bubbling up the edits.
289 for b in sorted(builders, key=len, reverse=True):
359 for b in sorted(builders, key=len, reverse=True):
290 if b == b'':
360 if b == b'':
291 break
361 break
292 cb = builders[b]
362 cb = builders[b]
293 dn, bn = b.rsplit(b'/', 1)
363 dn, bn = b.rsplit(b'/', 1)
294 builders[dn].insert(
364 builders[dn].insert(
295 pycompat.fsdecode(bn), cb.write(), pygit2.GIT_FILEMODE_TREE
365 pycompat.fsdecode(bn), cb.write(), pygit2.GIT_FILEMODE_TREE
296 )
366 )
297 return builders[b''].write().raw
367 return builders[b''].write().raw
General Comments 0
You need to be logged in to leave comments. Login now