##// END OF EJS Templates
subrepo: adjust subrepo prefix before calling subrepo.diff() (API)...
Martin von Zweigbergk -
r41779:3d094bfa default
parent child Browse files
Show More
@@ -1,919 +1,920 b''
1 1 # logcmdutil.py - utility for log-like commands
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
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
8 8 from __future__ import absolute_import
9 9
10 10 import itertools
11 11 import os
12 12
13 13 from .i18n import _
14 14 from .node import (
15 15 nullid,
16 16 wdirid,
17 17 wdirrev,
18 18 )
19 19
20 20 from . import (
21 21 dagop,
22 22 error,
23 23 formatter,
24 24 graphmod,
25 25 match as matchmod,
26 26 mdiff,
27 27 patch,
28 28 pathutil,
29 29 pycompat,
30 30 revset,
31 31 revsetlang,
32 32 scmutil,
33 33 smartset,
34 34 templatekw,
35 35 templater,
36 36 util,
37 37 )
38 38 from .utils import (
39 39 dateutil,
40 40 stringutil,
41 41 )
42 42
43 43 def getlimit(opts):
44 44 """get the log limit according to option -l/--limit"""
45 45 limit = opts.get('limit')
46 46 if limit:
47 47 try:
48 48 limit = int(limit)
49 49 except ValueError:
50 50 raise error.Abort(_('limit must be a positive integer'))
51 51 if limit <= 0:
52 52 raise error.Abort(_('limit must be positive'))
53 53 else:
54 54 limit = None
55 55 return limit
56 56
57 57 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
58 58 changes=None, stat=False, fp=None, graphwidth=0,
59 59 prefix='', root='', listsubrepos=False, hunksfilterfn=None):
60 60 '''show diff or diffstat.'''
61 61 ctx1 = repo[node1]
62 62 ctx2 = repo[node2]
63 63 if root:
64 64 relroot = pathutil.canonpath(repo.root, repo.getcwd(), root)
65 65 else:
66 66 relroot = ''
67 67 copysourcematch = None
68 68 if relroot != '':
69 69 # XXX relative roots currently don't work if the root is within a
70 70 # subrepo
71 71 uirelroot = match.uipath(relroot)
72 72 relroot += '/'
73 73 for matchroot in match.files():
74 74 if not matchroot.startswith(relroot):
75 75 ui.warn(_('warning: %s not inside relative root %s\n') % (
76 76 match.uipath(matchroot), uirelroot))
77 77
78 78 relrootmatch = scmutil.match(ctx2, pats=[relroot], default='path')
79 79 match = matchmod.intersectmatchers(match, relrootmatch)
80 80 copysourcematch = relrootmatch
81 81
82 82 if stat:
83 83 diffopts = diffopts.copy(context=0, noprefix=False)
84 84 width = 80
85 85 if not ui.plain():
86 86 width = ui.termwidth() - graphwidth
87 87
88 88 chunks = ctx2.diff(ctx1, match, changes, opts=diffopts, prefix=prefix,
89 89 relroot=relroot, copysourcematch=copysourcematch,
90 90 hunksfilterfn=hunksfilterfn)
91 91
92 92 if fp is not None or ui.canwritewithoutlabels():
93 93 out = fp or ui
94 94 if stat:
95 95 chunks = [patch.diffstat(util.iterlines(chunks), width=width)]
96 96 for chunk in util.filechunkiter(util.chunkbuffer(chunks)):
97 97 out.write(chunk)
98 98 else:
99 99 if stat:
100 100 chunks = patch.diffstatui(util.iterlines(chunks), width=width)
101 101 else:
102 102 chunks = patch.difflabel(lambda chunks, **kwargs: chunks, chunks,
103 103 opts=diffopts)
104 104 if ui.canbatchlabeledwrites():
105 105 def gen():
106 106 for chunk, label in chunks:
107 107 yield ui.label(chunk, label=label)
108 108 for chunk in util.filechunkiter(util.chunkbuffer(gen())):
109 109 ui.write(chunk)
110 110 else:
111 111 for chunk, label in chunks:
112 112 ui.write(chunk, label=label)
113 113
114 114 if listsubrepos:
115 115 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
116 116 tempnode2 = node2
117 117 try:
118 118 if node2 is not None:
119 119 tempnode2 = ctx2.substate[subpath][1]
120 120 except KeyError:
121 121 # A subrepo that existed in node1 was deleted between node1 and
122 122 # node2 (inclusive). Thus, ctx2's substate won't contain that
123 123 # subpath. The best we can do is to ignore it.
124 124 tempnode2 = None
125 125 submatch = matchmod.subdirmatcher(subpath, match)
126 subprefix = repo.wvfs.reljoin(prefix, subpath)
126 127 sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
127 stat=stat, fp=fp, prefix=prefix)
128 stat=stat, fp=fp, prefix=subprefix)
128 129
129 130 class changesetdiffer(object):
130 131 """Generate diff of changeset with pre-configured filtering functions"""
131 132
132 133 def _makefilematcher(self, ctx):
133 134 return scmutil.matchall(ctx.repo())
134 135
135 136 def _makehunksfilter(self, ctx):
136 137 return None
137 138
138 139 def showdiff(self, ui, ctx, diffopts, graphwidth=0, stat=False):
139 140 repo = ctx.repo()
140 141 node = ctx.node()
141 142 prev = ctx.p1().node()
142 143 diffordiffstat(ui, repo, diffopts, prev, node,
143 144 match=self._makefilematcher(ctx), stat=stat,
144 145 graphwidth=graphwidth,
145 146 hunksfilterfn=self._makehunksfilter(ctx))
146 147
147 148 def changesetlabels(ctx):
148 149 labels = ['log.changeset', 'changeset.%s' % ctx.phasestr()]
149 150 if ctx.obsolete():
150 151 labels.append('changeset.obsolete')
151 152 if ctx.isunstable():
152 153 labels.append('changeset.unstable')
153 154 for instability in ctx.instabilities():
154 155 labels.append('instability.%s' % instability)
155 156 return ' '.join(labels)
156 157
157 158 class changesetprinter(object):
158 159 '''show changeset information when templating not requested.'''
159 160
160 161 def __init__(self, ui, repo, differ=None, diffopts=None, buffered=False):
161 162 self.ui = ui
162 163 self.repo = repo
163 164 self.buffered = buffered
164 165 self._differ = differ or changesetdiffer()
165 166 self._diffopts = patch.diffallopts(ui, diffopts)
166 167 self._includestat = diffopts and diffopts.get('stat')
167 168 self._includediff = diffopts and diffopts.get('patch')
168 169 self.header = {}
169 170 self.hunk = {}
170 171 self.lastheader = None
171 172 self.footer = None
172 173 self._columns = templatekw.getlogcolumns()
173 174
174 175 def flush(self, ctx):
175 176 rev = ctx.rev()
176 177 if rev in self.header:
177 178 h = self.header[rev]
178 179 if h != self.lastheader:
179 180 self.lastheader = h
180 181 self.ui.write(h)
181 182 del self.header[rev]
182 183 if rev in self.hunk:
183 184 self.ui.write(self.hunk[rev])
184 185 del self.hunk[rev]
185 186
186 187 def close(self):
187 188 if self.footer:
188 189 self.ui.write(self.footer)
189 190
190 191 def show(self, ctx, copies=None, **props):
191 192 props = pycompat.byteskwargs(props)
192 193 if self.buffered:
193 194 self.ui.pushbuffer(labeled=True)
194 195 self._show(ctx, copies, props)
195 196 self.hunk[ctx.rev()] = self.ui.popbuffer()
196 197 else:
197 198 self._show(ctx, copies, props)
198 199
199 200 def _show(self, ctx, copies, props):
200 201 '''show a single changeset or file revision'''
201 202 changenode = ctx.node()
202 203 graphwidth = props.get('graphwidth', 0)
203 204
204 205 if self.ui.quiet:
205 206 self.ui.write("%s\n" % scmutil.formatchangeid(ctx),
206 207 label='log.node')
207 208 return
208 209
209 210 columns = self._columns
210 211 self.ui.write(columns['changeset'] % scmutil.formatchangeid(ctx),
211 212 label=changesetlabels(ctx))
212 213
213 214 # branches are shown first before any other names due to backwards
214 215 # compatibility
215 216 branch = ctx.branch()
216 217 # don't show the default branch name
217 218 if branch != 'default':
218 219 self.ui.write(columns['branch'] % branch, label='log.branch')
219 220
220 221 for nsname, ns in self.repo.names.iteritems():
221 222 # branches has special logic already handled above, so here we just
222 223 # skip it
223 224 if nsname == 'branches':
224 225 continue
225 226 # we will use the templatename as the color name since those two
226 227 # should be the same
227 228 for name in ns.names(self.repo, changenode):
228 229 self.ui.write(ns.logfmt % name,
229 230 label='log.%s' % ns.colorname)
230 231 if self.ui.debugflag:
231 232 self.ui.write(columns['phase'] % ctx.phasestr(), label='log.phase')
232 233 for pctx in scmutil.meaningfulparents(self.repo, ctx):
233 234 label = 'log.parent changeset.%s' % pctx.phasestr()
234 235 self.ui.write(columns['parent'] % scmutil.formatchangeid(pctx),
235 236 label=label)
236 237
237 238 if self.ui.debugflag:
238 239 mnode = ctx.manifestnode()
239 240 if mnode is None:
240 241 mnode = wdirid
241 242 mrev = wdirrev
242 243 else:
243 244 mrev = self.repo.manifestlog.rev(mnode)
244 245 self.ui.write(columns['manifest']
245 246 % scmutil.formatrevnode(self.ui, mrev, mnode),
246 247 label='ui.debug log.manifest')
247 248 self.ui.write(columns['user'] % ctx.user(), label='log.user')
248 249 self.ui.write(columns['date'] % dateutil.datestr(ctx.date()),
249 250 label='log.date')
250 251
251 252 if ctx.isunstable():
252 253 instabilities = ctx.instabilities()
253 254 self.ui.write(columns['instability'] % ', '.join(instabilities),
254 255 label='log.instability')
255 256
256 257 elif ctx.obsolete():
257 258 self._showobsfate(ctx)
258 259
259 260 self._exthook(ctx)
260 261
261 262 if self.ui.debugflag:
262 263 files = ctx.p1().status(ctx)[:3]
263 264 for key, value in zip(['files', 'files+', 'files-'], files):
264 265 if value:
265 266 self.ui.write(columns[key] % " ".join(value),
266 267 label='ui.debug log.files')
267 268 elif ctx.files() and self.ui.verbose:
268 269 self.ui.write(columns['files'] % " ".join(ctx.files()),
269 270 label='ui.note log.files')
270 271 if copies and self.ui.verbose:
271 272 copies = ['%s (%s)' % c for c in copies]
272 273 self.ui.write(columns['copies'] % ' '.join(copies),
273 274 label='ui.note log.copies')
274 275
275 276 extra = ctx.extra()
276 277 if extra and self.ui.debugflag:
277 278 for key, value in sorted(extra.items()):
278 279 self.ui.write(columns['extra']
279 280 % (key, stringutil.escapestr(value)),
280 281 label='ui.debug log.extra')
281 282
282 283 description = ctx.description().strip()
283 284 if description:
284 285 if self.ui.verbose:
285 286 self.ui.write(_("description:\n"),
286 287 label='ui.note log.description')
287 288 self.ui.write(description,
288 289 label='ui.note log.description')
289 290 self.ui.write("\n\n")
290 291 else:
291 292 self.ui.write(columns['summary'] % description.splitlines()[0],
292 293 label='log.summary')
293 294 self.ui.write("\n")
294 295
295 296 self._showpatch(ctx, graphwidth)
296 297
297 298 def _showobsfate(self, ctx):
298 299 # TODO: do not depend on templater
299 300 tres = formatter.templateresources(self.repo.ui, self.repo)
300 301 t = formatter.maketemplater(self.repo.ui, '{join(obsfate, "\n")}',
301 302 defaults=templatekw.keywords,
302 303 resources=tres)
303 304 obsfate = t.renderdefault({'ctx': ctx}).splitlines()
304 305
305 306 if obsfate:
306 307 for obsfateline in obsfate:
307 308 self.ui.write(self._columns['obsolete'] % obsfateline,
308 309 label='log.obsfate')
309 310
310 311 def _exthook(self, ctx):
311 312 '''empty method used by extension as a hook point
312 313 '''
313 314
314 315 def _showpatch(self, ctx, graphwidth=0):
315 316 if self._includestat:
316 317 self._differ.showdiff(self.ui, ctx, self._diffopts,
317 318 graphwidth, stat=True)
318 319 if self._includestat and self._includediff:
319 320 self.ui.write("\n")
320 321 if self._includediff:
321 322 self._differ.showdiff(self.ui, ctx, self._diffopts,
322 323 graphwidth, stat=False)
323 324 if self._includestat or self._includediff:
324 325 self.ui.write("\n")
325 326
326 327 class changesetformatter(changesetprinter):
327 328 """Format changeset information by generic formatter"""
328 329
329 330 def __init__(self, ui, repo, fm, differ=None, diffopts=None,
330 331 buffered=False):
331 332 changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered)
332 333 self._diffopts = patch.difffeatureopts(ui, diffopts, git=True)
333 334 self._fm = fm
334 335
335 336 def close(self):
336 337 self._fm.end()
337 338
338 339 def _show(self, ctx, copies, props):
339 340 '''show a single changeset or file revision'''
340 341 fm = self._fm
341 342 fm.startitem()
342 343 fm.context(ctx=ctx)
343 344 fm.data(rev=scmutil.intrev(ctx),
344 345 node=fm.hexfunc(scmutil.binnode(ctx)))
345 346
346 347 if self.ui.quiet:
347 348 return
348 349
349 350 fm.data(branch=ctx.branch(),
350 351 phase=ctx.phasestr(),
351 352 user=ctx.user(),
352 353 date=fm.formatdate(ctx.date()),
353 354 desc=ctx.description(),
354 355 bookmarks=fm.formatlist(ctx.bookmarks(), name='bookmark'),
355 356 tags=fm.formatlist(ctx.tags(), name='tag'),
356 357 parents=fm.formatlist([fm.hexfunc(c.node())
357 358 for c in ctx.parents()], name='node'))
358 359
359 360 if self.ui.debugflag:
360 361 fm.data(manifest=fm.hexfunc(ctx.manifestnode() or wdirid),
361 362 extra=fm.formatdict(ctx.extra()))
362 363
363 364 files = ctx.p1().status(ctx)
364 365 fm.data(modified=fm.formatlist(files[0], name='file'),
365 366 added=fm.formatlist(files[1], name='file'),
366 367 removed=fm.formatlist(files[2], name='file'))
367 368
368 369 elif self.ui.verbose:
369 370 fm.data(files=fm.formatlist(ctx.files(), name='file'))
370 371 if copies:
371 372 fm.data(copies=fm.formatdict(copies,
372 373 key='name', value='source'))
373 374
374 375 if self._includestat:
375 376 self.ui.pushbuffer()
376 377 self._differ.showdiff(self.ui, ctx, self._diffopts, stat=True)
377 378 fm.data(diffstat=self.ui.popbuffer())
378 379 if self._includediff:
379 380 self.ui.pushbuffer()
380 381 self._differ.showdiff(self.ui, ctx, self._diffopts, stat=False)
381 382 fm.data(diff=self.ui.popbuffer())
382 383
383 384 class changesettemplater(changesetprinter):
384 385 '''format changeset information.
385 386
386 387 Note: there are a variety of convenience functions to build a
387 388 changesettemplater for common cases. See functions such as:
388 389 maketemplater, changesetdisplayer, buildcommittemplate, or other
389 390 functions that use changesest_templater.
390 391 '''
391 392
392 393 # Arguments before "buffered" used to be positional. Consider not
393 394 # adding/removing arguments before "buffered" to not break callers.
394 395 def __init__(self, ui, repo, tmplspec, differ=None, diffopts=None,
395 396 buffered=False):
396 397 changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered)
397 398 # tres is shared with _graphnodeformatter()
398 399 self._tresources = tres = formatter.templateresources(ui, repo)
399 400 self.t = formatter.loadtemplater(ui, tmplspec,
400 401 defaults=templatekw.keywords,
401 402 resources=tres,
402 403 cache=templatekw.defaulttempl)
403 404 self._counter = itertools.count()
404 405
405 406 self._tref = tmplspec.ref
406 407 self._parts = {'header': '', 'footer': '',
407 408 tmplspec.ref: tmplspec.ref,
408 409 'docheader': '', 'docfooter': '',
409 410 'separator': ''}
410 411 if tmplspec.mapfile:
411 412 # find correct templates for current mode, for backward
412 413 # compatibility with 'log -v/-q/--debug' using a mapfile
413 414 tmplmodes = [
414 415 (True, ''),
415 416 (self.ui.verbose, '_verbose'),
416 417 (self.ui.quiet, '_quiet'),
417 418 (self.ui.debugflag, '_debug'),
418 419 ]
419 420 for mode, postfix in tmplmodes:
420 421 for t in self._parts:
421 422 cur = t + postfix
422 423 if mode and cur in self.t:
423 424 self._parts[t] = cur
424 425 else:
425 426 partnames = [p for p in self._parts.keys() if p != tmplspec.ref]
426 427 m = formatter.templatepartsmap(tmplspec, self.t, partnames)
427 428 self._parts.update(m)
428 429
429 430 if self._parts['docheader']:
430 431 self.ui.write(self.t.render(self._parts['docheader'], {}))
431 432
432 433 def close(self):
433 434 if self._parts['docfooter']:
434 435 if not self.footer:
435 436 self.footer = ""
436 437 self.footer += self.t.render(self._parts['docfooter'], {})
437 438 return super(changesettemplater, self).close()
438 439
439 440 def _show(self, ctx, copies, props):
440 441 '''show a single changeset or file revision'''
441 442 props = props.copy()
442 443 props['ctx'] = ctx
443 444 props['index'] = index = next(self._counter)
444 445 props['revcache'] = {'copies': copies}
445 446 graphwidth = props.get('graphwidth', 0)
446 447
447 448 # write separator, which wouldn't work well with the header part below
448 449 # since there's inherently a conflict between header (across items) and
449 450 # separator (per item)
450 451 if self._parts['separator'] and index > 0:
451 452 self.ui.write(self.t.render(self._parts['separator'], {}))
452 453
453 454 # write header
454 455 if self._parts['header']:
455 456 h = self.t.render(self._parts['header'], props)
456 457 if self.buffered:
457 458 self.header[ctx.rev()] = h
458 459 else:
459 460 if self.lastheader != h:
460 461 self.lastheader = h
461 462 self.ui.write(h)
462 463
463 464 # write changeset metadata, then patch if requested
464 465 key = self._parts[self._tref]
465 466 self.ui.write(self.t.render(key, props))
466 467 self._showpatch(ctx, graphwidth)
467 468
468 469 if self._parts['footer']:
469 470 if not self.footer:
470 471 self.footer = self.t.render(self._parts['footer'], props)
471 472
472 473 def templatespec(tmpl, mapfile):
473 474 if pycompat.ispy3:
474 475 assert not isinstance(tmpl, str), 'tmpl must not be a str'
475 476 if mapfile:
476 477 return formatter.templatespec('changeset', tmpl, mapfile)
477 478 else:
478 479 return formatter.templatespec('', tmpl, None)
479 480
480 481 def _lookuptemplate(ui, tmpl, style):
481 482 """Find the template matching the given template spec or style
482 483
483 484 See formatter.lookuptemplate() for details.
484 485 """
485 486
486 487 # ui settings
487 488 if not tmpl and not style: # template are stronger than style
488 489 tmpl = ui.config('ui', 'logtemplate')
489 490 if tmpl:
490 491 return templatespec(templater.unquotestring(tmpl), None)
491 492 else:
492 493 style = util.expandpath(ui.config('ui', 'style'))
493 494
494 495 if not tmpl and style:
495 496 mapfile = style
496 497 if not os.path.split(mapfile)[0]:
497 498 mapname = (templater.templatepath('map-cmdline.' + mapfile)
498 499 or templater.templatepath(mapfile))
499 500 if mapname:
500 501 mapfile = mapname
501 502 return templatespec(None, mapfile)
502 503
503 504 if not tmpl:
504 505 return templatespec(None, None)
505 506
506 507 return formatter.lookuptemplate(ui, 'changeset', tmpl)
507 508
508 509 def maketemplater(ui, repo, tmpl, buffered=False):
509 510 """Create a changesettemplater from a literal template 'tmpl'
510 511 byte-string."""
511 512 spec = templatespec(tmpl, None)
512 513 return changesettemplater(ui, repo, spec, buffered=buffered)
513 514
514 515 def changesetdisplayer(ui, repo, opts, differ=None, buffered=False):
515 516 """show one changeset using template or regular display.
516 517
517 518 Display format will be the first non-empty hit of:
518 519 1. option 'template'
519 520 2. option 'style'
520 521 3. [ui] setting 'logtemplate'
521 522 4. [ui] setting 'style'
522 523 If all of these values are either the unset or the empty string,
523 524 regular display via changesetprinter() is done.
524 525 """
525 526 postargs = (differ, opts, buffered)
526 527 if opts.get('template') == 'json':
527 528 fm = ui.formatter('log', opts)
528 529 return changesetformatter(ui, repo, fm, *postargs)
529 530
530 531 spec = _lookuptemplate(ui, opts.get('template'), opts.get('style'))
531 532
532 533 if not spec.ref and not spec.tmpl and not spec.mapfile:
533 534 return changesetprinter(ui, repo, *postargs)
534 535
535 536 return changesettemplater(ui, repo, spec, *postargs)
536 537
537 538 def _makematcher(repo, revs, pats, opts):
538 539 """Build matcher and expanded patterns from log options
539 540
540 541 If --follow, revs are the revisions to follow from.
541 542
542 543 Returns (match, pats, slowpath) where
543 544 - match: a matcher built from the given pats and -I/-X opts
544 545 - pats: patterns used (globs are expanded on Windows)
545 546 - slowpath: True if patterns aren't as simple as scanning filelogs
546 547 """
547 548 # pats/include/exclude are passed to match.match() directly in
548 549 # _matchfiles() revset but walkchangerevs() builds its matcher with
549 550 # scmutil.match(). The difference is input pats are globbed on
550 551 # platforms without shell expansion (windows).
551 552 wctx = repo[None]
552 553 match, pats = scmutil.matchandpats(wctx, pats, opts)
553 554 slowpath = match.anypats() or (not match.always() and opts.get('removed'))
554 555 if not slowpath:
555 556 follow = opts.get('follow') or opts.get('follow_first')
556 557 startctxs = []
557 558 if follow and opts.get('rev'):
558 559 startctxs = [repo[r] for r in revs]
559 560 for f in match.files():
560 561 if follow and startctxs:
561 562 # No idea if the path was a directory at that revision, so
562 563 # take the slow path.
563 564 if any(f not in c for c in startctxs):
564 565 slowpath = True
565 566 continue
566 567 elif follow and f not in wctx:
567 568 # If the file exists, it may be a directory, so let it
568 569 # take the slow path.
569 570 if os.path.exists(repo.wjoin(f)):
570 571 slowpath = True
571 572 continue
572 573 else:
573 574 raise error.Abort(_('cannot follow file not in parent '
574 575 'revision: "%s"') % f)
575 576 filelog = repo.file(f)
576 577 if not filelog:
577 578 # A zero count may be a directory or deleted file, so
578 579 # try to find matching entries on the slow path.
579 580 if follow:
580 581 raise error.Abort(
581 582 _('cannot follow nonexistent file: "%s"') % f)
582 583 slowpath = True
583 584
584 585 # We decided to fall back to the slowpath because at least one
585 586 # of the paths was not a file. Check to see if at least one of them
586 587 # existed in history - in that case, we'll continue down the
587 588 # slowpath; otherwise, we can turn off the slowpath
588 589 if slowpath:
589 590 for path in match.files():
590 591 if path == '.' or path in repo.store:
591 592 break
592 593 else:
593 594 slowpath = False
594 595
595 596 return match, pats, slowpath
596 597
597 598 def _fileancestors(repo, revs, match, followfirst):
598 599 fctxs = []
599 600 for r in revs:
600 601 ctx = repo[r]
601 602 fctxs.extend(ctx[f].introfilectx() for f in ctx.walk(match))
602 603
603 604 # When displaying a revision with --patch --follow FILE, we have
604 605 # to know which file of the revision must be diffed. With
605 606 # --follow, we want the names of the ancestors of FILE in the
606 607 # revision, stored in "fcache". "fcache" is populated as a side effect
607 608 # of the graph traversal.
608 609 fcache = {}
609 610 def filematcher(ctx):
610 611 return scmutil.matchfiles(repo, fcache.get(ctx.rev(), []))
611 612
612 613 def revgen():
613 614 for rev, cs in dagop.filectxancestors(fctxs, followfirst=followfirst):
614 615 fcache[rev] = [c.path() for c in cs]
615 616 yield rev
616 617 return smartset.generatorset(revgen(), iterasc=False), filematcher
617 618
618 619 def _makenofollowfilematcher(repo, pats, opts):
619 620 '''hook for extensions to override the filematcher for non-follow cases'''
620 621 return None
621 622
622 623 _opt2logrevset = {
623 624 'no_merges': ('not merge()', None),
624 625 'only_merges': ('merge()', None),
625 626 '_matchfiles': (None, '_matchfiles(%ps)'),
626 627 'date': ('date(%s)', None),
627 628 'branch': ('branch(%s)', '%lr'),
628 629 '_patslog': ('filelog(%s)', '%lr'),
629 630 'keyword': ('keyword(%s)', '%lr'),
630 631 'prune': ('ancestors(%s)', 'not %lr'),
631 632 'user': ('user(%s)', '%lr'),
632 633 }
633 634
634 635 def _makerevset(repo, match, pats, slowpath, opts):
635 636 """Return a revset string built from log options and file patterns"""
636 637 opts = dict(opts)
637 638 # follow or not follow?
638 639 follow = opts.get('follow') or opts.get('follow_first')
639 640
640 641 # branch and only_branch are really aliases and must be handled at
641 642 # the same time
642 643 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
643 644 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
644 645
645 646 if slowpath:
646 647 # See walkchangerevs() slow path.
647 648 #
648 649 # pats/include/exclude cannot be represented as separate
649 650 # revset expressions as their filtering logic applies at file
650 651 # level. For instance "-I a -X b" matches a revision touching
651 652 # "a" and "b" while "file(a) and not file(b)" does
652 653 # not. Besides, filesets are evaluated against the working
653 654 # directory.
654 655 matchargs = ['r:', 'd:relpath']
655 656 for p in pats:
656 657 matchargs.append('p:' + p)
657 658 for p in opts.get('include', []):
658 659 matchargs.append('i:' + p)
659 660 for p in opts.get('exclude', []):
660 661 matchargs.append('x:' + p)
661 662 opts['_matchfiles'] = matchargs
662 663 elif not follow:
663 664 opts['_patslog'] = list(pats)
664 665
665 666 expr = []
666 667 for op, val in sorted(opts.iteritems()):
667 668 if not val:
668 669 continue
669 670 if op not in _opt2logrevset:
670 671 continue
671 672 revop, listop = _opt2logrevset[op]
672 673 if revop and '%' not in revop:
673 674 expr.append(revop)
674 675 elif not listop:
675 676 expr.append(revsetlang.formatspec(revop, val))
676 677 else:
677 678 if revop:
678 679 val = [revsetlang.formatspec(revop, v) for v in val]
679 680 expr.append(revsetlang.formatspec(listop, val))
680 681
681 682 if expr:
682 683 expr = '(' + ' and '.join(expr) + ')'
683 684 else:
684 685 expr = None
685 686 return expr
686 687
687 688 def _initialrevs(repo, opts):
688 689 """Return the initial set of revisions to be filtered or followed"""
689 690 follow = opts.get('follow') or opts.get('follow_first')
690 691 if opts.get('rev'):
691 692 revs = scmutil.revrange(repo, opts['rev'])
692 693 elif follow and repo.dirstate.p1() == nullid:
693 694 revs = smartset.baseset()
694 695 elif follow:
695 696 revs = repo.revs('.')
696 697 else:
697 698 revs = smartset.spanset(repo)
698 699 revs.reverse()
699 700 return revs
700 701
701 702 def getrevs(repo, pats, opts):
702 703 """Return (revs, differ) where revs is a smartset
703 704
704 705 differ is a changesetdiffer with pre-configured file matcher.
705 706 """
706 707 follow = opts.get('follow') or opts.get('follow_first')
707 708 followfirst = opts.get('follow_first')
708 709 limit = getlimit(opts)
709 710 revs = _initialrevs(repo, opts)
710 711 if not revs:
711 712 return smartset.baseset(), None
712 713 match, pats, slowpath = _makematcher(repo, revs, pats, opts)
713 714 filematcher = None
714 715 if follow:
715 716 if slowpath or match.always():
716 717 revs = dagop.revancestors(repo, revs, followfirst=followfirst)
717 718 else:
718 719 revs, filematcher = _fileancestors(repo, revs, match, followfirst)
719 720 revs.reverse()
720 721 if filematcher is None:
721 722 filematcher = _makenofollowfilematcher(repo, pats, opts)
722 723 if filematcher is None:
723 724 def filematcher(ctx):
724 725 return match
725 726
726 727 expr = _makerevset(repo, match, pats, slowpath, opts)
727 728 if opts.get('graph') and opts.get('rev'):
728 729 # User-specified revs might be unsorted, but don't sort before
729 730 # _makerevset because it might depend on the order of revs
730 731 if not (revs.isdescending() or revs.istopo()):
731 732 revs.sort(reverse=True)
732 733 if expr:
733 734 matcher = revset.match(None, expr)
734 735 revs = matcher(repo, revs)
735 736 if limit is not None:
736 737 revs = revs.slice(0, limit)
737 738
738 739 differ = changesetdiffer()
739 740 differ._makefilematcher = filematcher
740 741 return revs, differ
741 742
742 743 def _parselinerangeopt(repo, opts):
743 744 """Parse --line-range log option and return a list of tuples (filename,
744 745 (fromline, toline)).
745 746 """
746 747 linerangebyfname = []
747 748 for pat in opts.get('line_range', []):
748 749 try:
749 750 pat, linerange = pat.rsplit(',', 1)
750 751 except ValueError:
751 752 raise error.Abort(_('malformatted line-range pattern %s') % pat)
752 753 try:
753 754 fromline, toline = map(int, linerange.split(':'))
754 755 except ValueError:
755 756 raise error.Abort(_("invalid line range for %s") % pat)
756 757 msg = _("line range pattern '%s' must match exactly one file") % pat
757 758 fname = scmutil.parsefollowlinespattern(repo, None, pat, msg)
758 759 linerangebyfname.append(
759 760 (fname, util.processlinerange(fromline, toline)))
760 761 return linerangebyfname
761 762
762 763 def getlinerangerevs(repo, userrevs, opts):
763 764 """Return (revs, differ).
764 765
765 766 "revs" are revisions obtained by processing "line-range" log options and
766 767 walking block ancestors of each specified file/line-range.
767 768
768 769 "differ" is a changesetdiffer with pre-configured file matcher and hunks
769 770 filter.
770 771 """
771 772 wctx = repo[None]
772 773
773 774 # Two-levels map of "rev -> file ctx -> [line range]".
774 775 linerangesbyrev = {}
775 776 for fname, (fromline, toline) in _parselinerangeopt(repo, opts):
776 777 if fname not in wctx:
777 778 raise error.Abort(_('cannot follow file not in parent '
778 779 'revision: "%s"') % fname)
779 780 fctx = wctx.filectx(fname)
780 781 for fctx, linerange in dagop.blockancestors(fctx, fromline, toline):
781 782 rev = fctx.introrev()
782 783 if rev not in userrevs:
783 784 continue
784 785 linerangesbyrev.setdefault(
785 786 rev, {}).setdefault(
786 787 fctx.path(), []).append(linerange)
787 788
788 789 def nofilterhunksfn(fctx, hunks):
789 790 return hunks
790 791
791 792 def hunksfilter(ctx):
792 793 fctxlineranges = linerangesbyrev.get(ctx.rev())
793 794 if fctxlineranges is None:
794 795 return nofilterhunksfn
795 796
796 797 def filterfn(fctx, hunks):
797 798 lineranges = fctxlineranges.get(fctx.path())
798 799 if lineranges is not None:
799 800 for hr, lines in hunks:
800 801 if hr is None: # binary
801 802 yield hr, lines
802 803 continue
803 804 if any(mdiff.hunkinrange(hr[2:], lr)
804 805 for lr in lineranges):
805 806 yield hr, lines
806 807 else:
807 808 for hunk in hunks:
808 809 yield hunk
809 810
810 811 return filterfn
811 812
812 813 def filematcher(ctx):
813 814 files = list(linerangesbyrev.get(ctx.rev(), []))
814 815 return scmutil.matchfiles(repo, files)
815 816
816 817 revs = sorted(linerangesbyrev, reverse=True)
817 818
818 819 differ = changesetdiffer()
819 820 differ._makefilematcher = filematcher
820 821 differ._makehunksfilter = hunksfilter
821 822 return revs, differ
822 823
823 824 def _graphnodeformatter(ui, displayer):
824 825 spec = ui.config('ui', 'graphnodetemplate')
825 826 if not spec:
826 827 return templatekw.getgraphnode # fast path for "{graphnode}"
827 828
828 829 spec = templater.unquotestring(spec)
829 830 if isinstance(displayer, changesettemplater):
830 831 # reuse cache of slow templates
831 832 tres = displayer._tresources
832 833 else:
833 834 tres = formatter.templateresources(ui)
834 835 templ = formatter.maketemplater(ui, spec, defaults=templatekw.keywords,
835 836 resources=tres)
836 837 def formatnode(repo, ctx):
837 838 props = {'ctx': ctx, 'repo': repo}
838 839 return templ.renderdefault(props)
839 840 return formatnode
840 841
841 842 def displaygraph(ui, repo, dag, displayer, edgefn, getrenamed=None, props=None):
842 843 props = props or {}
843 844 formatnode = _graphnodeformatter(ui, displayer)
844 845 state = graphmod.asciistate()
845 846 styles = state['styles']
846 847
847 848 # only set graph styling if HGPLAIN is not set.
848 849 if ui.plain('graph'):
849 850 # set all edge styles to |, the default pre-3.8 behaviour
850 851 styles.update(dict.fromkeys(styles, '|'))
851 852 else:
852 853 edgetypes = {
853 854 'parent': graphmod.PARENT,
854 855 'grandparent': graphmod.GRANDPARENT,
855 856 'missing': graphmod.MISSINGPARENT
856 857 }
857 858 for name, key in edgetypes.items():
858 859 # experimental config: experimental.graphstyle.*
859 860 styles[key] = ui.config('experimental', 'graphstyle.%s' % name,
860 861 styles[key])
861 862 if not styles[key]:
862 863 styles[key] = None
863 864
864 865 # experimental config: experimental.graphshorten
865 866 state['graphshorten'] = ui.configbool('experimental', 'graphshorten')
866 867
867 868 for rev, type, ctx, parents in dag:
868 869 char = formatnode(repo, ctx)
869 870 copies = None
870 871 if getrenamed and ctx.rev():
871 872 copies = []
872 873 for fn in ctx.files():
873 874 rename = getrenamed(fn, ctx.rev())
874 875 if rename:
875 876 copies.append((fn, rename))
876 877 edges = edgefn(type, char, state, rev, parents)
877 878 firstedge = next(edges)
878 879 width = firstedge[2]
879 880 displayer.show(ctx, copies=copies,
880 881 graphwidth=width, **pycompat.strkwargs(props))
881 882 lines = displayer.hunk.pop(rev).split('\n')
882 883 if not lines[-1]:
883 884 del lines[-1]
884 885 displayer.flush(ctx)
885 886 for type, char, width, coldata in itertools.chain([firstedge], edges):
886 887 graphmod.ascii(ui, state, type, char, lines, coldata)
887 888 lines = []
888 889 displayer.close()
889 890
890 891 def displaygraphrevs(ui, repo, revs, displayer, getrenamed):
891 892 revdag = graphmod.dagwalker(repo, revs)
892 893 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed)
893 894
894 895 def displayrevs(ui, repo, revs, displayer, getrenamed):
895 896 for rev in revs:
896 897 ctx = repo[rev]
897 898 copies = None
898 899 if getrenamed is not None and rev:
899 900 copies = []
900 901 for fn in ctx.files():
901 902 rename = getrenamed(fn, rev)
902 903 if rename:
903 904 copies.append((fn, rename))
904 905 displayer.show(ctx, copies=copies)
905 906 displayer.flush(ctx)
906 907 displayer.close()
907 908
908 909 def checkunsupportedgraphflags(pats, opts):
909 910 for op in ["newest_first"]:
910 911 if op in opts and opts[op]:
911 912 raise error.Abort(_("-G/--graph option is incompatible with --%s")
912 913 % op.replace("_", "-"))
913 914
914 915 def graphrevs(repo, nodes, opts):
915 916 limit = getlimit(opts)
916 917 nodes.reverse()
917 918 if limit is not None:
918 919 nodes = nodes[:limit]
919 920 return graphmod.nodes(repo, nodes)
@@ -1,1843 +1,1839 b''
1 1 # subrepo.py - sub-repository classes and factory
2 2 #
3 3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
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
8 8 from __future__ import absolute_import
9 9
10 10 import copy
11 11 import errno
12 12 import hashlib
13 13 import os
14 import posixpath
15 14 import re
16 15 import stat
17 16 import subprocess
18 17 import sys
19 18 import tarfile
20 19 import xml.dom.minidom
21 20
22 21 from .i18n import _
23 22 from . import (
24 23 cmdutil,
25 24 encoding,
26 25 error,
27 26 exchange,
28 27 logcmdutil,
29 28 match as matchmod,
30 29 node,
31 30 pathutil,
32 31 phases,
33 32 pycompat,
34 33 scmutil,
35 34 subrepoutil,
36 35 util,
37 36 vfs as vfsmod,
38 37 )
39 38 from .utils import (
40 39 dateutil,
41 40 procutil,
42 41 stringutil,
43 42 )
44 43
45 44 hg = None
46 45 reporelpath = subrepoutil.reporelpath
47 46 subrelpath = subrepoutil.subrelpath
48 47 _abssource = subrepoutil._abssource
49 48 propertycache = util.propertycache
50 49
51 50 def _expandedabspath(path):
52 51 '''
53 52 get a path or url and if it is a path expand it and return an absolute path
54 53 '''
55 54 expandedpath = util.urllocalpath(util.expandpath(path))
56 55 u = util.url(expandedpath)
57 56 if not u.scheme:
58 57 path = util.normpath(os.path.abspath(u.path))
59 58 return path
60 59
61 60 def _getstorehashcachename(remotepath):
62 61 '''get a unique filename for the store hash cache of a remote repository'''
63 62 return node.hex(hashlib.sha1(_expandedabspath(remotepath)).digest())[0:12]
64 63
65 64 class SubrepoAbort(error.Abort):
66 65 """Exception class used to avoid handling a subrepo error more than once"""
67 66 def __init__(self, *args, **kw):
68 67 self.subrepo = kw.pop(r'subrepo', None)
69 68 self.cause = kw.pop(r'cause', None)
70 69 error.Abort.__init__(self, *args, **kw)
71 70
72 71 def annotatesubrepoerror(func):
73 72 def decoratedmethod(self, *args, **kargs):
74 73 try:
75 74 res = func(self, *args, **kargs)
76 75 except SubrepoAbort as ex:
77 76 # This exception has already been handled
78 77 raise ex
79 78 except error.Abort as ex:
80 79 subrepo = subrelpath(self)
81 80 errormsg = (stringutil.forcebytestr(ex) + ' '
82 81 + _('(in subrepository "%s")') % subrepo)
83 82 # avoid handling this exception by raising a SubrepoAbort exception
84 83 raise SubrepoAbort(errormsg, hint=ex.hint, subrepo=subrepo,
85 84 cause=sys.exc_info())
86 85 return res
87 86 return decoratedmethod
88 87
89 88 def _updateprompt(ui, sub, dirty, local, remote):
90 89 if dirty:
91 90 msg = (_(' subrepository sources for %s differ\n'
92 91 'use (l)ocal source (%s) or (r)emote source (%s)?'
93 92 '$$ &Local $$ &Remote')
94 93 % (subrelpath(sub), local, remote))
95 94 else:
96 95 msg = (_(' subrepository sources for %s differ (in checked out '
97 96 'version)\n'
98 97 'use (l)ocal source (%s) or (r)emote source (%s)?'
99 98 '$$ &Local $$ &Remote')
100 99 % (subrelpath(sub), local, remote))
101 100 return ui.promptchoice(msg, 0)
102 101
103 102 def _sanitize(ui, vfs, ignore):
104 103 for dirname, dirs, names in vfs.walk():
105 104 for i, d in enumerate(dirs):
106 105 if d.lower() == ignore:
107 106 del dirs[i]
108 107 break
109 108 if vfs.basename(dirname).lower() != '.hg':
110 109 continue
111 110 for f in names:
112 111 if f.lower() == 'hgrc':
113 112 ui.warn(_("warning: removing potentially hostile 'hgrc' "
114 113 "in '%s'\n") % vfs.join(dirname))
115 114 vfs.unlink(vfs.reljoin(dirname, f))
116 115
117 116 def _auditsubrepopath(repo, path):
118 117 # sanity check for potentially unsafe paths such as '~' and '$FOO'
119 118 if path.startswith('~') or '$' in path or util.expandpath(path) != path:
120 119 raise error.Abort(_('subrepo path contains illegal component: %s')
121 120 % path)
122 121 # auditor doesn't check if the path itself is a symlink
123 122 pathutil.pathauditor(repo.root)(path)
124 123 if repo.wvfs.islink(path):
125 124 raise error.Abort(_("subrepo '%s' traverses symbolic link") % path)
126 125
127 126 SUBREPO_ALLOWED_DEFAULTS = {
128 127 'hg': True,
129 128 'git': False,
130 129 'svn': False,
131 130 }
132 131
133 132 def _checktype(ui, kind):
134 133 # subrepos.allowed is a master kill switch. If disabled, subrepos are
135 134 # disabled period.
136 135 if not ui.configbool('subrepos', 'allowed', True):
137 136 raise error.Abort(_('subrepos not enabled'),
138 137 hint=_("see 'hg help config.subrepos' for details"))
139 138
140 139 default = SUBREPO_ALLOWED_DEFAULTS.get(kind, False)
141 140 if not ui.configbool('subrepos', '%s:allowed' % kind, default):
142 141 raise error.Abort(_('%s subrepos not allowed') % kind,
143 142 hint=_("see 'hg help config.subrepos' for details"))
144 143
145 144 if kind not in types:
146 145 raise error.Abort(_('unknown subrepo type %s') % kind)
147 146
148 147 def subrepo(ctx, path, allowwdir=False, allowcreate=True):
149 148 """return instance of the right subrepo class for subrepo in path"""
150 149 # subrepo inherently violates our import layering rules
151 150 # because it wants to make repo objects from deep inside the stack
152 151 # so we manually delay the circular imports to not break
153 152 # scripts that don't use our demand-loading
154 153 global hg
155 154 from . import hg as h
156 155 hg = h
157 156
158 157 repo = ctx.repo()
159 158 _auditsubrepopath(repo, path)
160 159 state = ctx.substate[path]
161 160 _checktype(repo.ui, state[2])
162 161 if allowwdir:
163 162 state = (state[0], ctx.subrev(path), state[2])
164 163 return types[state[2]](ctx, path, state[:2], allowcreate)
165 164
166 165 def nullsubrepo(ctx, path, pctx):
167 166 """return an empty subrepo in pctx for the extant subrepo in ctx"""
168 167 # subrepo inherently violates our import layering rules
169 168 # because it wants to make repo objects from deep inside the stack
170 169 # so we manually delay the circular imports to not break
171 170 # scripts that don't use our demand-loading
172 171 global hg
173 172 from . import hg as h
174 173 hg = h
175 174
176 175 repo = ctx.repo()
177 176 _auditsubrepopath(repo, path)
178 177 state = ctx.substate[path]
179 178 _checktype(repo.ui, state[2])
180 179 subrev = ''
181 180 if state[2] == 'hg':
182 181 subrev = "0" * 40
183 182 return types[state[2]](pctx, path, (state[0], subrev), True)
184 183
185 184 # subrepo classes need to implement the following abstract class:
186 185
187 186 class abstractsubrepo(object):
188 187
189 188 def __init__(self, ctx, path):
190 189 """Initialize abstractsubrepo part
191 190
192 191 ``ctx`` is the context referring this subrepository in the
193 192 parent repository.
194 193
195 194 ``path`` is the path to this subrepository as seen from
196 195 innermost repository.
197 196 """
198 197 self.ui = ctx.repo().ui
199 198 self._ctx = ctx
200 199 self._path = path
201 200
202 201 def addwebdirpath(self, serverpath, webconf):
203 202 """Add the hgwebdir entries for this subrepo, and any of its subrepos.
204 203
205 204 ``serverpath`` is the path component of the URL for this repo.
206 205
207 206 ``webconf`` is the dictionary of hgwebdir entries.
208 207 """
209 208 pass
210 209
211 210 def storeclean(self, path):
212 211 """
213 212 returns true if the repository has not changed since it was last
214 213 cloned from or pushed to a given repository.
215 214 """
216 215 return False
217 216
218 217 def dirty(self, ignoreupdate=False, missing=False):
219 218 """returns true if the dirstate of the subrepo is dirty or does not
220 219 match current stored state. If ignoreupdate is true, only check
221 220 whether the subrepo has uncommitted changes in its dirstate. If missing
222 221 is true, check for deleted files.
223 222 """
224 223 raise NotImplementedError
225 224
226 225 def dirtyreason(self, ignoreupdate=False, missing=False):
227 226 """return reason string if it is ``dirty()``
228 227
229 228 Returned string should have enough information for the message
230 229 of exception.
231 230
232 231 This returns None, otherwise.
233 232 """
234 233 if self.dirty(ignoreupdate=ignoreupdate, missing=missing):
235 234 return _('uncommitted changes in subrepository "%s"'
236 235 ) % subrelpath(self)
237 236
238 237 def bailifchanged(self, ignoreupdate=False, hint=None):
239 238 """raise Abort if subrepository is ``dirty()``
240 239 """
241 240 dirtyreason = self.dirtyreason(ignoreupdate=ignoreupdate,
242 241 missing=True)
243 242 if dirtyreason:
244 243 raise error.Abort(dirtyreason, hint=hint)
245 244
246 245 def basestate(self):
247 246 """current working directory base state, disregarding .hgsubstate
248 247 state and working directory modifications"""
249 248 raise NotImplementedError
250 249
251 250 def checknested(self, path):
252 251 """check if path is a subrepository within this repository"""
253 252 return False
254 253
255 254 def commit(self, text, user, date):
256 255 """commit the current changes to the subrepo with the given
257 256 log message. Use given user and date if possible. Return the
258 257 new state of the subrepo.
259 258 """
260 259 raise NotImplementedError
261 260
262 261 def phase(self, state):
263 262 """returns phase of specified state in the subrepository.
264 263 """
265 264 return phases.public
266 265
267 266 def remove(self):
268 267 """remove the subrepo
269 268
270 269 (should verify the dirstate is not dirty first)
271 270 """
272 271 raise NotImplementedError
273 272
274 273 def get(self, state, overwrite=False):
275 274 """run whatever commands are needed to put the subrepo into
276 275 this state
277 276 """
278 277 raise NotImplementedError
279 278
280 279 def merge(self, state):
281 280 """merge currently-saved state with the new state."""
282 281 raise NotImplementedError
283 282
284 283 def push(self, opts):
285 284 """perform whatever action is analogous to 'hg push'
286 285
287 286 This may be a no-op on some systems.
288 287 """
289 288 raise NotImplementedError
290 289
291 290 def add(self, ui, match, prefix, explicitonly, **opts):
292 291 return []
293 292
294 293 def addremove(self, matcher, prefix, opts):
295 294 self.ui.warn("%s: %s" % (prefix, _("addremove is not supported")))
296 295 return 1
297 296
298 297 def cat(self, match, fm, fntemplate, prefix, **opts):
299 298 return 1
300 299
301 300 def status(self, rev2, **opts):
302 301 return scmutil.status([], [], [], [], [], [], [])
303 302
304 303 def diff(self, ui, diffopts, node2, match, prefix, **opts):
305 304 pass
306 305
307 306 def outgoing(self, ui, dest, opts):
308 307 return 1
309 308
310 309 def incoming(self, ui, source, opts):
311 310 return 1
312 311
313 312 def files(self):
314 313 """return filename iterator"""
315 314 raise NotImplementedError
316 315
317 316 def filedata(self, name, decode):
318 317 """return file data, optionally passed through repo decoders"""
319 318 raise NotImplementedError
320 319
321 320 def fileflags(self, name):
322 321 """return file flags"""
323 322 return ''
324 323
325 324 def matchfileset(self, expr, badfn=None):
326 325 """Resolve the fileset expression for this repo"""
327 326 return matchmod.nevermatcher(self.wvfs.base, '', badfn=badfn)
328 327
329 328 def printfiles(self, ui, m, fm, fmt, subrepos):
330 329 """handle the files command for this subrepo"""
331 330 return 1
332 331
333 332 def archive(self, archiver, prefix, match=None, decode=True):
334 333 if match is not None:
335 334 files = [f for f in self.files() if match(f)]
336 335 else:
337 336 files = self.files()
338 337 total = len(files)
339 338 relpath = subrelpath(self)
340 339 progress = self.ui.makeprogress(_('archiving (%s)') % relpath,
341 340 unit=_('files'), total=total)
342 341 progress.update(0)
343 342 for name in files:
344 343 flags = self.fileflags(name)
345 344 mode = 'x' in flags and 0o755 or 0o644
346 345 symlink = 'l' in flags
347 346 archiver.addfile(prefix + self._path + '/' + name,
348 347 mode, symlink, self.filedata(name, decode))
349 348 progress.increment()
350 349 progress.complete()
351 350 return total
352 351
353 352 def walk(self, match):
354 353 '''
355 354 walk recursively through the directory tree, finding all files
356 355 matched by the match function
357 356 '''
358 357
359 358 def forget(self, match, prefix, dryrun, interactive):
360 359 return ([], [])
361 360
362 361 def removefiles(self, matcher, prefix, after, force, subrepos,
363 362 dryrun, warnings):
364 363 """remove the matched files from the subrepository and the filesystem,
365 364 possibly by force and/or after the file has been removed from the
366 365 filesystem. Return 0 on success, 1 on any warning.
367 366 """
368 367 warnings.append(_("warning: removefiles not implemented (%s)")
369 368 % self._path)
370 369 return 1
371 370
372 371 def revert(self, substate, *pats, **opts):
373 372 self.ui.warn(_('%s: reverting %s subrepos is unsupported\n') \
374 373 % (substate[0], substate[2]))
375 374 return []
376 375
377 376 def shortid(self, revid):
378 377 return revid
379 378
380 379 def unshare(self):
381 380 '''
382 381 convert this repository from shared to normal storage.
383 382 '''
384 383
385 384 def verify(self):
386 385 '''verify the integrity of the repository. Return 0 on success or
387 386 warning, 1 on any error.
388 387 '''
389 388 return 0
390 389
391 390 @propertycache
392 391 def wvfs(self):
393 392 """return vfs to access the working directory of this subrepository
394 393 """
395 394 return vfsmod.vfs(self._ctx.repo().wvfs.join(self._path))
396 395
397 396 @propertycache
398 397 def _relpath(self):
399 398 """return path to this subrepository as seen from outermost repository
400 399 """
401 400 return self.wvfs.reljoin(reporelpath(self._ctx.repo()), self._path)
402 401
403 402 class hgsubrepo(abstractsubrepo):
404 403 def __init__(self, ctx, path, state, allowcreate):
405 404 super(hgsubrepo, self).__init__(ctx, path)
406 405 self._state = state
407 406 r = ctx.repo()
408 407 root = r.wjoin(path)
409 408 create = allowcreate and not r.wvfs.exists('%s/.hg' % path)
410 409 # repository constructor does expand variables in path, which is
411 410 # unsafe since subrepo path might come from untrusted source.
412 411 if os.path.realpath(util.expandpath(root)) != root:
413 412 raise error.Abort(_('subrepo path contains illegal component: %s')
414 413 % path)
415 414 self._repo = hg.repository(r.baseui, root, create=create)
416 415 if self._repo.root != root:
417 416 raise error.ProgrammingError('failed to reject unsafe subrepo '
418 417 'path: %s (expanded to %s)'
419 418 % (root, self._repo.root))
420 419
421 420 # Propagate the parent's --hidden option
422 421 if r is r.unfiltered():
423 422 self._repo = self._repo.unfiltered()
424 423
425 424 self.ui = self._repo.ui
426 425 for s, k in [('ui', 'commitsubrepos')]:
427 426 v = r.ui.config(s, k)
428 427 if v:
429 428 self.ui.setconfig(s, k, v, 'subrepo')
430 429 # internal config: ui._usedassubrepo
431 430 self.ui.setconfig('ui', '_usedassubrepo', 'True', 'subrepo')
432 431 self._initrepo(r, state[0], create)
433 432
434 433 @annotatesubrepoerror
435 434 def addwebdirpath(self, serverpath, webconf):
436 435 cmdutil.addwebdirpath(self._repo, subrelpath(self), webconf)
437 436
438 437 def storeclean(self, path):
439 438 with self._repo.lock():
440 439 return self._storeclean(path)
441 440
442 441 def _storeclean(self, path):
443 442 clean = True
444 443 itercache = self._calcstorehash(path)
445 444 for filehash in self._readstorehashcache(path):
446 445 if filehash != next(itercache, None):
447 446 clean = False
448 447 break
449 448 if clean:
450 449 # if not empty:
451 450 # the cached and current pull states have a different size
452 451 clean = next(itercache, None) is None
453 452 return clean
454 453
455 454 def _calcstorehash(self, remotepath):
456 455 '''calculate a unique "store hash"
457 456
458 457 This method is used to to detect when there are changes that may
459 458 require a push to a given remote path.'''
460 459 # sort the files that will be hashed in increasing (likely) file size
461 460 filelist = ('bookmarks', 'store/phaseroots', 'store/00changelog.i')
462 461 yield '# %s\n' % _expandedabspath(remotepath)
463 462 vfs = self._repo.vfs
464 463 for relname in filelist:
465 464 filehash = node.hex(hashlib.sha1(vfs.tryread(relname)).digest())
466 465 yield '%s = %s\n' % (relname, filehash)
467 466
468 467 @propertycache
469 468 def _cachestorehashvfs(self):
470 469 return vfsmod.vfs(self._repo.vfs.join('cache/storehash'))
471 470
472 471 def _readstorehashcache(self, remotepath):
473 472 '''read the store hash cache for a given remote repository'''
474 473 cachefile = _getstorehashcachename(remotepath)
475 474 return self._cachestorehashvfs.tryreadlines(cachefile, 'r')
476 475
477 476 def _cachestorehash(self, remotepath):
478 477 '''cache the current store hash
479 478
480 479 Each remote repo requires its own store hash cache, because a subrepo
481 480 store may be "clean" versus a given remote repo, but not versus another
482 481 '''
483 482 cachefile = _getstorehashcachename(remotepath)
484 483 with self._repo.lock():
485 484 storehash = list(self._calcstorehash(remotepath))
486 485 vfs = self._cachestorehashvfs
487 486 vfs.writelines(cachefile, storehash, mode='wb', notindexed=True)
488 487
489 488 def _getctx(self):
490 489 '''fetch the context for this subrepo revision, possibly a workingctx
491 490 '''
492 491 if self._ctx.rev() is None:
493 492 return self._repo[None] # workingctx if parent is workingctx
494 493 else:
495 494 rev = self._state[1]
496 495 return self._repo[rev]
497 496
498 497 @annotatesubrepoerror
499 498 def _initrepo(self, parentrepo, source, create):
500 499 self._repo._subparent = parentrepo
501 500 self._repo._subsource = source
502 501
503 502 if create:
504 503 lines = ['[paths]\n']
505 504
506 505 def addpathconfig(key, value):
507 506 if value:
508 507 lines.append('%s = %s\n' % (key, value))
509 508 self.ui.setconfig('paths', key, value, 'subrepo')
510 509
511 510 defpath = _abssource(self._repo, abort=False)
512 511 defpushpath = _abssource(self._repo, True, abort=False)
513 512 addpathconfig('default', defpath)
514 513 if defpath != defpushpath:
515 514 addpathconfig('default-push', defpushpath)
516 515
517 516 self._repo.vfs.write('hgrc', util.tonativeeol(''.join(lines)))
518 517
519 518 @annotatesubrepoerror
520 519 def add(self, ui, match, prefix, explicitonly, **opts):
521 520 return cmdutil.add(ui, self._repo, match, prefix, explicitonly, **opts)
522 521
523 522 @annotatesubrepoerror
524 523 def addremove(self, m, prefix, opts):
525 524 # In the same way as sub directories are processed, once in a subrepo,
526 525 # always entry any of its subrepos. Don't corrupt the options that will
527 526 # be used to process sibling subrepos however.
528 527 opts = copy.copy(opts)
529 528 opts['subrepos'] = True
530 529 return scmutil.addremove(self._repo, m, prefix, opts)
531 530
532 531 @annotatesubrepoerror
533 532 def cat(self, match, fm, fntemplate, prefix, **opts):
534 533 rev = self._state[1]
535 534 ctx = self._repo[rev]
536 535 return cmdutil.cat(self.ui, self._repo, ctx, match, fm, fntemplate,
537 536 prefix, **opts)
538 537
539 538 @annotatesubrepoerror
540 539 def status(self, rev2, **opts):
541 540 try:
542 541 rev1 = self._state[1]
543 542 ctx1 = self._repo[rev1]
544 543 ctx2 = self._repo[rev2]
545 544 return self._repo.status(ctx1, ctx2, **opts)
546 545 except error.RepoLookupError as inst:
547 546 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
548 547 % (inst, subrelpath(self)))
549 548 return scmutil.status([], [], [], [], [], [], [])
550 549
551 550 @annotatesubrepoerror
552 551 def diff(self, ui, diffopts, node2, match, prefix, **opts):
553 552 try:
554 553 node1 = node.bin(self._state[1])
555 554 # We currently expect node2 to come from substate and be
556 555 # in hex format
557 556 if node2 is not None:
558 557 node2 = node.bin(node2)
559 logcmdutil.diffordiffstat(ui, self._repo, diffopts,
560 node1, node2, match,
561 prefix=posixpath.join(prefix, self._path),
562 listsubrepos=True, **opts)
558 logcmdutil.diffordiffstat(ui, self._repo, diffopts, node1, node2,
559 match, prefix=prefix, listsubrepos=True,
560 **opts)
563 561 except error.RepoLookupError as inst:
564 562 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
565 563 % (inst, subrelpath(self)))
566 564
567 565 @annotatesubrepoerror
568 566 def archive(self, archiver, prefix, match=None, decode=True):
569 567 self._get(self._state + ('hg',))
570 568 files = self.files()
571 569 if match:
572 570 files = [f for f in files if match(f)]
573 571 rev = self._state[1]
574 572 ctx = self._repo[rev]
575 573 scmutil.prefetchfiles(self._repo, [ctx.rev()],
576 574 scmutil.matchfiles(self._repo, files))
577 575 total = abstractsubrepo.archive(self, archiver, prefix, match)
578 576 for subpath in ctx.substate:
579 577 s = subrepo(ctx, subpath, True)
580 578 submatch = matchmod.subdirmatcher(subpath, match)
581 579 total += s.archive(archiver, prefix + self._path + '/', submatch,
582 580 decode)
583 581 return total
584 582
585 583 @annotatesubrepoerror
586 584 def dirty(self, ignoreupdate=False, missing=False):
587 585 r = self._state[1]
588 586 if r == '' and not ignoreupdate: # no state recorded
589 587 return True
590 588 w = self._repo[None]
591 589 if r != w.p1().hex() and not ignoreupdate:
592 590 # different version checked out
593 591 return True
594 592 return w.dirty(missing=missing) # working directory changed
595 593
596 594 def basestate(self):
597 595 return self._repo['.'].hex()
598 596
599 597 def checknested(self, path):
600 598 return self._repo._checknested(self._repo.wjoin(path))
601 599
602 600 @annotatesubrepoerror
603 601 def commit(self, text, user, date):
604 602 # don't bother committing in the subrepo if it's only been
605 603 # updated
606 604 if not self.dirty(True):
607 605 return self._repo['.'].hex()
608 606 self.ui.debug("committing subrepo %s\n" % subrelpath(self))
609 607 n = self._repo.commit(text, user, date)
610 608 if not n:
611 609 return self._repo['.'].hex() # different version checked out
612 610 return node.hex(n)
613 611
614 612 @annotatesubrepoerror
615 613 def phase(self, state):
616 614 return self._repo[state or '.'].phase()
617 615
618 616 @annotatesubrepoerror
619 617 def remove(self):
620 618 # we can't fully delete the repository as it may contain
621 619 # local-only history
622 620 self.ui.note(_('removing subrepo %s\n') % subrelpath(self))
623 621 hg.clean(self._repo, node.nullid, False)
624 622
625 623 def _get(self, state):
626 624 source, revision, kind = state
627 625 parentrepo = self._repo._subparent
628 626
629 627 if revision in self._repo.unfiltered():
630 628 # Allow shared subrepos tracked at null to setup the sharedpath
631 629 if len(self._repo) != 0 or not parentrepo.shared():
632 630 return True
633 631 self._repo._subsource = source
634 632 srcurl = _abssource(self._repo)
635 633
636 634 # Defer creating the peer until after the status message is logged, in
637 635 # case there are network problems.
638 636 getpeer = lambda: hg.peer(self._repo, {}, srcurl)
639 637
640 638 if len(self._repo) == 0:
641 639 # use self._repo.vfs instead of self.wvfs to remove .hg only
642 640 self._repo.vfs.rmtree()
643 641
644 642 # A remote subrepo could be shared if there is a local copy
645 643 # relative to the parent's share source. But clone pooling doesn't
646 644 # assemble the repos in a tree, so that can't be consistently done.
647 645 # A simpler option is for the user to configure clone pooling, and
648 646 # work with that.
649 647 if parentrepo.shared() and hg.islocal(srcurl):
650 648 self.ui.status(_('sharing subrepo %s from %s\n')
651 649 % (subrelpath(self), srcurl))
652 650 shared = hg.share(self._repo._subparent.baseui,
653 651 getpeer(), self._repo.root,
654 652 update=False, bookmarks=False)
655 653 self._repo = shared.local()
656 654 else:
657 655 # TODO: find a common place for this and this code in the
658 656 # share.py wrap of the clone command.
659 657 if parentrepo.shared():
660 658 pool = self.ui.config('share', 'pool')
661 659 if pool:
662 660 pool = util.expandpath(pool)
663 661
664 662 shareopts = {
665 663 'pool': pool,
666 664 'mode': self.ui.config('share', 'poolnaming'),
667 665 }
668 666 else:
669 667 shareopts = {}
670 668
671 669 self.ui.status(_('cloning subrepo %s from %s\n')
672 670 % (subrelpath(self), util.hidepassword(srcurl)))
673 671 other, cloned = hg.clone(self._repo._subparent.baseui, {},
674 672 getpeer(), self._repo.root,
675 673 update=False, shareopts=shareopts)
676 674 self._repo = cloned.local()
677 675 self._initrepo(parentrepo, source, create=True)
678 676 self._cachestorehash(srcurl)
679 677 else:
680 678 self.ui.status(_('pulling subrepo %s from %s\n')
681 679 % (subrelpath(self), util.hidepassword(srcurl)))
682 680 cleansub = self.storeclean(srcurl)
683 681 exchange.pull(self._repo, getpeer())
684 682 if cleansub:
685 683 # keep the repo clean after pull
686 684 self._cachestorehash(srcurl)
687 685 return False
688 686
689 687 @annotatesubrepoerror
690 688 def get(self, state, overwrite=False):
691 689 inrepo = self._get(state)
692 690 source, revision, kind = state
693 691 repo = self._repo
694 692 repo.ui.debug("getting subrepo %s\n" % self._path)
695 693 if inrepo:
696 694 urepo = repo.unfiltered()
697 695 ctx = urepo[revision]
698 696 if ctx.hidden():
699 697 urepo.ui.warn(
700 698 _('revision %s in subrepository "%s" is hidden\n') \
701 699 % (revision[0:12], self._path))
702 700 repo = urepo
703 701 hg.updaterepo(repo, revision, overwrite)
704 702
705 703 @annotatesubrepoerror
706 704 def merge(self, state):
707 705 self._get(state)
708 706 cur = self._repo['.']
709 707 dst = self._repo[state[1]]
710 708 anc = dst.ancestor(cur)
711 709
712 710 def mergefunc():
713 711 if anc == cur and dst.branch() == cur.branch():
714 712 self.ui.debug('updating subrepository "%s"\n'
715 713 % subrelpath(self))
716 714 hg.update(self._repo, state[1])
717 715 elif anc == dst:
718 716 self.ui.debug('skipping subrepository "%s"\n'
719 717 % subrelpath(self))
720 718 else:
721 719 self.ui.debug('merging subrepository "%s"\n' % subrelpath(self))
722 720 hg.merge(self._repo, state[1], remind=False)
723 721
724 722 wctx = self._repo[None]
725 723 if self.dirty():
726 724 if anc != dst:
727 725 if _updateprompt(self.ui, self, wctx.dirty(), cur, dst):
728 726 mergefunc()
729 727 else:
730 728 mergefunc()
731 729 else:
732 730 mergefunc()
733 731
734 732 @annotatesubrepoerror
735 733 def push(self, opts):
736 734 force = opts.get('force')
737 735 newbranch = opts.get('new_branch')
738 736 ssh = opts.get('ssh')
739 737
740 738 # push subrepos depth-first for coherent ordering
741 739 c = self._repo['.']
742 740 subs = c.substate # only repos that are committed
743 741 for s in sorted(subs):
744 742 if c.sub(s).push(opts) == 0:
745 743 return False
746 744
747 745 dsturl = _abssource(self._repo, True)
748 746 if not force:
749 747 if self.storeclean(dsturl):
750 748 self.ui.status(
751 749 _('no changes made to subrepo %s since last push to %s\n')
752 750 % (subrelpath(self), util.hidepassword(dsturl)))
753 751 return None
754 752 self.ui.status(_('pushing subrepo %s to %s\n') %
755 753 (subrelpath(self), util.hidepassword(dsturl)))
756 754 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
757 755 res = exchange.push(self._repo, other, force, newbranch=newbranch)
758 756
759 757 # the repo is now clean
760 758 self._cachestorehash(dsturl)
761 759 return res.cgresult
762 760
763 761 @annotatesubrepoerror
764 762 def outgoing(self, ui, dest, opts):
765 763 if 'rev' in opts or 'branch' in opts:
766 764 opts = copy.copy(opts)
767 765 opts.pop('rev', None)
768 766 opts.pop('branch', None)
769 767 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
770 768
771 769 @annotatesubrepoerror
772 770 def incoming(self, ui, source, opts):
773 771 if 'rev' in opts or 'branch' in opts:
774 772 opts = copy.copy(opts)
775 773 opts.pop('rev', None)
776 774 opts.pop('branch', None)
777 775 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
778 776
779 777 @annotatesubrepoerror
780 778 def files(self):
781 779 rev = self._state[1]
782 780 ctx = self._repo[rev]
783 781 return ctx.manifest().keys()
784 782
785 783 def filedata(self, name, decode):
786 784 rev = self._state[1]
787 785 data = self._repo[rev][name].data()
788 786 if decode:
789 787 data = self._repo.wwritedata(name, data)
790 788 return data
791 789
792 790 def fileflags(self, name):
793 791 rev = self._state[1]
794 792 ctx = self._repo[rev]
795 793 return ctx.flags(name)
796 794
797 795 @annotatesubrepoerror
798 796 def printfiles(self, ui, m, fm, fmt, subrepos):
799 797 # If the parent context is a workingctx, use the workingctx here for
800 798 # consistency.
801 799 if self._ctx.rev() is None:
802 800 ctx = self._repo[None]
803 801 else:
804 802 rev = self._state[1]
805 803 ctx = self._repo[rev]
806 804 return cmdutil.files(ui, ctx, m, fm, fmt, subrepos)
807 805
808 806 @annotatesubrepoerror
809 807 def matchfileset(self, expr, badfn=None):
810 808 repo = self._repo
811 809 if self._ctx.rev() is None:
812 810 ctx = repo[None]
813 811 else:
814 812 rev = self._state[1]
815 813 ctx = repo[rev]
816 814
817 815 matchers = [ctx.matchfileset(expr, badfn=badfn)]
818 816
819 817 for subpath in ctx.substate:
820 818 sub = ctx.sub(subpath)
821 819
822 820 try:
823 821 sm = sub.matchfileset(expr, badfn=badfn)
824 822 pm = matchmod.prefixdirmatcher(repo.root, repo.getcwd(),
825 823 subpath, sm, badfn=badfn)
826 824 matchers.append(pm)
827 825 except error.LookupError:
828 826 self.ui.status(_("skipping missing subrepository: %s\n")
829 827 % self.wvfs.reljoin(reporelpath(self), subpath))
830 828 if len(matchers) == 1:
831 829 return matchers[0]
832 830 return matchmod.unionmatcher(matchers)
833 831
834 832 def walk(self, match):
835 833 ctx = self._repo[None]
836 834 return ctx.walk(match)
837 835
838 836 @annotatesubrepoerror
839 837 def forget(self, match, prefix, dryrun, interactive):
840 838 return cmdutil.forget(self.ui, self._repo, match, prefix,
841 839 True, dryrun=dryrun, interactive=interactive)
842 840
843 841 @annotatesubrepoerror
844 842 def removefiles(self, matcher, prefix, after, force, subrepos,
845 843 dryrun, warnings):
846 844 return cmdutil.remove(self.ui, self._repo, matcher, prefix,
847 845 after, force, subrepos, dryrun)
848 846
849 847 @annotatesubrepoerror
850 848 def revert(self, substate, *pats, **opts):
851 849 # reverting a subrepo is a 2 step process:
852 850 # 1. if the no_backup is not set, revert all modified
853 851 # files inside the subrepo
854 852 # 2. update the subrepo to the revision specified in
855 853 # the corresponding substate dictionary
856 854 self.ui.status(_('reverting subrepo %s\n') % substate[0])
857 855 if not opts.get(r'no_backup'):
858 856 # Revert all files on the subrepo, creating backups
859 857 # Note that this will not recursively revert subrepos
860 858 # We could do it if there was a set:subrepos() predicate
861 859 opts = opts.copy()
862 860 opts[r'date'] = None
863 861 opts[r'rev'] = substate[1]
864 862
865 863 self.filerevert(*pats, **opts)
866 864
867 865 # Update the repo to the revision specified in the given substate
868 866 if not opts.get(r'dry_run'):
869 867 self.get(substate, overwrite=True)
870 868
871 869 def filerevert(self, *pats, **opts):
872 870 ctx = self._repo[opts[r'rev']]
873 871 parents = self._repo.dirstate.parents()
874 872 if opts.get(r'all'):
875 873 pats = ['set:modified()']
876 874 else:
877 875 pats = []
878 876 cmdutil.revert(self.ui, self._repo, ctx, parents, *pats, **opts)
879 877
880 878 def shortid(self, revid):
881 879 return revid[:12]
882 880
883 881 @annotatesubrepoerror
884 882 def unshare(self):
885 883 # subrepo inherently violates our import layering rules
886 884 # because it wants to make repo objects from deep inside the stack
887 885 # so we manually delay the circular imports to not break
888 886 # scripts that don't use our demand-loading
889 887 global hg
890 888 from . import hg as h
891 889 hg = h
892 890
893 891 # Nothing prevents a user from sharing in a repo, and then making that a
894 892 # subrepo. Alternately, the previous unshare attempt may have failed
895 893 # part way through. So recurse whether or not this layer is shared.
896 894 if self._repo.shared():
897 895 self.ui.status(_("unsharing subrepo '%s'\n") % self._relpath)
898 896
899 897 hg.unshare(self.ui, self._repo)
900 898
901 899 def verify(self):
902 900 try:
903 901 rev = self._state[1]
904 902 ctx = self._repo.unfiltered()[rev]
905 903 if ctx.hidden():
906 904 # Since hidden revisions aren't pushed/pulled, it seems worth an
907 905 # explicit warning.
908 906 ui = self._repo.ui
909 907 ui.warn(_("subrepo '%s' is hidden in revision %s\n") %
910 908 (self._relpath, node.short(self._ctx.node())))
911 909 return 0
912 910 except error.RepoLookupError:
913 911 # A missing subrepo revision may be a case of needing to pull it, so
914 912 # don't treat this as an error.
915 913 self._repo.ui.warn(_("subrepo '%s' not found in revision %s\n") %
916 914 (self._relpath, node.short(self._ctx.node())))
917 915 return 0
918 916
919 917 @propertycache
920 918 def wvfs(self):
921 919 """return own wvfs for efficiency and consistency
922 920 """
923 921 return self._repo.wvfs
924 922
925 923 @propertycache
926 924 def _relpath(self):
927 925 """return path to this subrepository as seen from outermost repository
928 926 """
929 927 # Keep consistent dir separators by avoiding vfs.join(self._path)
930 928 return reporelpath(self._repo)
931 929
932 930 class svnsubrepo(abstractsubrepo):
933 931 def __init__(self, ctx, path, state, allowcreate):
934 932 super(svnsubrepo, self).__init__(ctx, path)
935 933 self._state = state
936 934 self._exe = procutil.findexe('svn')
937 935 if not self._exe:
938 936 raise error.Abort(_("'svn' executable not found for subrepo '%s'")
939 937 % self._path)
940 938
941 939 def _svncommand(self, commands, filename='', failok=False):
942 940 cmd = [self._exe]
943 941 extrakw = {}
944 942 if not self.ui.interactive():
945 943 # Making stdin be a pipe should prevent svn from behaving
946 944 # interactively even if we can't pass --non-interactive.
947 945 extrakw[r'stdin'] = subprocess.PIPE
948 946 # Starting in svn 1.5 --non-interactive is a global flag
949 947 # instead of being per-command, but we need to support 1.4 so
950 948 # we have to be intelligent about what commands take
951 949 # --non-interactive.
952 950 if commands[0] in ('update', 'checkout', 'commit'):
953 951 cmd.append('--non-interactive')
954 952 cmd.extend(commands)
955 953 if filename is not None:
956 954 path = self.wvfs.reljoin(self._ctx.repo().origroot,
957 955 self._path, filename)
958 956 cmd.append(path)
959 957 env = dict(encoding.environ)
960 958 # Avoid localized output, preserve current locale for everything else.
961 959 lc_all = env.get('LC_ALL')
962 960 if lc_all:
963 961 env['LANG'] = lc_all
964 962 del env['LC_ALL']
965 963 env['LC_MESSAGES'] = 'C'
966 964 p = subprocess.Popen(pycompat.rapply(procutil.tonativestr, cmd),
967 965 bufsize=-1, close_fds=procutil.closefds,
968 966 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
969 967 env=procutil.tonativeenv(env), **extrakw)
970 968 stdout, stderr = map(util.fromnativeeol, p.communicate())
971 969 stderr = stderr.strip()
972 970 if not failok:
973 971 if p.returncode:
974 972 raise error.Abort(stderr or 'exited with code %d'
975 973 % p.returncode)
976 974 if stderr:
977 975 self.ui.warn(stderr + '\n')
978 976 return stdout, stderr
979 977
980 978 @propertycache
981 979 def _svnversion(self):
982 980 output, err = self._svncommand(['--version', '--quiet'], filename=None)
983 981 m = re.search(br'^(\d+)\.(\d+)', output)
984 982 if not m:
985 983 raise error.Abort(_('cannot retrieve svn tool version'))
986 984 return (int(m.group(1)), int(m.group(2)))
987 985
988 986 def _svnmissing(self):
989 987 return not self.wvfs.exists('.svn')
990 988
991 989 def _wcrevs(self):
992 990 # Get the working directory revision as well as the last
993 991 # commit revision so we can compare the subrepo state with
994 992 # both. We used to store the working directory one.
995 993 output, err = self._svncommand(['info', '--xml'])
996 994 doc = xml.dom.minidom.parseString(output)
997 995 entries = doc.getElementsByTagName(r'entry')
998 996 lastrev, rev = '0', '0'
999 997 if entries:
1000 998 rev = pycompat.bytestr(entries[0].getAttribute(r'revision')) or '0'
1001 999 commits = entries[0].getElementsByTagName(r'commit')
1002 1000 if commits:
1003 1001 lastrev = pycompat.bytestr(
1004 1002 commits[0].getAttribute(r'revision')) or '0'
1005 1003 return (lastrev, rev)
1006 1004
1007 1005 def _wcrev(self):
1008 1006 return self._wcrevs()[0]
1009 1007
1010 1008 def _wcchanged(self):
1011 1009 """Return (changes, extchanges, missing) where changes is True
1012 1010 if the working directory was changed, extchanges is
1013 1011 True if any of these changes concern an external entry and missing
1014 1012 is True if any change is a missing entry.
1015 1013 """
1016 1014 output, err = self._svncommand(['status', '--xml'])
1017 1015 externals, changes, missing = [], [], []
1018 1016 doc = xml.dom.minidom.parseString(output)
1019 1017 for e in doc.getElementsByTagName(r'entry'):
1020 1018 s = e.getElementsByTagName(r'wc-status')
1021 1019 if not s:
1022 1020 continue
1023 1021 item = s[0].getAttribute(r'item')
1024 1022 props = s[0].getAttribute(r'props')
1025 1023 path = e.getAttribute(r'path').encode('utf8')
1026 1024 if item == r'external':
1027 1025 externals.append(path)
1028 1026 elif item == r'missing':
1029 1027 missing.append(path)
1030 1028 if (item not in (r'', r'normal', r'unversioned', r'external')
1031 1029 or props not in (r'', r'none', r'normal')):
1032 1030 changes.append(path)
1033 1031 for path in changes:
1034 1032 for ext in externals:
1035 1033 if path == ext or path.startswith(ext + pycompat.ossep):
1036 1034 return True, True, bool(missing)
1037 1035 return bool(changes), False, bool(missing)
1038 1036
1039 1037 @annotatesubrepoerror
1040 1038 def dirty(self, ignoreupdate=False, missing=False):
1041 1039 if self._svnmissing():
1042 1040 return self._state[1] != ''
1043 1041 wcchanged = self._wcchanged()
1044 1042 changed = wcchanged[0] or (missing and wcchanged[2])
1045 1043 if not changed:
1046 1044 if self._state[1] in self._wcrevs() or ignoreupdate:
1047 1045 return False
1048 1046 return True
1049 1047
1050 1048 def basestate(self):
1051 1049 lastrev, rev = self._wcrevs()
1052 1050 if lastrev != rev:
1053 1051 # Last committed rev is not the same than rev. We would
1054 1052 # like to take lastrev but we do not know if the subrepo
1055 1053 # URL exists at lastrev. Test it and fallback to rev it
1056 1054 # is not there.
1057 1055 try:
1058 1056 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
1059 1057 return lastrev
1060 1058 except error.Abort:
1061 1059 pass
1062 1060 return rev
1063 1061
1064 1062 @annotatesubrepoerror
1065 1063 def commit(self, text, user, date):
1066 1064 # user and date are out of our hands since svn is centralized
1067 1065 changed, extchanged, missing = self._wcchanged()
1068 1066 if not changed:
1069 1067 return self.basestate()
1070 1068 if extchanged:
1071 1069 # Do not try to commit externals
1072 1070 raise error.Abort(_('cannot commit svn externals'))
1073 1071 if missing:
1074 1072 # svn can commit with missing entries but aborting like hg
1075 1073 # seems a better approach.
1076 1074 raise error.Abort(_('cannot commit missing svn entries'))
1077 1075 commitinfo, err = self._svncommand(['commit', '-m', text])
1078 1076 self.ui.status(commitinfo)
1079 1077 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
1080 1078 if not newrev:
1081 1079 if not commitinfo.strip():
1082 1080 # Sometimes, our definition of "changed" differs from
1083 1081 # svn one. For instance, svn ignores missing files
1084 1082 # when committing. If there are only missing files, no
1085 1083 # commit is made, no output and no error code.
1086 1084 raise error.Abort(_('failed to commit svn changes'))
1087 1085 raise error.Abort(commitinfo.splitlines()[-1])
1088 1086 newrev = newrev.groups()[0]
1089 1087 self.ui.status(self._svncommand(['update', '-r', newrev])[0])
1090 1088 return newrev
1091 1089
1092 1090 @annotatesubrepoerror
1093 1091 def remove(self):
1094 1092 if self.dirty():
1095 1093 self.ui.warn(_('not removing repo %s because '
1096 1094 'it has changes.\n') % self._path)
1097 1095 return
1098 1096 self.ui.note(_('removing subrepo %s\n') % self._path)
1099 1097
1100 1098 self.wvfs.rmtree(forcibly=True)
1101 1099 try:
1102 1100 pwvfs = self._ctx.repo().wvfs
1103 1101 pwvfs.removedirs(pwvfs.dirname(self._path))
1104 1102 except OSError:
1105 1103 pass
1106 1104
1107 1105 @annotatesubrepoerror
1108 1106 def get(self, state, overwrite=False):
1109 1107 if overwrite:
1110 1108 self._svncommand(['revert', '--recursive'])
1111 1109 args = ['checkout']
1112 1110 if self._svnversion >= (1, 5):
1113 1111 args.append('--force')
1114 1112 # The revision must be specified at the end of the URL to properly
1115 1113 # update to a directory which has since been deleted and recreated.
1116 1114 args.append('%s@%s' % (state[0], state[1]))
1117 1115
1118 1116 # SEC: check that the ssh url is safe
1119 1117 util.checksafessh(state[0])
1120 1118
1121 1119 status, err = self._svncommand(args, failok=True)
1122 1120 _sanitize(self.ui, self.wvfs, '.svn')
1123 1121 if not re.search('Checked out revision [0-9]+.', status):
1124 1122 if ('is already a working copy for a different URL' in err
1125 1123 and (self._wcchanged()[:2] == (False, False))):
1126 1124 # obstructed but clean working copy, so just blow it away.
1127 1125 self.remove()
1128 1126 self.get(state, overwrite=False)
1129 1127 return
1130 1128 raise error.Abort((status or err).splitlines()[-1])
1131 1129 self.ui.status(status)
1132 1130
1133 1131 @annotatesubrepoerror
1134 1132 def merge(self, state):
1135 1133 old = self._state[1]
1136 1134 new = state[1]
1137 1135 wcrev = self._wcrev()
1138 1136 if new != wcrev:
1139 1137 dirty = old == wcrev or self._wcchanged()[0]
1140 1138 if _updateprompt(self.ui, self, dirty, wcrev, new):
1141 1139 self.get(state, False)
1142 1140
1143 1141 def push(self, opts):
1144 1142 # push is a no-op for SVN
1145 1143 return True
1146 1144
1147 1145 @annotatesubrepoerror
1148 1146 def files(self):
1149 1147 output = self._svncommand(['list', '--recursive', '--xml'])[0]
1150 1148 doc = xml.dom.minidom.parseString(output)
1151 1149 paths = []
1152 1150 for e in doc.getElementsByTagName(r'entry'):
1153 1151 kind = pycompat.bytestr(e.getAttribute(r'kind'))
1154 1152 if kind != 'file':
1155 1153 continue
1156 1154 name = r''.join(c.data for c
1157 1155 in e.getElementsByTagName(r'name')[0].childNodes
1158 1156 if c.nodeType == c.TEXT_NODE)
1159 1157 paths.append(name.encode('utf8'))
1160 1158 return paths
1161 1159
1162 1160 def filedata(self, name, decode):
1163 1161 return self._svncommand(['cat'], name)[0]
1164 1162
1165 1163
1166 1164 class gitsubrepo(abstractsubrepo):
1167 1165 def __init__(self, ctx, path, state, allowcreate):
1168 1166 super(gitsubrepo, self).__init__(ctx, path)
1169 1167 self._state = state
1170 1168 self._abspath = ctx.repo().wjoin(path)
1171 1169 self._subparent = ctx.repo()
1172 1170 self._ensuregit()
1173 1171
1174 1172 def _ensuregit(self):
1175 1173 try:
1176 1174 self._gitexecutable = 'git'
1177 1175 out, err = self._gitnodir(['--version'])
1178 1176 except OSError as e:
1179 1177 genericerror = _("error executing git for subrepo '%s': %s")
1180 1178 notfoundhint = _("check git is installed and in your PATH")
1181 1179 if e.errno != errno.ENOENT:
1182 1180 raise error.Abort(genericerror % (
1183 1181 self._path, encoding.strtolocal(e.strerror)))
1184 1182 elif pycompat.iswindows:
1185 1183 try:
1186 1184 self._gitexecutable = 'git.cmd'
1187 1185 out, err = self._gitnodir(['--version'])
1188 1186 except OSError as e2:
1189 1187 if e2.errno == errno.ENOENT:
1190 1188 raise error.Abort(_("couldn't find 'git' or 'git.cmd'"
1191 1189 " for subrepo '%s'") % self._path,
1192 1190 hint=notfoundhint)
1193 1191 else:
1194 1192 raise error.Abort(genericerror % (self._path,
1195 1193 encoding.strtolocal(e2.strerror)))
1196 1194 else:
1197 1195 raise error.Abort(_("couldn't find git for subrepo '%s'")
1198 1196 % self._path, hint=notfoundhint)
1199 1197 versionstatus = self._checkversion(out)
1200 1198 if versionstatus == 'unknown':
1201 1199 self.ui.warn(_('cannot retrieve git version\n'))
1202 1200 elif versionstatus == 'abort':
1203 1201 raise error.Abort(_('git subrepo requires at least 1.6.0 or later'))
1204 1202 elif versionstatus == 'warning':
1205 1203 self.ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
1206 1204
1207 1205 @staticmethod
1208 1206 def _gitversion(out):
1209 1207 m = re.search(br'^git version (\d+)\.(\d+)\.(\d+)', out)
1210 1208 if m:
1211 1209 return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
1212 1210
1213 1211 m = re.search(br'^git version (\d+)\.(\d+)', out)
1214 1212 if m:
1215 1213 return (int(m.group(1)), int(m.group(2)), 0)
1216 1214
1217 1215 return -1
1218 1216
1219 1217 @staticmethod
1220 1218 def _checkversion(out):
1221 1219 '''ensure git version is new enough
1222 1220
1223 1221 >>> _checkversion = gitsubrepo._checkversion
1224 1222 >>> _checkversion(b'git version 1.6.0')
1225 1223 'ok'
1226 1224 >>> _checkversion(b'git version 1.8.5')
1227 1225 'ok'
1228 1226 >>> _checkversion(b'git version 1.4.0')
1229 1227 'abort'
1230 1228 >>> _checkversion(b'git version 1.5.0')
1231 1229 'warning'
1232 1230 >>> _checkversion(b'git version 1.9-rc0')
1233 1231 'ok'
1234 1232 >>> _checkversion(b'git version 1.9.0.265.g81cdec2')
1235 1233 'ok'
1236 1234 >>> _checkversion(b'git version 1.9.0.GIT')
1237 1235 'ok'
1238 1236 >>> _checkversion(b'git version 12345')
1239 1237 'unknown'
1240 1238 >>> _checkversion(b'no')
1241 1239 'unknown'
1242 1240 '''
1243 1241 version = gitsubrepo._gitversion(out)
1244 1242 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1245 1243 # despite the docstring comment. For now, error on 1.4.0, warn on
1246 1244 # 1.5.0 but attempt to continue.
1247 1245 if version == -1:
1248 1246 return 'unknown'
1249 1247 if version < (1, 5, 0):
1250 1248 return 'abort'
1251 1249 elif version < (1, 6, 0):
1252 1250 return 'warning'
1253 1251 return 'ok'
1254 1252
1255 1253 def _gitcommand(self, commands, env=None, stream=False):
1256 1254 return self._gitdir(commands, env=env, stream=stream)[0]
1257 1255
1258 1256 def _gitdir(self, commands, env=None, stream=False):
1259 1257 return self._gitnodir(commands, env=env, stream=stream,
1260 1258 cwd=self._abspath)
1261 1259
1262 1260 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1263 1261 """Calls the git command
1264 1262
1265 1263 The methods tries to call the git command. versions prior to 1.6.0
1266 1264 are not supported and very probably fail.
1267 1265 """
1268 1266 self.ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1269 1267 if env is None:
1270 1268 env = encoding.environ.copy()
1271 1269 # disable localization for Git output (issue5176)
1272 1270 env['LC_ALL'] = 'C'
1273 1271 # fix for Git CVE-2015-7545
1274 1272 if 'GIT_ALLOW_PROTOCOL' not in env:
1275 1273 env['GIT_ALLOW_PROTOCOL'] = 'file:git:http:https:ssh'
1276 1274 # unless ui.quiet is set, print git's stderr,
1277 1275 # which is mostly progress and useful info
1278 1276 errpipe = None
1279 1277 if self.ui.quiet:
1280 1278 errpipe = open(os.devnull, 'w')
1281 1279 if self.ui._colormode and len(commands) and commands[0] == "diff":
1282 1280 # insert the argument in the front,
1283 1281 # the end of git diff arguments is used for paths
1284 1282 commands.insert(1, '--color')
1285 1283 p = subprocess.Popen(pycompat.rapply(procutil.tonativestr,
1286 1284 [self._gitexecutable] + commands),
1287 1285 bufsize=-1,
1288 1286 cwd=pycompat.rapply(procutil.tonativestr, cwd),
1289 1287 env=procutil.tonativeenv(env),
1290 1288 close_fds=procutil.closefds,
1291 1289 stdout=subprocess.PIPE, stderr=errpipe)
1292 1290 if stream:
1293 1291 return p.stdout, None
1294 1292
1295 1293 retdata = p.stdout.read().strip()
1296 1294 # wait for the child to exit to avoid race condition.
1297 1295 p.wait()
1298 1296
1299 1297 if p.returncode != 0 and p.returncode != 1:
1300 1298 # there are certain error codes that are ok
1301 1299 command = commands[0]
1302 1300 if command in ('cat-file', 'symbolic-ref'):
1303 1301 return retdata, p.returncode
1304 1302 # for all others, abort
1305 1303 raise error.Abort(_('git %s error %d in %s') %
1306 1304 (command, p.returncode, self._relpath))
1307 1305
1308 1306 return retdata, p.returncode
1309 1307
1310 1308 def _gitmissing(self):
1311 1309 return not self.wvfs.exists('.git')
1312 1310
1313 1311 def _gitstate(self):
1314 1312 return self._gitcommand(['rev-parse', 'HEAD'])
1315 1313
1316 1314 def _gitcurrentbranch(self):
1317 1315 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1318 1316 if err:
1319 1317 current = None
1320 1318 return current
1321 1319
1322 1320 def _gitremote(self, remote):
1323 1321 out = self._gitcommand(['remote', 'show', '-n', remote])
1324 1322 line = out.split('\n')[1]
1325 1323 i = line.index('URL: ') + len('URL: ')
1326 1324 return line[i:]
1327 1325
1328 1326 def _githavelocally(self, revision):
1329 1327 out, code = self._gitdir(['cat-file', '-e', revision])
1330 1328 return code == 0
1331 1329
1332 1330 def _gitisancestor(self, r1, r2):
1333 1331 base = self._gitcommand(['merge-base', r1, r2])
1334 1332 return base == r1
1335 1333
1336 1334 def _gitisbare(self):
1337 1335 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1338 1336
1339 1337 def _gitupdatestat(self):
1340 1338 """This must be run before git diff-index.
1341 1339 diff-index only looks at changes to file stat;
1342 1340 this command looks at file contents and updates the stat."""
1343 1341 self._gitcommand(['update-index', '-q', '--refresh'])
1344 1342
1345 1343 def _gitbranchmap(self):
1346 1344 '''returns 2 things:
1347 1345 a map from git branch to revision
1348 1346 a map from revision to branches'''
1349 1347 branch2rev = {}
1350 1348 rev2branch = {}
1351 1349
1352 1350 out = self._gitcommand(['for-each-ref', '--format',
1353 1351 '%(objectname) %(refname)'])
1354 1352 for line in out.split('\n'):
1355 1353 revision, ref = line.split(' ')
1356 1354 if (not ref.startswith('refs/heads/') and
1357 1355 not ref.startswith('refs/remotes/')):
1358 1356 continue
1359 1357 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1360 1358 continue # ignore remote/HEAD redirects
1361 1359 branch2rev[ref] = revision
1362 1360 rev2branch.setdefault(revision, []).append(ref)
1363 1361 return branch2rev, rev2branch
1364 1362
1365 1363 def _gittracking(self, branches):
1366 1364 'return map of remote branch to local tracking branch'
1367 1365 # assumes no more than one local tracking branch for each remote
1368 1366 tracking = {}
1369 1367 for b in branches:
1370 1368 if b.startswith('refs/remotes/'):
1371 1369 continue
1372 1370 bname = b.split('/', 2)[2]
1373 1371 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1374 1372 if remote:
1375 1373 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1376 1374 tracking['refs/remotes/%s/%s' %
1377 1375 (remote, ref.split('/', 2)[2])] = b
1378 1376 return tracking
1379 1377
1380 1378 def _abssource(self, source):
1381 1379 if '://' not in source:
1382 1380 # recognize the scp syntax as an absolute source
1383 1381 colon = source.find(':')
1384 1382 if colon != -1 and '/' not in source[:colon]:
1385 1383 return source
1386 1384 self._subsource = source
1387 1385 return _abssource(self)
1388 1386
1389 1387 def _fetch(self, source, revision):
1390 1388 if self._gitmissing():
1391 1389 # SEC: check for safe ssh url
1392 1390 util.checksafessh(source)
1393 1391
1394 1392 source = self._abssource(source)
1395 1393 self.ui.status(_('cloning subrepo %s from %s\n') %
1396 1394 (self._relpath, source))
1397 1395 self._gitnodir(['clone', source, self._abspath])
1398 1396 if self._githavelocally(revision):
1399 1397 return
1400 1398 self.ui.status(_('pulling subrepo %s from %s\n') %
1401 1399 (self._relpath, self._gitremote('origin')))
1402 1400 # try only origin: the originally cloned repo
1403 1401 self._gitcommand(['fetch'])
1404 1402 if not self._githavelocally(revision):
1405 1403 raise error.Abort(_('revision %s does not exist in subrepository '
1406 1404 '"%s"\n') % (revision, self._relpath))
1407 1405
1408 1406 @annotatesubrepoerror
1409 1407 def dirty(self, ignoreupdate=False, missing=False):
1410 1408 if self._gitmissing():
1411 1409 return self._state[1] != ''
1412 1410 if self._gitisbare():
1413 1411 return True
1414 1412 if not ignoreupdate and self._state[1] != self._gitstate():
1415 1413 # different version checked out
1416 1414 return True
1417 1415 # check for staged changes or modified files; ignore untracked files
1418 1416 self._gitupdatestat()
1419 1417 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1420 1418 return code == 1
1421 1419
1422 1420 def basestate(self):
1423 1421 return self._gitstate()
1424 1422
1425 1423 @annotatesubrepoerror
1426 1424 def get(self, state, overwrite=False):
1427 1425 source, revision, kind = state
1428 1426 if not revision:
1429 1427 self.remove()
1430 1428 return
1431 1429 self._fetch(source, revision)
1432 1430 # if the repo was set to be bare, unbare it
1433 1431 if self._gitisbare():
1434 1432 self._gitcommand(['config', 'core.bare', 'false'])
1435 1433 if self._gitstate() == revision:
1436 1434 self._gitcommand(['reset', '--hard', 'HEAD'])
1437 1435 return
1438 1436 elif self._gitstate() == revision:
1439 1437 if overwrite:
1440 1438 # first reset the index to unmark new files for commit, because
1441 1439 # reset --hard will otherwise throw away files added for commit,
1442 1440 # not just unmark them.
1443 1441 self._gitcommand(['reset', 'HEAD'])
1444 1442 self._gitcommand(['reset', '--hard', 'HEAD'])
1445 1443 return
1446 1444 branch2rev, rev2branch = self._gitbranchmap()
1447 1445
1448 1446 def checkout(args):
1449 1447 cmd = ['checkout']
1450 1448 if overwrite:
1451 1449 # first reset the index to unmark new files for commit, because
1452 1450 # the -f option will otherwise throw away files added for
1453 1451 # commit, not just unmark them.
1454 1452 self._gitcommand(['reset', 'HEAD'])
1455 1453 cmd.append('-f')
1456 1454 self._gitcommand(cmd + args)
1457 1455 _sanitize(self.ui, self.wvfs, '.git')
1458 1456
1459 1457 def rawcheckout():
1460 1458 # no branch to checkout, check it out with no branch
1461 1459 self.ui.warn(_('checking out detached HEAD in '
1462 1460 'subrepository "%s"\n') % self._relpath)
1463 1461 self.ui.warn(_('check out a git branch if you intend '
1464 1462 'to make changes\n'))
1465 1463 checkout(['-q', revision])
1466 1464
1467 1465 if revision not in rev2branch:
1468 1466 rawcheckout()
1469 1467 return
1470 1468 branches = rev2branch[revision]
1471 1469 firstlocalbranch = None
1472 1470 for b in branches:
1473 1471 if b == 'refs/heads/master':
1474 1472 # master trumps all other branches
1475 1473 checkout(['refs/heads/master'])
1476 1474 return
1477 1475 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1478 1476 firstlocalbranch = b
1479 1477 if firstlocalbranch:
1480 1478 checkout([firstlocalbranch])
1481 1479 return
1482 1480
1483 1481 tracking = self._gittracking(branch2rev.keys())
1484 1482 # choose a remote branch already tracked if possible
1485 1483 remote = branches[0]
1486 1484 if remote not in tracking:
1487 1485 for b in branches:
1488 1486 if b in tracking:
1489 1487 remote = b
1490 1488 break
1491 1489
1492 1490 if remote not in tracking:
1493 1491 # create a new local tracking branch
1494 1492 local = remote.split('/', 3)[3]
1495 1493 checkout(['-b', local, remote])
1496 1494 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1497 1495 # When updating to a tracked remote branch,
1498 1496 # if the local tracking branch is downstream of it,
1499 1497 # a normal `git pull` would have performed a "fast-forward merge"
1500 1498 # which is equivalent to updating the local branch to the remote.
1501 1499 # Since we are only looking at branching at update, we need to
1502 1500 # detect this situation and perform this action lazily.
1503 1501 if tracking[remote] != self._gitcurrentbranch():
1504 1502 checkout([tracking[remote]])
1505 1503 self._gitcommand(['merge', '--ff', remote])
1506 1504 _sanitize(self.ui, self.wvfs, '.git')
1507 1505 else:
1508 1506 # a real merge would be required, just checkout the revision
1509 1507 rawcheckout()
1510 1508
1511 1509 @annotatesubrepoerror
1512 1510 def commit(self, text, user, date):
1513 1511 if self._gitmissing():
1514 1512 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1515 1513 cmd = ['commit', '-a', '-m', text]
1516 1514 env = encoding.environ.copy()
1517 1515 if user:
1518 1516 cmd += ['--author', user]
1519 1517 if date:
1520 1518 # git's date parser silently ignores when seconds < 1e9
1521 1519 # convert to ISO8601
1522 1520 env['GIT_AUTHOR_DATE'] = dateutil.datestr(date,
1523 1521 '%Y-%m-%dT%H:%M:%S %1%2')
1524 1522 self._gitcommand(cmd, env=env)
1525 1523 # make sure commit works otherwise HEAD might not exist under certain
1526 1524 # circumstances
1527 1525 return self._gitstate()
1528 1526
1529 1527 @annotatesubrepoerror
1530 1528 def merge(self, state):
1531 1529 source, revision, kind = state
1532 1530 self._fetch(source, revision)
1533 1531 base = self._gitcommand(['merge-base', revision, self._state[1]])
1534 1532 self._gitupdatestat()
1535 1533 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1536 1534
1537 1535 def mergefunc():
1538 1536 if base == revision:
1539 1537 self.get(state) # fast forward merge
1540 1538 elif base != self._state[1]:
1541 1539 self._gitcommand(['merge', '--no-commit', revision])
1542 1540 _sanitize(self.ui, self.wvfs, '.git')
1543 1541
1544 1542 if self.dirty():
1545 1543 if self._gitstate() != revision:
1546 1544 dirty = self._gitstate() == self._state[1] or code != 0
1547 1545 if _updateprompt(self.ui, self, dirty,
1548 1546 self._state[1][:7], revision[:7]):
1549 1547 mergefunc()
1550 1548 else:
1551 1549 mergefunc()
1552 1550
1553 1551 @annotatesubrepoerror
1554 1552 def push(self, opts):
1555 1553 force = opts.get('force')
1556 1554
1557 1555 if not self._state[1]:
1558 1556 return True
1559 1557 if self._gitmissing():
1560 1558 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1561 1559 # if a branch in origin contains the revision, nothing to do
1562 1560 branch2rev, rev2branch = self._gitbranchmap()
1563 1561 if self._state[1] in rev2branch:
1564 1562 for b in rev2branch[self._state[1]]:
1565 1563 if b.startswith('refs/remotes/origin/'):
1566 1564 return True
1567 1565 for b, revision in branch2rev.iteritems():
1568 1566 if b.startswith('refs/remotes/origin/'):
1569 1567 if self._gitisancestor(self._state[1], revision):
1570 1568 return True
1571 1569 # otherwise, try to push the currently checked out branch
1572 1570 cmd = ['push']
1573 1571 if force:
1574 1572 cmd.append('--force')
1575 1573
1576 1574 current = self._gitcurrentbranch()
1577 1575 if current:
1578 1576 # determine if the current branch is even useful
1579 1577 if not self._gitisancestor(self._state[1], current):
1580 1578 self.ui.warn(_('unrelated git branch checked out '
1581 1579 'in subrepository "%s"\n') % self._relpath)
1582 1580 return False
1583 1581 self.ui.status(_('pushing branch %s of subrepository "%s"\n') %
1584 1582 (current.split('/', 2)[2], self._relpath))
1585 1583 ret = self._gitdir(cmd + ['origin', current])
1586 1584 return ret[1] == 0
1587 1585 else:
1588 1586 self.ui.warn(_('no branch checked out in subrepository "%s"\n'
1589 1587 'cannot push revision %s\n') %
1590 1588 (self._relpath, self._state[1]))
1591 1589 return False
1592 1590
1593 1591 @annotatesubrepoerror
1594 1592 def add(self, ui, match, prefix, explicitonly, **opts):
1595 1593 if self._gitmissing():
1596 1594 return []
1597 1595
1598 1596 s = self.status(None, unknown=True, clean=True)
1599 1597
1600 1598 tracked = set()
1601 1599 # dirstates 'amn' warn, 'r' is added again
1602 1600 for l in (s.modified, s.added, s.deleted, s.clean):
1603 1601 tracked.update(l)
1604 1602
1605 1603 # Unknown files not of interest will be rejected by the matcher
1606 1604 files = s.unknown
1607 1605 files.extend(match.files())
1608 1606
1609 1607 rejected = []
1610 1608
1611 1609 files = [f for f in sorted(set(files)) if match(f)]
1612 1610 for f in files:
1613 1611 exact = match.exact(f)
1614 1612 command = ["add"]
1615 1613 if exact:
1616 1614 command.append("-f") #should be added, even if ignored
1617 1615 if ui.verbose or not exact:
1618 1616 ui.status(_('adding %s\n') % match.rel(f))
1619 1617
1620 1618 if f in tracked: # hg prints 'adding' even if already tracked
1621 1619 if exact:
1622 1620 rejected.append(f)
1623 1621 continue
1624 1622 if not opts.get(r'dry_run'):
1625 1623 self._gitcommand(command + [f])
1626 1624
1627 1625 for f in rejected:
1628 1626 ui.warn(_("%s already tracked!\n") % match.abs(f))
1629 1627
1630 1628 return rejected
1631 1629
1632 1630 @annotatesubrepoerror
1633 1631 def remove(self):
1634 1632 if self._gitmissing():
1635 1633 return
1636 1634 if self.dirty():
1637 1635 self.ui.warn(_('not removing repo %s because '
1638 1636 'it has changes.\n') % self._relpath)
1639 1637 return
1640 1638 # we can't fully delete the repository as it may contain
1641 1639 # local-only history
1642 1640 self.ui.note(_('removing subrepo %s\n') % self._relpath)
1643 1641 self._gitcommand(['config', 'core.bare', 'true'])
1644 1642 for f, kind in self.wvfs.readdir():
1645 1643 if f == '.git':
1646 1644 continue
1647 1645 if kind == stat.S_IFDIR:
1648 1646 self.wvfs.rmtree(f)
1649 1647 else:
1650 1648 self.wvfs.unlink(f)
1651 1649
1652 1650 def archive(self, archiver, prefix, match=None, decode=True):
1653 1651 total = 0
1654 1652 source, revision = self._state
1655 1653 if not revision:
1656 1654 return total
1657 1655 self._fetch(source, revision)
1658 1656
1659 1657 # Parse git's native archive command.
1660 1658 # This should be much faster than manually traversing the trees
1661 1659 # and objects with many subprocess calls.
1662 1660 tarstream = self._gitcommand(['archive', revision], stream=True)
1663 1661 tar = tarfile.open(fileobj=tarstream, mode=r'r|')
1664 1662 relpath = subrelpath(self)
1665 1663 progress = self.ui.makeprogress(_('archiving (%s)') % relpath,
1666 1664 unit=_('files'))
1667 1665 progress.update(0)
1668 1666 for info in tar:
1669 1667 if info.isdir():
1670 1668 continue
1671 1669 bname = pycompat.fsencode(info.name)
1672 1670 if match and not match(bname):
1673 1671 continue
1674 1672 if info.issym():
1675 1673 data = info.linkname
1676 1674 else:
1677 1675 data = tar.extractfile(info).read()
1678 1676 archiver.addfile(prefix + self._path + '/' + bname,
1679 1677 info.mode, info.issym(), data)
1680 1678 total += 1
1681 1679 progress.increment()
1682 1680 progress.complete()
1683 1681 return total
1684 1682
1685 1683
1686 1684 @annotatesubrepoerror
1687 1685 def cat(self, match, fm, fntemplate, prefix, **opts):
1688 1686 rev = self._state[1]
1689 1687 if match.anypats():
1690 1688 return 1 #No support for include/exclude yet
1691 1689
1692 1690 if not match.files():
1693 1691 return 1
1694 1692
1695 1693 # TODO: add support for non-plain formatter (see cmdutil.cat())
1696 1694 for f in match.files():
1697 1695 output = self._gitcommand(["show", "%s:%s" % (rev, f)])
1698 1696 fp = cmdutil.makefileobj(self._ctx, fntemplate,
1699 1697 pathname=self.wvfs.reljoin(prefix, f))
1700 1698 fp.write(output)
1701 1699 fp.close()
1702 1700 return 0
1703 1701
1704 1702
1705 1703 @annotatesubrepoerror
1706 1704 def status(self, rev2, **opts):
1707 1705 rev1 = self._state[1]
1708 1706 if self._gitmissing() or not rev1:
1709 1707 # if the repo is missing, return no results
1710 1708 return scmutil.status([], [], [], [], [], [], [])
1711 1709 modified, added, removed = [], [], []
1712 1710 self._gitupdatestat()
1713 1711 if rev2:
1714 1712 command = ['diff-tree', '--no-renames', '-r', rev1, rev2]
1715 1713 else:
1716 1714 command = ['diff-index', '--no-renames', rev1]
1717 1715 out = self._gitcommand(command)
1718 1716 for line in out.split('\n'):
1719 1717 tab = line.find('\t')
1720 1718 if tab == -1:
1721 1719 continue
1722 1720 status, f = line[tab - 1:tab], line[tab + 1:]
1723 1721 if status == 'M':
1724 1722 modified.append(f)
1725 1723 elif status == 'A':
1726 1724 added.append(f)
1727 1725 elif status == 'D':
1728 1726 removed.append(f)
1729 1727
1730 1728 deleted, unknown, ignored, clean = [], [], [], []
1731 1729
1732 1730 command = ['status', '--porcelain', '-z']
1733 1731 if opts.get(r'unknown'):
1734 1732 command += ['--untracked-files=all']
1735 1733 if opts.get(r'ignored'):
1736 1734 command += ['--ignored']
1737 1735 out = self._gitcommand(command)
1738 1736
1739 1737 changedfiles = set()
1740 1738 changedfiles.update(modified)
1741 1739 changedfiles.update(added)
1742 1740 changedfiles.update(removed)
1743 1741 for line in out.split('\0'):
1744 1742 if not line:
1745 1743 continue
1746 1744 st = line[0:2]
1747 1745 #moves and copies show 2 files on one line
1748 1746 if line.find('\0') >= 0:
1749 1747 filename1, filename2 = line[3:].split('\0')
1750 1748 else:
1751 1749 filename1 = line[3:]
1752 1750 filename2 = None
1753 1751
1754 1752 changedfiles.add(filename1)
1755 1753 if filename2:
1756 1754 changedfiles.add(filename2)
1757 1755
1758 1756 if st == '??':
1759 1757 unknown.append(filename1)
1760 1758 elif st == '!!':
1761 1759 ignored.append(filename1)
1762 1760
1763 1761 if opts.get(r'clean'):
1764 1762 out = self._gitcommand(['ls-files'])
1765 1763 for f in out.split('\n'):
1766 1764 if not f in changedfiles:
1767 1765 clean.append(f)
1768 1766
1769 1767 return scmutil.status(modified, added, removed, deleted,
1770 1768 unknown, ignored, clean)
1771 1769
1772 1770 @annotatesubrepoerror
1773 1771 def diff(self, ui, diffopts, node2, match, prefix, **opts):
1774 1772 node1 = self._state[1]
1775 1773 cmd = ['diff', '--no-renames']
1776 1774 if opts[r'stat']:
1777 1775 cmd.append('--stat')
1778 1776 else:
1779 1777 # for Git, this also implies '-p'
1780 1778 cmd.append('-U%d' % diffopts.context)
1781 1779
1782 gitprefix = self.wvfs.reljoin(prefix, self._path)
1783
1784 1780 if diffopts.noprefix:
1785 cmd.extend(['--src-prefix=%s/' % gitprefix,
1786 '--dst-prefix=%s/' % gitprefix])
1781 cmd.extend(['--src-prefix=%s/' % prefix,
1782 '--dst-prefix=%s/' % prefix])
1787 1783 else:
1788 cmd.extend(['--src-prefix=a/%s/' % gitprefix,
1789 '--dst-prefix=b/%s/' % gitprefix])
1784 cmd.extend(['--src-prefix=a/%s/' % prefix,
1785 '--dst-prefix=b/%s/' % prefix])
1790 1786
1791 1787 if diffopts.ignorews:
1792 1788 cmd.append('--ignore-all-space')
1793 1789 if diffopts.ignorewsamount:
1794 1790 cmd.append('--ignore-space-change')
1795 1791 if self._gitversion(self._gitcommand(['--version'])) >= (1, 8, 4) \
1796 1792 and diffopts.ignoreblanklines:
1797 1793 cmd.append('--ignore-blank-lines')
1798 1794
1799 1795 cmd.append(node1)
1800 1796 if node2:
1801 1797 cmd.append(node2)
1802 1798
1803 1799 output = ""
1804 1800 if match.always():
1805 1801 output += self._gitcommand(cmd) + '\n'
1806 1802 else:
1807 1803 st = self.status(node2)[:3]
1808 1804 files = [f for sublist in st for f in sublist]
1809 1805 for f in files:
1810 1806 if match(f):
1811 1807 output += self._gitcommand(cmd + ['--', f]) + '\n'
1812 1808
1813 1809 if output.strip():
1814 1810 ui.write(output)
1815 1811
1816 1812 @annotatesubrepoerror
1817 1813 def revert(self, substate, *pats, **opts):
1818 1814 self.ui.status(_('reverting subrepo %s\n') % substate[0])
1819 1815 if not opts.get(r'no_backup'):
1820 1816 status = self.status(None)
1821 1817 names = status.modified
1822 1818 for name in names:
1823 1819 # backuppath() expects a path relative to the parent repo (the
1824 1820 # repo that ui.origbackuppath is relative to)
1825 1821 parentname = os.path.join(self._path, name)
1826 1822 bakname = scmutil.backuppath(self.ui, self._subparent,
1827 1823 parentname)
1828 1824 self.ui.note(_('saving current version of %s as %s\n') %
1829 1825 (name, os.path.relpath(bakname)))
1830 1826 util.rename(self.wvfs.join(name), bakname)
1831 1827
1832 1828 if not opts.get(r'dry_run'):
1833 1829 self.get(substate, overwrite=True)
1834 1830 return []
1835 1831
1836 1832 def shortid(self, revid):
1837 1833 return revid[:7]
1838 1834
1839 1835 types = {
1840 1836 'hg': hgsubrepo,
1841 1837 'svn': svnsubrepo,
1842 1838 'git': gitsubrepo,
1843 1839 }
General Comments 0
You need to be logged in to leave comments. Login now