##// END OF EJS Templates
narrow: extract wdir cleanup function to make it extensible...
Kyle Lippincott -
r39770:9d5c919b default
parent child Browse files
Show More
@@ -1,412 +1,420 b''
1 1 # narrowcommands.py - command modifications for narrowhg extension
2 2 #
3 3 # Copyright 2017 Google, Inc.
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7 from __future__ import absolute_import
8 8
9 9 import itertools
10 10 import os
11 11
12 12 from mercurial.i18n import _
13 13 from mercurial import (
14 14 cmdutil,
15 15 commands,
16 16 discovery,
17 17 encoding,
18 18 error,
19 19 exchange,
20 20 extensions,
21 21 hg,
22 22 merge,
23 23 narrowspec,
24 24 node,
25 25 pycompat,
26 26 registrar,
27 27 repair,
28 28 repository,
29 29 repoview,
30 30 sparse,
31 31 util,
32 32 )
33 33
34 34 from . import (
35 35 narrowwirepeer,
36 36 )
37 37
38 38 table = {}
39 39 command = registrar.command(table)
40 40
41 41 def setup():
42 42 """Wraps user-facing mercurial commands with narrow-aware versions."""
43 43
44 44 entry = extensions.wrapcommand(commands.table, 'clone', clonenarrowcmd)
45 45 entry[1].append(('', 'narrow', None,
46 46 _("create a narrow clone of select files")))
47 47 entry[1].append(('', 'depth', '',
48 48 _("limit the history fetched by distance from heads")))
49 49 entry[1].append(('', 'narrowspec', '',
50 50 _("read narrowspecs from file")))
51 51 # TODO(durin42): unify sparse/narrow --include/--exclude logic a bit
52 52 if 'sparse' not in extensions.enabled():
53 53 entry[1].append(('', 'include', [],
54 54 _("specifically fetch this file/directory")))
55 55 entry[1].append(
56 56 ('', 'exclude', [],
57 57 _("do not fetch this file/directory, even if included")))
58 58
59 59 entry = extensions.wrapcommand(commands.table, 'pull', pullnarrowcmd)
60 60 entry[1].append(('', 'depth', '',
61 61 _("limit the history fetched by distance from heads")))
62 62
63 63 extensions.wrapcommand(commands.table, 'archive', archivenarrowcmd)
64 64
65 65 def clonenarrowcmd(orig, ui, repo, *args, **opts):
66 66 """Wraps clone command, so 'hg clone' first wraps localrepo.clone()."""
67 67 opts = pycompat.byteskwargs(opts)
68 68 wrappedextraprepare = util.nullcontextmanager()
69 69 narrowspecfile = opts['narrowspec']
70 70
71 71 if narrowspecfile:
72 72 filepath = os.path.join(pycompat.getcwd(), narrowspecfile)
73 73 ui.status(_("reading narrowspec from '%s'\n") % filepath)
74 74 try:
75 75 fdata = util.readfile(filepath)
76 76 except IOError as inst:
77 77 raise error.Abort(_("cannot read narrowspecs from '%s': %s") %
78 78 (filepath, encoding.strtolocal(inst.strerror)))
79 79
80 80 includes, excludes, profiles = sparse.parseconfig(ui, fdata, 'narrow')
81 81 if profiles:
82 82 raise error.Abort(_("cannot specify other files using '%include' in"
83 83 " narrowspec"))
84 84
85 85 narrowspec.validatepatterns(includes)
86 86 narrowspec.validatepatterns(excludes)
87 87
88 88 # narrowspec is passed so we should assume that user wants narrow clone
89 89 opts['narrow'] = True
90 90 opts['include'].extend(includes)
91 91 opts['exclude'].extend(excludes)
92 92
93 93 if opts['narrow']:
94 94 def pullbundle2extraprepare_widen(orig, pullop, kwargs):
95 95 orig(pullop, kwargs)
96 96
97 97 if opts.get('depth'):
98 98 kwargs['depth'] = opts['depth']
99 99 wrappedextraprepare = extensions.wrappedfunction(exchange,
100 100 '_pullbundle2extraprepare', pullbundle2extraprepare_widen)
101 101
102 102 with wrappedextraprepare:
103 103 return orig(ui, repo, *args, **pycompat.strkwargs(opts))
104 104
105 105 def pullnarrowcmd(orig, ui, repo, *args, **opts):
106 106 """Wraps pull command to allow modifying narrow spec."""
107 107 wrappedextraprepare = util.nullcontextmanager()
108 108 if repository.NARROW_REQUIREMENT in repo.requirements:
109 109
110 110 def pullbundle2extraprepare_widen(orig, pullop, kwargs):
111 111 orig(pullop, kwargs)
112 112 if opts.get(r'depth'):
113 113 kwargs['depth'] = opts[r'depth']
114 114 wrappedextraprepare = extensions.wrappedfunction(exchange,
115 115 '_pullbundle2extraprepare', pullbundle2extraprepare_widen)
116 116
117 117 with wrappedextraprepare:
118 118 return orig(ui, repo, *args, **opts)
119 119
120 120 def archivenarrowcmd(orig, ui, repo, *args, **opts):
121 121 """Wraps archive command to narrow the default includes."""
122 122 if repository.NARROW_REQUIREMENT in repo.requirements:
123 123 repo_includes, repo_excludes = repo.narrowpats
124 124 includes = set(opts.get(r'include', []))
125 125 excludes = set(opts.get(r'exclude', []))
126 126 includes, excludes, unused_invalid = narrowspec.restrictpatterns(
127 127 includes, excludes, repo_includes, repo_excludes)
128 128 if includes:
129 129 opts[r'include'] = includes
130 130 if excludes:
131 131 opts[r'exclude'] = excludes
132 132 return orig(ui, repo, *args, **opts)
133 133
134 134 def pullbundle2extraprepare(orig, pullop, kwargs):
135 135 repo = pullop.repo
136 136 if repository.NARROW_REQUIREMENT not in repo.requirements:
137 137 return orig(pullop, kwargs)
138 138
139 139 if narrowwirepeer.NARROWCAP not in pullop.remote.capabilities():
140 140 raise error.Abort(_("server doesn't support narrow clones"))
141 141 orig(pullop, kwargs)
142 142 kwargs['narrow'] = True
143 143 include, exclude = repo.narrowpats
144 144 kwargs['oldincludepats'] = include
145 145 kwargs['oldexcludepats'] = exclude
146 146 kwargs['includepats'] = include
147 147 kwargs['excludepats'] = exclude
148 148 # calculate known nodes only in ellipses cases because in non-ellipses cases
149 149 # we have all the nodes
150 150 if narrowwirepeer.ELLIPSESCAP in pullop.remote.capabilities():
151 151 kwargs['known'] = [node.hex(ctx.node()) for ctx in
152 152 repo.set('::%ln', pullop.common)
153 153 if ctx.node() != node.nullid]
154 154 if not kwargs['known']:
155 155 # Mercurial serializes an empty list as '' and deserializes it as
156 156 # [''], so delete it instead to avoid handling the empty string on
157 157 # the server.
158 158 del kwargs['known']
159 159
160 160 extensions.wrapfunction(exchange,'_pullbundle2extraprepare',
161 161 pullbundle2extraprepare)
162 162
163 # This is an extension point for filesystems that need to do something other
164 # than just blindly unlink the files. It's not clear what arguments would be
165 # useful, so we're passing in a fair number of them, some of them redundant.
166 def _narrowcleanupwdir(repo, oldincludes, oldexcludes, newincludes, newexcludes,
167 oldmatch, newmatch):
168 for f in repo.dirstate:
169 if not newmatch(f):
170 repo.dirstate.drop(f)
171 repo.wvfs.unlinkpath(f)
172
163 173 def _narrow(ui, repo, remote, commoninc, oldincludes, oldexcludes,
164 174 newincludes, newexcludes, force):
165 175 oldmatch = narrowspec.match(repo.root, oldincludes, oldexcludes)
166 176 newmatch = narrowspec.match(repo.root, newincludes, newexcludes)
167 177
168 178 # This is essentially doing "hg outgoing" to find all local-only
169 179 # commits. We will then check that the local-only commits don't
170 180 # have any changes to files that will be untracked.
171 181 unfi = repo.unfiltered()
172 182 outgoing = discovery.findcommonoutgoing(unfi, remote,
173 183 commoninc=commoninc)
174 184 ui.status(_('looking for local changes to affected paths\n'))
175 185 localnodes = []
176 186 for n in itertools.chain(outgoing.missing, outgoing.excluded):
177 187 if any(oldmatch(f) and not newmatch(f) for f in unfi[n].files()):
178 188 localnodes.append(n)
179 189 revstostrip = unfi.revs('descendants(%ln)', localnodes)
180 190 hiddenrevs = repoview.filterrevs(repo, 'visible')
181 191 visibletostrip = list(repo.changelog.node(r)
182 192 for r in (revstostrip - hiddenrevs))
183 193 if visibletostrip:
184 194 ui.status(_('The following changeset(s) or their ancestors have '
185 195 'local changes not on the remote:\n'))
186 196 maxnodes = 10
187 197 if ui.verbose or len(visibletostrip) <= maxnodes:
188 198 for n in visibletostrip:
189 199 ui.status('%s\n' % node.short(n))
190 200 else:
191 201 for n in visibletostrip[:maxnodes]:
192 202 ui.status('%s\n' % node.short(n))
193 203 ui.status(_('...and %d more, use --verbose to list all\n') %
194 204 (len(visibletostrip) - maxnodes))
195 205 if not force:
196 206 raise error.Abort(_('local changes found'),
197 207 hint=_('use --force-delete-local-changes to '
198 208 'ignore'))
199 209
200 210 with ui.uninterruptable():
201 211 if revstostrip:
202 212 tostrip = [unfi.changelog.node(r) for r in revstostrip]
203 213 if repo['.'].node() in tostrip:
204 214 # stripping working copy, so move to a different commit first
205 215 urev = max(repo.revs('(::%n) - %ln + null',
206 216 repo['.'].node(), visibletostrip))
207 217 hg.clean(repo, urev)
208 218 repair.strip(ui, unfi, tostrip, topic='narrow')
209 219
210 220 todelete = []
211 221 for f, f2, size in repo.store.datafiles():
212 222 if f.startswith('data/'):
213 223 file = f[5:-2]
214 224 if not newmatch(file):
215 225 todelete.append(f)
216 226 elif f.startswith('meta/'):
217 227 dir = f[5:-13]
218 228 dirs = ['.'] + sorted(util.dirs({dir})) + [dir]
219 229 include = True
220 230 for d in dirs:
221 231 visit = newmatch.visitdir(d)
222 232 if not visit:
223 233 include = False
224 234 break
225 235 if visit == 'all':
226 236 break
227 237 if not include:
228 238 todelete.append(f)
229 239
230 240 repo.destroying()
231 241
232 242 with repo.transaction("narrowing"):
233 243 for f in todelete:
234 244 ui.status(_('deleting %s\n') % f)
235 245 util.unlinkpath(repo.svfs.join(f))
236 246 repo.store.markremoved(f)
237 247
238 for f in repo.dirstate:
239 if not newmatch(f):
240 repo.dirstate.drop(f)
241 repo.wvfs.unlinkpath(f)
248 _narrowcleanupwdir(repo, oldincludes, oldexcludes, newincludes,
249 newexcludes, oldmatch, newmatch)
242 250 repo.setnarrowpats(newincludes, newexcludes)
243 251
244 252 repo.destroyed()
245 253
246 254 def _widen(ui, repo, remote, commoninc, newincludes, newexcludes):
247 255 newmatch = narrowspec.match(repo.root, newincludes, newexcludes)
248 256
249 257 def pullbundle2extraprepare_widen(orig, pullop, kwargs):
250 258 orig(pullop, kwargs)
251 259 # The old{in,ex}cludepats have already been set by orig()
252 260 kwargs['includepats'] = newincludes
253 261 kwargs['excludepats'] = newexcludes
254 262 kwargs['widen'] = True
255 263 wrappedextraprepare = extensions.wrappedfunction(exchange,
256 264 '_pullbundle2extraprepare', pullbundle2extraprepare_widen)
257 265
258 266 # define a function that narrowbundle2 can call after creating the
259 267 # backup bundle, but before applying the bundle from the server
260 268 def setnewnarrowpats():
261 269 repo.setnarrowpats(newincludes, newexcludes)
262 270 repo.setnewnarrowpats = setnewnarrowpats
263 271 # silence the devel-warning of applying an empty changegroup
264 272 overrides = {('devel', 'all-warnings'): False}
265 273
266 274 with ui.uninterruptable():
267 275 ds = repo.dirstate
268 276 p1, p2 = ds.p1(), ds.p2()
269 277 with ds.parentchange():
270 278 ds.setparents(node.nullid, node.nullid)
271 279 common = commoninc[0]
272 280 with wrappedextraprepare, repo.ui.configoverride(overrides, 'widen'):
273 281 exchange.pull(repo, remote, heads=common)
274 282 with ds.parentchange():
275 283 ds.setparents(p1, p2)
276 284
277 285 repo.setnewnarrowpats()
278 286 actions = {k: [] for k in 'a am f g cd dc r dm dg m e k p pr'.split()}
279 287 addgaction = actions['g'].append
280 288
281 289 mf = repo['.'].manifest().matches(newmatch)
282 290 for f, fn in mf.iteritems():
283 291 if f not in repo.dirstate:
284 292 addgaction((f, (mf.flags(f), False),
285 293 "add from widened narrow clone"))
286 294
287 295 merge.applyupdates(repo, actions, wctx=repo[None],
288 296 mctx=repo['.'], overwrite=False)
289 297 merge.recordupdates(repo, actions, branchmerge=False)
290 298
291 299 # TODO(rdamazio): Make new matcher format and update description
292 300 @command('tracked',
293 301 [('', 'addinclude', [], _('new paths to include')),
294 302 ('', 'removeinclude', [], _('old paths to no longer include')),
295 303 ('', 'addexclude', [], _('new paths to exclude')),
296 304 ('', 'import-rules', '', _('import narrowspecs from a file')),
297 305 ('', 'removeexclude', [], _('old paths to no longer exclude')),
298 306 ('', 'clear', False, _('whether to replace the existing narrowspec')),
299 307 ('', 'force-delete-local-changes', False,
300 308 _('forces deletion of local changes when narrowing')),
301 309 ] + commands.remoteopts,
302 310 _('[OPTIONS]... [REMOTE]'),
303 311 inferrepo=True)
304 312 def trackedcmd(ui, repo, remotepath=None, *pats, **opts):
305 313 """show or change the current narrowspec
306 314
307 315 With no argument, shows the current narrowspec entries, one per line. Each
308 316 line will be prefixed with 'I' or 'X' for included or excluded patterns,
309 317 respectively.
310 318
311 319 The narrowspec is comprised of expressions to match remote files and/or
312 320 directories that should be pulled into your client.
313 321 The narrowspec has *include* and *exclude* expressions, with excludes always
314 322 trumping includes: that is, if a file matches an exclude expression, it will
315 323 be excluded even if it also matches an include expression.
316 324 Excluding files that were never included has no effect.
317 325
318 326 Each included or excluded entry is in the format described by
319 327 'hg help patterns'.
320 328
321 329 The options allow you to add or remove included and excluded expressions.
322 330
323 331 If --clear is specified, then all previous includes and excludes are DROPPED
324 332 and replaced by the new ones specified to --addinclude and --addexclude.
325 333 If --clear is specified without any further options, the narrowspec will be
326 334 empty and will not match any files.
327 335 """
328 336 opts = pycompat.byteskwargs(opts)
329 337 if repository.NARROW_REQUIREMENT not in repo.requirements:
330 338 ui.warn(_('The narrow command is only supported on respositories cloned'
331 339 ' with --narrow.\n'))
332 340 return 1
333 341
334 342 # Before supporting, decide whether it "hg tracked --clear" should mean
335 343 # tracking no paths or all paths.
336 344 if opts['clear']:
337 345 ui.warn(_('The --clear option is not yet supported.\n'))
338 346 return 1
339 347
340 348 # import rules from a file
341 349 newrules = opts.get('import_rules')
342 350 if newrules:
343 351 try:
344 352 filepath = os.path.join(pycompat.getcwd(), newrules)
345 353 fdata = util.readfile(filepath)
346 354 except IOError as inst:
347 355 raise error.Abort(_("cannot read narrowspecs from '%s': %s") %
348 356 (filepath, encoding.strtolocal(inst.strerror)))
349 357 includepats, excludepats, profiles = sparse.parseconfig(ui, fdata,
350 358 'narrow')
351 359 if profiles:
352 360 raise error.Abort(_("including other spec files using '%include' "
353 361 "is not supported in narrowspec"))
354 362 opts['addinclude'].extend(includepats)
355 363 opts['addexclude'].extend(excludepats)
356 364
357 365 addedincludes = narrowspec.parsepatterns(opts['addinclude'])
358 366 removedincludes = narrowspec.parsepatterns(opts['removeinclude'])
359 367 addedexcludes = narrowspec.parsepatterns(opts['addexclude'])
360 368 removedexcludes = narrowspec.parsepatterns(opts['removeexclude'])
361 369 widening = addedincludes or removedexcludes
362 370 narrowing = removedincludes or addedexcludes
363 371 only_show = not widening and not narrowing
364 372
365 373 # Only print the current narrowspec.
366 374 if only_show:
367 375 include, exclude = repo.narrowpats
368 376
369 377 ui.pager('tracked')
370 378 fm = ui.formatter('narrow', opts)
371 379 for i in sorted(include):
372 380 fm.startitem()
373 381 fm.write('status', '%s ', 'I', label='narrow.included')
374 382 fm.write('pat', '%s\n', i, label='narrow.included')
375 383 for i in sorted(exclude):
376 384 fm.startitem()
377 385 fm.write('status', '%s ', 'X', label='narrow.excluded')
378 386 fm.write('pat', '%s\n', i, label='narrow.excluded')
379 387 fm.end()
380 388 return 0
381 389
382 390 with repo.wlock(), repo.lock():
383 391 cmdutil.bailifchanged(repo)
384 392
385 393 # Find the revisions we have in common with the remote. These will
386 394 # be used for finding local-only changes for narrowing. They will
387 395 # also define the set of revisions to update for widening.
388 396 remotepath = ui.expandpath(remotepath or 'default')
389 397 url, branches = hg.parseurl(remotepath)
390 398 ui.status(_('comparing with %s\n') % util.hidepassword(url))
391 399 remote = hg.peer(repo, opts, url)
392 400 commoninc = discovery.findcommonincoming(repo, remote)
393 401
394 402 oldincludes, oldexcludes = repo.narrowpats
395 403 if narrowing:
396 404 newincludes = oldincludes - removedincludes
397 405 newexcludes = oldexcludes | addedexcludes
398 406 _narrow(ui, repo, remote, commoninc, oldincludes, oldexcludes,
399 407 newincludes, newexcludes,
400 408 opts['force_delete_local_changes'])
401 409 # _narrow() updated the narrowspec and _widen() below needs to
402 410 # use the updated values as its base (otherwise removed includes
403 411 # and addedexcludes will be lost in the resulting narrowspec)
404 412 oldincludes = newincludes
405 413 oldexcludes = newexcludes
406 414
407 415 if widening:
408 416 newincludes = oldincludes | addedincludes
409 417 newexcludes = oldexcludes - removedexcludes
410 418 _widen(ui, repo, remote, commoninc, newincludes, newexcludes)
411 419
412 420 return 0
General Comments 0
You need to be logged in to leave comments. Login now