##// END OF EJS Templates
narrow: refactor code around widening complicated by previous patch...
Pulkit Goyal -
r42606:8381b706 default
parent child Browse files
Show More
@@ -1,493 +1,478
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 bundle2,
15 15 cmdutil,
16 16 commands,
17 17 discovery,
18 18 encoding,
19 19 error,
20 20 exchange,
21 21 extensions,
22 22 hg,
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 wireprototypes,
33 33 )
34 34
35 35 table = {}
36 36 command = registrar.command(table)
37 37
38 38 def setup():
39 39 """Wraps user-facing mercurial commands with narrow-aware versions."""
40 40
41 41 entry = extensions.wrapcommand(commands.table, 'clone', clonenarrowcmd)
42 42 entry[1].append(('', 'narrow', None,
43 43 _("create a narrow clone of select files")))
44 44 entry[1].append(('', 'depth', '',
45 45 _("limit the history fetched by distance from heads")))
46 46 entry[1].append(('', 'narrowspec', '',
47 47 _("read narrowspecs from file")))
48 48 # TODO(durin42): unify sparse/narrow --include/--exclude logic a bit
49 49 if 'sparse' not in extensions.enabled():
50 50 entry[1].append(('', 'include', [],
51 51 _("specifically fetch this file/directory")))
52 52 entry[1].append(
53 53 ('', 'exclude', [],
54 54 _("do not fetch this file/directory, even if included")))
55 55
56 56 entry = extensions.wrapcommand(commands.table, 'pull', pullnarrowcmd)
57 57 entry[1].append(('', 'depth', '',
58 58 _("limit the history fetched by distance from heads")))
59 59
60 60 extensions.wrapcommand(commands.table, 'archive', archivenarrowcmd)
61 61
62 62 def clonenarrowcmd(orig, ui, repo, *args, **opts):
63 63 """Wraps clone command, so 'hg clone' first wraps localrepo.clone()."""
64 64 opts = pycompat.byteskwargs(opts)
65 65 wrappedextraprepare = util.nullcontextmanager()
66 66 narrowspecfile = opts['narrowspec']
67 67
68 68 if narrowspecfile:
69 69 filepath = os.path.join(encoding.getcwd(), narrowspecfile)
70 70 ui.status(_("reading narrowspec from '%s'\n") % filepath)
71 71 try:
72 72 fdata = util.readfile(filepath)
73 73 except IOError as inst:
74 74 raise error.Abort(_("cannot read narrowspecs from '%s': %s") %
75 75 (filepath, encoding.strtolocal(inst.strerror)))
76 76
77 77 includes, excludes, profiles = sparse.parseconfig(ui, fdata, 'narrow')
78 78 if profiles:
79 79 raise error.Abort(_("cannot specify other files using '%include' in"
80 80 " narrowspec"))
81 81
82 82 narrowspec.validatepatterns(includes)
83 83 narrowspec.validatepatterns(excludes)
84 84
85 85 # narrowspec is passed so we should assume that user wants narrow clone
86 86 opts['narrow'] = True
87 87 opts['include'].extend(includes)
88 88 opts['exclude'].extend(excludes)
89 89
90 90 if opts['narrow']:
91 91 def pullbundle2extraprepare_widen(orig, pullop, kwargs):
92 92 orig(pullop, kwargs)
93 93
94 94 if opts.get('depth'):
95 95 kwargs['depth'] = opts['depth']
96 96 wrappedextraprepare = extensions.wrappedfunction(exchange,
97 97 '_pullbundle2extraprepare', pullbundle2extraprepare_widen)
98 98
99 99 with wrappedextraprepare:
100 100 return orig(ui, repo, *args, **pycompat.strkwargs(opts))
101 101
102 102 def pullnarrowcmd(orig, ui, repo, *args, **opts):
103 103 """Wraps pull command to allow modifying narrow spec."""
104 104 wrappedextraprepare = util.nullcontextmanager()
105 105 if repository.NARROW_REQUIREMENT in repo.requirements:
106 106
107 107 def pullbundle2extraprepare_widen(orig, pullop, kwargs):
108 108 orig(pullop, kwargs)
109 109 if opts.get(r'depth'):
110 110 kwargs['depth'] = opts[r'depth']
111 111 wrappedextraprepare = extensions.wrappedfunction(exchange,
112 112 '_pullbundle2extraprepare', pullbundle2extraprepare_widen)
113 113
114 114 with wrappedextraprepare:
115 115 return orig(ui, repo, *args, **opts)
116 116
117 117 def archivenarrowcmd(orig, ui, repo, *args, **opts):
118 118 """Wraps archive command to narrow the default includes."""
119 119 if repository.NARROW_REQUIREMENT in repo.requirements:
120 120 repo_includes, repo_excludes = repo.narrowpats
121 121 includes = set(opts.get(r'include', []))
122 122 excludes = set(opts.get(r'exclude', []))
123 123 includes, excludes, unused_invalid = narrowspec.restrictpatterns(
124 124 includes, excludes, repo_includes, repo_excludes)
125 125 if includes:
126 126 opts[r'include'] = includes
127 127 if excludes:
128 128 opts[r'exclude'] = excludes
129 129 return orig(ui, repo, *args, **opts)
130 130
131 131 def pullbundle2extraprepare(orig, pullop, kwargs):
132 132 repo = pullop.repo
133 133 if repository.NARROW_REQUIREMENT not in repo.requirements:
134 134 return orig(pullop, kwargs)
135 135
136 136 if wireprototypes.NARROWCAP not in pullop.remote.capabilities():
137 137 raise error.Abort(_("server does not support narrow clones"))
138 138 orig(pullop, kwargs)
139 139 kwargs['narrow'] = True
140 140 include, exclude = repo.narrowpats
141 141 kwargs['oldincludepats'] = include
142 142 kwargs['oldexcludepats'] = exclude
143 143 if include:
144 144 kwargs['includepats'] = include
145 145 if exclude:
146 146 kwargs['excludepats'] = exclude
147 147 # calculate known nodes only in ellipses cases because in non-ellipses cases
148 148 # we have all the nodes
149 149 if wireprototypes.ELLIPSESCAP1 in pullop.remote.capabilities():
150 150 kwargs['known'] = [node.hex(ctx.node()) for ctx in
151 151 repo.set('::%ln', pullop.common)
152 152 if ctx.node() != node.nullid]
153 153 if not kwargs['known']:
154 154 # Mercurial serializes an empty list as '' and deserializes it as
155 155 # [''], so delete it instead to avoid handling the empty string on
156 156 # the server.
157 157 del kwargs['known']
158 158
159 159 extensions.wrapfunction(exchange,'_pullbundle2extraprepare',
160 160 pullbundle2extraprepare)
161 161
162 162 def _narrow(ui, repo, remote, commoninc, oldincludes, oldexcludes,
163 163 newincludes, newexcludes, force):
164 164 oldmatch = narrowspec.match(repo.root, oldincludes, oldexcludes)
165 165 newmatch = narrowspec.match(repo.root, newincludes, newexcludes)
166 166
167 167 # This is essentially doing "hg outgoing" to find all local-only
168 168 # commits. We will then check that the local-only commits don't
169 169 # have any changes to files that will be untracked.
170 170 unfi = repo.unfiltered()
171 171 outgoing = discovery.findcommonoutgoing(unfi, remote,
172 172 commoninc=commoninc)
173 173 ui.status(_('looking for local changes to affected paths\n'))
174 174 localnodes = []
175 175 for n in itertools.chain(outgoing.missing, outgoing.excluded):
176 176 if any(oldmatch(f) and not newmatch(f) for f in unfi[n].files()):
177 177 localnodes.append(n)
178 178 revstostrip = unfi.revs('descendants(%ln)', localnodes)
179 179 hiddenrevs = repoview.filterrevs(repo, 'visible')
180 180 visibletostrip = list(repo.changelog.node(r)
181 181 for r in (revstostrip - hiddenrevs))
182 182 if visibletostrip:
183 183 ui.status(_('The following changeset(s) or their ancestors have '
184 184 'local changes not on the remote:\n'))
185 185 maxnodes = 10
186 186 if ui.verbose or len(visibletostrip) <= maxnodes:
187 187 for n in visibletostrip:
188 188 ui.status('%s\n' % node.short(n))
189 189 else:
190 190 for n in visibletostrip[:maxnodes]:
191 191 ui.status('%s\n' % node.short(n))
192 192 ui.status(_('...and %d more, use --verbose to list all\n') %
193 193 (len(visibletostrip) - maxnodes))
194 194 if not force:
195 195 raise error.Abort(_('local changes found'),
196 196 hint=_('use --force-delete-local-changes to '
197 197 'ignore'))
198 198
199 199 with ui.uninterruptible():
200 200 if revstostrip:
201 201 tostrip = [unfi.changelog.node(r) for r in revstostrip]
202 202 if repo['.'].node() in tostrip:
203 203 # stripping working copy, so move to a different commit first
204 204 urev = max(repo.revs('(::%n) - %ln + null',
205 205 repo['.'].node(), visibletostrip))
206 206 hg.clean(repo, urev)
207 207 overrides = {('devel', 'strip-obsmarkers'): False}
208 208 with ui.configoverride(overrides, 'narrow'):
209 209 repair.strip(ui, unfi, tostrip, topic='narrow')
210 210
211 211 todelete = []
212 212 for f, f2, size in repo.store.datafiles():
213 213 if f.startswith('data/'):
214 214 file = f[5:-2]
215 215 if not newmatch(file):
216 216 todelete.append(f)
217 217 elif f.startswith('meta/'):
218 218 dir = f[5:-13]
219 219 dirs = sorted(util.dirs({dir})) + [dir]
220 220 include = True
221 221 for d in dirs:
222 222 visit = newmatch.visitdir(d)
223 223 if not visit:
224 224 include = False
225 225 break
226 226 if visit == 'all':
227 227 break
228 228 if not include:
229 229 todelete.append(f)
230 230
231 231 repo.destroying()
232 232
233 233 with repo.transaction('narrowing'):
234 234 # Update narrowspec before removing revlogs, so repo won't be
235 235 # corrupt in case of crash
236 236 repo.setnarrowpats(newincludes, newexcludes)
237 237
238 238 for f in todelete:
239 239 ui.status(_('deleting %s\n') % f)
240 240 util.unlinkpath(repo.svfs.join(f))
241 241 repo.store.markremoved(f)
242 242
243 243 narrowspec.updateworkingcopy(repo, assumeclean=True)
244 244 narrowspec.copytoworkingcopy(repo)
245 245
246 246 repo.destroyed()
247 247
248 248 def _widen(ui, repo, remote, commoninc, oldincludes, oldexcludes,
249 249 newincludes, newexcludes):
250 250 # for now we assume that if a server has ellipses enabled, we will be
251 251 # exchanging ellipses nodes. In future we should add ellipses as a client
252 252 # side requirement (maybe) to distinguish a client is shallow or not and
253 253 # then send that information to server whether we want ellipses or not.
254 254 # Theoretically a non-ellipses repo should be able to use narrow
255 255 # functionality from an ellipses enabled server
256 256 remotecap = remote.capabilities()
257 257 ellipsesremote = any(cap in remotecap
258 258 for cap in wireprototypes.SUPPORTED_ELLIPSESCAP)
259 259
260 260 # check whether we are talking to a server which supports old version of
261 261 # ellipses capabilities
262 262 isoldellipses = (ellipsesremote and wireprototypes.ELLIPSESCAP1 in
263 263 remotecap and wireprototypes.ELLIPSESCAP not in remotecap)
264 264
265 265 def pullbundle2extraprepare_widen(orig, pullop, kwargs):
266 266 orig(pullop, kwargs)
267 267 # The old{in,ex}cludepats have already been set by orig()
268 268 kwargs['includepats'] = newincludes
269 269 kwargs['excludepats'] = newexcludes
270 270 wrappedextraprepare = extensions.wrappedfunction(exchange,
271 271 '_pullbundle2extraprepare', pullbundle2extraprepare_widen)
272 272
273 273 # define a function that narrowbundle2 can call after creating the
274 274 # backup bundle, but before applying the bundle from the server
275 275 def setnewnarrowpats():
276 276 repo.setnarrowpats(newincludes, newexcludes)
277 277 repo.setnewnarrowpats = setnewnarrowpats
278 278 # silence the devel-warning of applying an empty changegroup
279 279 overrides = {('devel', 'all-warnings'): False}
280 280
281 common = commoninc[0]
281 282 with ui.uninterruptible():
282 common = commoninc[0]
283 283 if ellipsesremote:
284 284 ds = repo.dirstate
285 285 p1, p2 = ds.p1(), ds.p2()
286 286 with ds.parentchange():
287 287 ds.setparents(node.nullid, node.nullid)
288 if isoldellipses:
288 289 with wrappedextraprepare:
289 if isoldellipses:
290 exchange.pull(repo, remote, heads=common)
291 else:
292 known = [node.hex(ctx.node()) for ctx in
293 repo.set('::%ln', common)
294 if ctx.node() != node.nullid]
295
296 with remote.commandexecutor() as e:
297 bundle = e.callcommand('narrow_widen', {
298 'oldincludes': oldincludes,
299 'oldexcludes': oldexcludes,
300 'newincludes': newincludes,
301 'newexcludes': newexcludes,
302 'cgversion': '03',
303 'commonheads': common,
304 'known': known,
305 'ellipses': True,
306 }).result()
307 trmanager = exchange.transactionmanager(repo, 'widen',
308 remote.url())
309 with trmanager:
310 op = bundle2.bundleoperation(repo,
311 trmanager.transaction, source='widen')
312 bundle2.processbundle(repo, bundle, op=op)
313
314 with ds.parentchange():
315 ds.setparents(p1, p2)
290 exchange.pull(repo, remote, heads=common)
316 291 else:
292 known = []
293 if ellipsesremote:
294 known = [node.hex(ctx.node()) for ctx in
295 repo.set('::%ln', common)
296 if ctx.node() != node.nullid]
317 297 with remote.commandexecutor() as e:
318 298 bundle = e.callcommand('narrow_widen', {
319 299 'oldincludes': oldincludes,
320 300 'oldexcludes': oldexcludes,
321 301 'newincludes': newincludes,
322 302 'newexcludes': newexcludes,
323 303 'cgversion': '03',
324 304 'commonheads': common,
325 'known': [],
326 'ellipses': False,
305 'known': known,
306 'ellipses': ellipsesremote,
327 307 }).result()
328 308
329 with repo.transaction('widening') as tr:
330 with repo.ui.configoverride(overrides, 'widen'):
331 tgetter = lambda: tr
332 bundle2.processbundle(repo, bundle,
333 transactiongetter=tgetter)
309 trmanager = exchange.transactionmanager(repo, 'widen', remote.url())
310 with trmanager, repo.ui.configoverride(overrides, 'widen'):
311 op = bundle2.bundleoperation(repo, trmanager.transaction,
312 source='widen')
313 # TODO: we should catch error.Abort here
314 bundle2.processbundle(repo, bundle, op=op)
315
316 if ellipsesremote:
317 with ds.parentchange():
318 ds.setparents(p1, p2)
334 319
335 320 with repo.transaction('widening'):
336 321 repo.setnewnarrowpats()
337 322 narrowspec.updateworkingcopy(repo)
338 323 narrowspec.copytoworkingcopy(repo)
339 324
340 325 # TODO(rdamazio): Make new matcher format and update description
341 326 @command('tracked',
342 327 [('', 'addinclude', [], _('new paths to include')),
343 328 ('', 'removeinclude', [], _('old paths to no longer include')),
344 329 ('', 'addexclude', [], _('new paths to exclude')),
345 330 ('', 'import-rules', '', _('import narrowspecs from a file')),
346 331 ('', 'removeexclude', [], _('old paths to no longer exclude')),
347 332 ('', 'clear', False, _('whether to replace the existing narrowspec')),
348 333 ('', 'force-delete-local-changes', False,
349 334 _('forces deletion of local changes when narrowing')),
350 335 ('', 'update-working-copy', False,
351 336 _('update working copy when the store has changed')),
352 337 ] + commands.remoteopts,
353 338 _('[OPTIONS]... [REMOTE]'),
354 339 inferrepo=True)
355 340 def trackedcmd(ui, repo, remotepath=None, *pats, **opts):
356 341 """show or change the current narrowspec
357 342
358 343 With no argument, shows the current narrowspec entries, one per line. Each
359 344 line will be prefixed with 'I' or 'X' for included or excluded patterns,
360 345 respectively.
361 346
362 347 The narrowspec is comprised of expressions to match remote files and/or
363 348 directories that should be pulled into your client.
364 349 The narrowspec has *include* and *exclude* expressions, with excludes always
365 350 trumping includes: that is, if a file matches an exclude expression, it will
366 351 be excluded even if it also matches an include expression.
367 352 Excluding files that were never included has no effect.
368 353
369 354 Each included or excluded entry is in the format described by
370 355 'hg help patterns'.
371 356
372 357 The options allow you to add or remove included and excluded expressions.
373 358
374 359 If --clear is specified, then all previous includes and excludes are DROPPED
375 360 and replaced by the new ones specified to --addinclude and --addexclude.
376 361 If --clear is specified without any further options, the narrowspec will be
377 362 empty and will not match any files.
378 363
379 364 --import-rules accepts a path to a file containing rules, allowing you to
380 365 add --addinclude, --addexclude rules in bulk. Like the other include and
381 366 exclude switches, the changes are applied immediately.
382 367 """
383 368 opts = pycompat.byteskwargs(opts)
384 369 if repository.NARROW_REQUIREMENT not in repo.requirements:
385 370 raise error.Abort(_('the tracked command is only supported on '
386 371 'respositories cloned with --narrow'))
387 372
388 373 # Before supporting, decide whether it "hg tracked --clear" should mean
389 374 # tracking no paths or all paths.
390 375 if opts['clear']:
391 376 raise error.Abort(_('the --clear option is not yet supported'))
392 377
393 378 # import rules from a file
394 379 newrules = opts.get('import_rules')
395 380 if newrules:
396 381 try:
397 382 filepath = os.path.join(encoding.getcwd(), newrules)
398 383 fdata = util.readfile(filepath)
399 384 except IOError as inst:
400 385 raise error.Abort(_("cannot read narrowspecs from '%s': %s") %
401 386 (filepath, encoding.strtolocal(inst.strerror)))
402 387 includepats, excludepats, profiles = sparse.parseconfig(ui, fdata,
403 388 'narrow')
404 389 if profiles:
405 390 raise error.Abort(_("including other spec files using '%include' "
406 391 "is not supported in narrowspec"))
407 392 opts['addinclude'].extend(includepats)
408 393 opts['addexclude'].extend(excludepats)
409 394
410 395 addedincludes = narrowspec.parsepatterns(opts['addinclude'])
411 396 removedincludes = narrowspec.parsepatterns(opts['removeinclude'])
412 397 addedexcludes = narrowspec.parsepatterns(opts['addexclude'])
413 398 removedexcludes = narrowspec.parsepatterns(opts['removeexclude'])
414 399
415 400 update_working_copy = opts['update_working_copy']
416 401 only_show = not (addedincludes or removedincludes or addedexcludes or
417 402 removedexcludes or newrules or update_working_copy)
418 403
419 404 oldincludes, oldexcludes = repo.narrowpats
420 405
421 406 # filter the user passed additions and deletions into actual additions and
422 407 # deletions of excludes and includes
423 408 addedincludes -= oldincludes
424 409 removedincludes &= oldincludes
425 410 addedexcludes -= oldexcludes
426 411 removedexcludes &= oldexcludes
427 412
428 413 widening = addedincludes or removedexcludes
429 414 narrowing = removedincludes or addedexcludes
430 415
431 416 # Only print the current narrowspec.
432 417 if only_show:
433 418 ui.pager('tracked')
434 419 fm = ui.formatter('narrow', opts)
435 420 for i in sorted(oldincludes):
436 421 fm.startitem()
437 422 fm.write('status', '%s ', 'I', label='narrow.included')
438 423 fm.write('pat', '%s\n', i, label='narrow.included')
439 424 for i in sorted(oldexcludes):
440 425 fm.startitem()
441 426 fm.write('status', '%s ', 'X', label='narrow.excluded')
442 427 fm.write('pat', '%s\n', i, label='narrow.excluded')
443 428 fm.end()
444 429 return 0
445 430
446 431 if update_working_copy:
447 432 with repo.wlock(), repo.lock(), repo.transaction('narrow-wc'):
448 433 narrowspec.updateworkingcopy(repo)
449 434 narrowspec.copytoworkingcopy(repo)
450 435 return 0
451 436
452 437 if not widening and not narrowing:
453 438 ui.status(_("nothing to widen or narrow\n"))
454 439 return 0
455 440
456 441 with repo.wlock(), repo.lock():
457 442 cmdutil.bailifchanged(repo)
458 443
459 444 # Find the revisions we have in common with the remote. These will
460 445 # be used for finding local-only changes for narrowing. They will
461 446 # also define the set of revisions to update for widening.
462 447 remotepath = ui.expandpath(remotepath or 'default')
463 448 url, branches = hg.parseurl(remotepath)
464 449 ui.status(_('comparing with %s\n') % util.hidepassword(url))
465 450 remote = hg.peer(repo, opts, url)
466 451
467 452 # check narrow support before doing anything if widening needs to be
468 453 # performed. In future we should also abort if client is ellipses and
469 454 # server does not support ellipses
470 455 if widening and wireprototypes.NARROWCAP not in remote.capabilities():
471 456 raise error.Abort(_("server does not support narrow clones"))
472 457
473 458 commoninc = discovery.findcommonincoming(repo, remote)
474 459
475 460 if narrowing:
476 461 newincludes = oldincludes - removedincludes
477 462 newexcludes = oldexcludes | addedexcludes
478 463 _narrow(ui, repo, remote, commoninc, oldincludes, oldexcludes,
479 464 newincludes, newexcludes,
480 465 opts['force_delete_local_changes'])
481 466 # _narrow() updated the narrowspec and _widen() below needs to
482 467 # use the updated values as its base (otherwise removed includes
483 468 # and addedexcludes will be lost in the resulting narrowspec)
484 469 oldincludes = newincludes
485 470 oldexcludes = newexcludes
486 471
487 472 if widening:
488 473 newincludes = oldincludes | addedincludes
489 474 newexcludes = oldexcludes - removedexcludes
490 475 _widen(ui, repo, remote, commoninc, oldincludes, oldexcludes,
491 476 newincludes, newexcludes)
492 477
493 478 return 0
General Comments 0
You need to be logged in to leave comments. Login now