##// END OF EJS Templates
git: don't yield paths for directories when walking
Josef 'Jeff' Sipek -
r45448:3679c88b default
parent child Browse files
Show More
@@ -1,297 +1,296 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 for comp in comps[:-1]:
60 60 te = self._tree[comp]
61 61 t = self._git_repo[te.id]
62 62 ent = t[comps[-1]]
63 63 if ent.filemode == pygit2.GIT_FILEMODE_BLOB:
64 64 flags = b''
65 65 elif ent.filemode == pygit2.GIT_FILEMODE_BLOB_EXECUTABLE:
66 66 flags = b'x'
67 67 elif ent.filemode == pygit2.GIT_FILEMODE_LINK:
68 68 flags = b'l'
69 69 else:
70 70 raise ValueError('unsupported mode %s' % oct(ent.filemode))
71 71 return ent.id.raw, flags
72 72
73 73 def __getitem__(self, path):
74 74 return self._resolve_entry(path)[0]
75 75
76 76 def find(self, path):
77 77 return self._resolve_entry(path)
78 78
79 79 def __len__(self):
80 80 return len(list(self.walk(matchmod.always())))
81 81
82 82 def __nonzero__(self):
83 83 try:
84 84 next(iter(self))
85 85 return True
86 86 except StopIteration:
87 87 return False
88 88
89 89 __bool__ = __nonzero__
90 90
91 91 def __contains__(self, path):
92 92 try:
93 93 self._resolve_entry(path)
94 94 return True
95 95 except KeyError:
96 96 return False
97 97
98 98 def iterkeys(self):
99 99 return self.walk(matchmod.always())
100 100
101 101 def keys(self):
102 102 return list(self.iterkeys())
103 103
104 104 def __iter__(self):
105 105 return self.iterkeys()
106 106
107 107 def __setitem__(self, path, node):
108 108 self._pending_changes[path] = node, self.flags(path)
109 109
110 110 def __delitem__(self, path):
111 111 # TODO: should probably KeyError for already-deleted files?
112 112 self._pending_changes[path] = None
113 113
114 114 def filesnotin(self, other, match=None):
115 115 if match is not None:
116 116 match = matchmod.badmatch(match, lambda path, msg: None)
117 117 sm2 = set(other.walk(match))
118 118 return {f for f in self.walk(match) if f not in sm2}
119 119 return {f for f in self if f not in other}
120 120
121 121 @util.propertycache
122 122 def _dirs(self):
123 123 return pathutil.dirs(self)
124 124
125 125 def hasdir(self, dir):
126 126 return dir in self._dirs
127 127
128 128 def diff(self, other, match=None, clean=False):
129 129 # TODO
130 130 assert False
131 131
132 132 def setflag(self, path, flag):
133 133 node, unused_flag = self._resolve_entry(path)
134 134 self._pending_changes[path] = node, flag
135 135
136 136 def get(self, path, default=None):
137 137 try:
138 138 return self._resolve_entry(path)[0]
139 139 except KeyError:
140 140 return default
141 141
142 142 def flags(self, path):
143 143 try:
144 144 return self._resolve_entry(path)[1]
145 145 except KeyError:
146 146 return b''
147 147
148 148 def copy(self):
149 149 pass
150 150
151 151 def items(self):
152 152 for f in self:
153 153 # TODO: build a proper iterator version of this
154 154 yield self[f]
155 155
156 156 def iteritems(self):
157 157 return self.items()
158 158
159 159 def iterentries(self):
160 160 for f in self:
161 161 # TODO: build a proper iterator version of this
162 162 yield self._resolve_entry(f)
163 163
164 164 def text(self):
165 165 assert False # TODO can this method move out of the manifest iface?
166 166
167 167 def _walkonetree(self, tree, match, subdir):
168 168 for te in tree:
169 169 # TODO: can we prune dir walks with the matcher?
170 170 realname = subdir + pycompat.fsencode(te.name)
171 171 if te.type == pygit2.GIT_OBJ_TREE:
172 172 for inner in self._walkonetree(
173 173 self._git_repo[te.id], match, realname + b'/'
174 174 ):
175 175 yield inner
176 if not match(realname):
177 continue
178 yield pycompat.fsencode(realname)
176 elif match(realname):
177 yield pycompat.fsencode(realname)
179 178
180 179 def walk(self, match):
181 180 # TODO: this is a very lazy way to merge in the pending
182 181 # changes. There is absolutely room for optimization here by
183 182 # being clever about walking over the sets...
184 183 baseline = set(self._walkonetree(self._tree, match, b''))
185 184 deleted = {p for p, v in self._pending_changes.items() if v is None}
186 185 pend = {p for p in self._pending_changes if match(p)}
187 186 return iter(sorted((baseline | pend) - deleted))
188 187
189 188
190 189 @interfaceutil.implementer(repository.imanifestrevisionstored)
191 190 class gittreemanifestctx(object):
192 191 def __init__(self, repo, gittree):
193 192 self._repo = repo
194 193 self._tree = gittree
195 194
196 195 def read(self):
197 196 return gittreemanifest(self._repo, self._tree, None)
198 197
199 198 def readfast(self, shallow=False):
200 199 return self.read()
201 200
202 201 def copy(self):
203 202 # NB: it's important that we return a memgittreemanifestctx
204 203 # because the caller expects a mutable manifest.
205 204 return memgittreemanifestctx(self._repo, self._tree)
206 205
207 206 def find(self, path):
208 207 return self.read()[path]
209 208
210 209
211 210 @interfaceutil.implementer(repository.imanifestrevisionwritable)
212 211 class memgittreemanifestctx(object):
213 212 def __init__(self, repo, tree):
214 213 self._repo = repo
215 214 self._tree = tree
216 215 # dict of path: Optional[Tuple(node, flags)]
217 216 self._pending_changes = {}
218 217
219 218 def read(self):
220 219 return gittreemanifest(self._repo, self._tree, self._pending_changes)
221 220
222 221 def copy(self):
223 222 # TODO: if we have a builder in play, what should happen here?
224 223 # Maybe we can shuffle copy() into the immutable interface.
225 224 return memgittreemanifestctx(self._repo, self._tree)
226 225
227 226 def write(self, transaction, link, p1, p2, added, removed, match=None):
228 227 # We're not (for now, anyway) going to audit filenames, so we
229 228 # can ignore added and removed.
230 229
231 230 # TODO what does this match argument get used for? hopefully
232 231 # just narrow?
233 232 assert not match or isinstance(match, matchmod.alwaysmatcher)
234 233
235 234 touched_dirs = pathutil.dirs(list(self._pending_changes))
236 235 trees = {
237 236 b'': self._tree,
238 237 }
239 238 # path: treebuilder
240 239 builders = {
241 240 b'': self._repo.TreeBuilder(self._tree),
242 241 }
243 242 # get a TreeBuilder for every tree in the touched_dirs set
244 243 for d in sorted(touched_dirs, key=lambda x: (len(x), x)):
245 244 if d == b'':
246 245 # loaded root tree above
247 246 continue
248 247 comps = d.split(b'/')
249 248 full = b''
250 249 for part in comps:
251 250 parent = trees[full]
252 251 try:
253 252 new = self._repo[parent[pycompat.fsdecode(part)]]
254 253 except KeyError:
255 254 # new directory
256 255 new = None
257 256 full += b'/' + part
258 257 if new is not None:
259 258 # existing directory
260 259 trees[full] = new
261 260 builders[full] = self._repo.TreeBuilder(new)
262 261 else:
263 262 # new directory, use an empty dict to easily
264 263 # generate KeyError as any nested new dirs get
265 264 # created.
266 265 trees[full] = {}
267 266 builders[full] = self._repo.TreeBuilder()
268 267 for f, info in self._pending_changes.items():
269 268 if b'/' not in f:
270 269 dirname = b''
271 270 basename = f
272 271 else:
273 272 dirname, basename = f.rsplit(b'/', 1)
274 273 dirname = b'/' + dirname
275 274 if info is None:
276 275 builders[dirname].remove(pycompat.fsdecode(basename))
277 276 else:
278 277 n, fl = info
279 278 mode = {
280 279 b'': pygit2.GIT_FILEMODE_BLOB,
281 280 b'x': pygit2.GIT_FILEMODE_BLOB_EXECUTABLE,
282 281 b'l': pygit2.GIT_FILEMODE_LINK,
283 282 }[fl]
284 283 builders[dirname].insert(
285 284 pycompat.fsdecode(basename), gitutil.togitnode(n), mode
286 285 )
287 286 # This visits the buffered TreeBuilders in deepest-first
288 287 # order, bubbling up the edits.
289 288 for b in sorted(builders, key=len, reverse=True):
290 289 if b == b'':
291 290 break
292 291 cb = builders[b]
293 292 dn, bn = b.rsplit(b'/', 1)
294 293 builders[dn].insert(
295 294 pycompat.fsdecode(bn), cb.write(), pygit2.GIT_FILEMODE_TREE
296 295 )
297 296 return builders[b''].write().raw
General Comments 0
You need to be logged in to leave comments. Login now