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