##// END OF EJS Templates
subrepo: use vfs.dirname instead of os.path.dirname...
FUJIWARA Katsunori -
r25773:de654a83 default
parent child Browse files
Show More
@@ -1,1907 +1,1908 b''
1 1 # subrepo.py - sub-repository handling for Mercurial
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 import copy
9 9 import errno, os, re, posixpath, sys
10 10 import xml.dom.minidom
11 11 import stat, subprocess, tarfile
12 12 from i18n import _
13 13 import config, util, node, error, cmdutil, scmutil, match as matchmod
14 14 import phases
15 15 import pathutil
16 16 import exchange
17 17 hg = None
18 18 propertycache = util.propertycache
19 19
20 20 nullstate = ('', '', 'empty')
21 21
22 22 def _expandedabspath(path):
23 23 '''
24 24 get a path or url and if it is a path expand it and return an absolute path
25 25 '''
26 26 expandedpath = util.urllocalpath(util.expandpath(path))
27 27 u = util.url(expandedpath)
28 28 if not u.scheme:
29 29 path = util.normpath(os.path.abspath(u.path))
30 30 return path
31 31
32 32 def _getstorehashcachename(remotepath):
33 33 '''get a unique filename for the store hash cache of a remote repository'''
34 34 return util.sha1(_expandedabspath(remotepath)).hexdigest()[0:12]
35 35
36 36 class SubrepoAbort(error.Abort):
37 37 """Exception class used to avoid handling a subrepo error more than once"""
38 38 def __init__(self, *args, **kw):
39 39 error.Abort.__init__(self, *args, **kw)
40 40 self.subrepo = kw.get('subrepo')
41 41 self.cause = kw.get('cause')
42 42
43 43 def annotatesubrepoerror(func):
44 44 def decoratedmethod(self, *args, **kargs):
45 45 try:
46 46 res = func(self, *args, **kargs)
47 47 except SubrepoAbort as ex:
48 48 # This exception has already been handled
49 49 raise ex
50 50 except error.Abort as ex:
51 51 subrepo = subrelpath(self)
52 52 errormsg = str(ex) + ' ' + _('(in subrepo %s)') % subrepo
53 53 # avoid handling this exception by raising a SubrepoAbort exception
54 54 raise SubrepoAbort(errormsg, hint=ex.hint, subrepo=subrepo,
55 55 cause=sys.exc_info())
56 56 return res
57 57 return decoratedmethod
58 58
59 59 def state(ctx, ui):
60 60 """return a state dict, mapping subrepo paths configured in .hgsub
61 61 to tuple: (source from .hgsub, revision from .hgsubstate, kind
62 62 (key in types dict))
63 63 """
64 64 p = config.config()
65 65 repo = ctx.repo()
66 66 def read(f, sections=None, remap=None):
67 67 if f in ctx:
68 68 try:
69 69 data = ctx[f].data()
70 70 except IOError as err:
71 71 if err.errno != errno.ENOENT:
72 72 raise
73 73 # handle missing subrepo spec files as removed
74 74 ui.warn(_("warning: subrepo spec file \'%s\' not found\n") %
75 75 repo.pathto(f))
76 76 return
77 77 p.parse(f, data, sections, remap, read)
78 78 else:
79 79 raise util.Abort(_("subrepo spec file \'%s\' not found") %
80 80 repo.pathto(f))
81 81 if '.hgsub' in ctx:
82 82 read('.hgsub')
83 83
84 84 for path, src in ui.configitems('subpaths'):
85 85 p.set('subpaths', path, src, ui.configsource('subpaths', path))
86 86
87 87 rev = {}
88 88 if '.hgsubstate' in ctx:
89 89 try:
90 90 for i, l in enumerate(ctx['.hgsubstate'].data().splitlines()):
91 91 l = l.lstrip()
92 92 if not l:
93 93 continue
94 94 try:
95 95 revision, path = l.split(" ", 1)
96 96 except ValueError:
97 97 raise util.Abort(_("invalid subrepository revision "
98 98 "specifier in \'%s\' line %d")
99 99 % (repo.pathto('.hgsubstate'), (i + 1)))
100 100 rev[path] = revision
101 101 except IOError as err:
102 102 if err.errno != errno.ENOENT:
103 103 raise
104 104
105 105 def remap(src):
106 106 for pattern, repl in p.items('subpaths'):
107 107 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
108 108 # does a string decode.
109 109 repl = repl.encode('string-escape')
110 110 # However, we still want to allow back references to go
111 111 # through unharmed, so we turn r'\\1' into r'\1'. Again,
112 112 # extra escapes are needed because re.sub string decodes.
113 113 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
114 114 try:
115 115 src = re.sub(pattern, repl, src, 1)
116 116 except re.error as e:
117 117 raise util.Abort(_("bad subrepository pattern in %s: %s")
118 118 % (p.source('subpaths', pattern), e))
119 119 return src
120 120
121 121 state = {}
122 122 for path, src in p[''].items():
123 123 kind = 'hg'
124 124 if src.startswith('['):
125 125 if ']' not in src:
126 126 raise util.Abort(_('missing ] in subrepo source'))
127 127 kind, src = src.split(']', 1)
128 128 kind = kind[1:]
129 129 src = src.lstrip() # strip any extra whitespace after ']'
130 130
131 131 if not util.url(src).isabs():
132 132 parent = _abssource(repo, abort=False)
133 133 if parent:
134 134 parent = util.url(parent)
135 135 parent.path = posixpath.join(parent.path or '', src)
136 136 parent.path = posixpath.normpath(parent.path)
137 137 joined = str(parent)
138 138 # Remap the full joined path and use it if it changes,
139 139 # else remap the original source.
140 140 remapped = remap(joined)
141 141 if remapped == joined:
142 142 src = remap(src)
143 143 else:
144 144 src = remapped
145 145
146 146 src = remap(src)
147 147 state[util.pconvert(path)] = (src.strip(), rev.get(path, ''), kind)
148 148
149 149 return state
150 150
151 151 def writestate(repo, state):
152 152 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
153 153 lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state)
154 154 if state[s][1] != nullstate[1]]
155 155 repo.wwrite('.hgsubstate', ''.join(lines), '')
156 156
157 157 def submerge(repo, wctx, mctx, actx, overwrite):
158 158 """delegated from merge.applyupdates: merging of .hgsubstate file
159 159 in working context, merging context and ancestor context"""
160 160 if mctx == actx: # backwards?
161 161 actx = wctx.p1()
162 162 s1 = wctx.substate
163 163 s2 = mctx.substate
164 164 sa = actx.substate
165 165 sm = {}
166 166
167 167 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
168 168
169 169 def debug(s, msg, r=""):
170 170 if r:
171 171 r = "%s:%s:%s" % r
172 172 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
173 173
174 174 for s, l in sorted(s1.iteritems()):
175 175 a = sa.get(s, nullstate)
176 176 ld = l # local state with possible dirty flag for compares
177 177 if wctx.sub(s).dirty():
178 178 ld = (l[0], l[1] + "+")
179 179 if wctx == actx: # overwrite
180 180 a = ld
181 181
182 182 if s in s2:
183 183 r = s2[s]
184 184 if ld == r or r == a: # no change or local is newer
185 185 sm[s] = l
186 186 continue
187 187 elif ld == a: # other side changed
188 188 debug(s, "other changed, get", r)
189 189 wctx.sub(s).get(r, overwrite)
190 190 sm[s] = r
191 191 elif ld[0] != r[0]: # sources differ
192 192 if repo.ui.promptchoice(
193 193 _(' subrepository sources for %s differ\n'
194 194 'use (l)ocal source (%s) or (r)emote source (%s)?'
195 195 '$$ &Local $$ &Remote') % (s, l[0], r[0]), 0):
196 196 debug(s, "prompt changed, get", r)
197 197 wctx.sub(s).get(r, overwrite)
198 198 sm[s] = r
199 199 elif ld[1] == a[1]: # local side is unchanged
200 200 debug(s, "other side changed, get", r)
201 201 wctx.sub(s).get(r, overwrite)
202 202 sm[s] = r
203 203 else:
204 204 debug(s, "both sides changed")
205 205 srepo = wctx.sub(s)
206 206 option = repo.ui.promptchoice(
207 207 _(' subrepository %s diverged (local revision: %s, '
208 208 'remote revision: %s)\n'
209 209 '(M)erge, keep (l)ocal or keep (r)emote?'
210 210 '$$ &Merge $$ &Local $$ &Remote')
211 211 % (s, srepo.shortid(l[1]), srepo.shortid(r[1])), 0)
212 212 if option == 0:
213 213 wctx.sub(s).merge(r)
214 214 sm[s] = l
215 215 debug(s, "merge with", r)
216 216 elif option == 1:
217 217 sm[s] = l
218 218 debug(s, "keep local subrepo revision", l)
219 219 else:
220 220 wctx.sub(s).get(r, overwrite)
221 221 sm[s] = r
222 222 debug(s, "get remote subrepo revision", r)
223 223 elif ld == a: # remote removed, local unchanged
224 224 debug(s, "remote removed, remove")
225 225 wctx.sub(s).remove()
226 226 elif a == nullstate: # not present in remote or ancestor
227 227 debug(s, "local added, keep")
228 228 sm[s] = l
229 229 continue
230 230 else:
231 231 if repo.ui.promptchoice(
232 232 _(' local changed subrepository %s which remote removed\n'
233 233 'use (c)hanged version or (d)elete?'
234 234 '$$ &Changed $$ &Delete') % s, 0):
235 235 debug(s, "prompt remove")
236 236 wctx.sub(s).remove()
237 237
238 238 for s, r in sorted(s2.items()):
239 239 if s in s1:
240 240 continue
241 241 elif s not in sa:
242 242 debug(s, "remote added, get", r)
243 243 mctx.sub(s).get(r)
244 244 sm[s] = r
245 245 elif r != sa[s]:
246 246 if repo.ui.promptchoice(
247 247 _(' remote changed subrepository %s which local removed\n'
248 248 'use (c)hanged version or (d)elete?'
249 249 '$$ &Changed $$ &Delete') % s, 0) == 0:
250 250 debug(s, "prompt recreate", r)
251 251 mctx.sub(s).get(r)
252 252 sm[s] = r
253 253
254 254 # record merged .hgsubstate
255 255 writestate(repo, sm)
256 256 return sm
257 257
258 258 def _updateprompt(ui, sub, dirty, local, remote):
259 259 if dirty:
260 260 msg = (_(' subrepository sources for %s differ\n'
261 261 'use (l)ocal source (%s) or (r)emote source (%s)?'
262 262 '$$ &Local $$ &Remote')
263 263 % (subrelpath(sub), local, remote))
264 264 else:
265 265 msg = (_(' subrepository sources for %s differ (in checked out '
266 266 'version)\n'
267 267 'use (l)ocal source (%s) or (r)emote source (%s)?'
268 268 '$$ &Local $$ &Remote')
269 269 % (subrelpath(sub), local, remote))
270 270 return ui.promptchoice(msg, 0)
271 271
272 272 def reporelpath(repo):
273 273 """return path to this (sub)repo as seen from outermost repo"""
274 274 parent = repo
275 275 while util.safehasattr(parent, '_subparent'):
276 276 parent = parent._subparent
277 277 return repo.root[len(pathutil.normasprefix(parent.root)):]
278 278
279 279 def subrelpath(sub):
280 280 """return path to this subrepo as seen from outermost repo"""
281 281 return sub._relpath
282 282
283 283 def _abssource(repo, push=False, abort=True):
284 284 """return pull/push path of repo - either based on parent repo .hgsub info
285 285 or on the top repo config. Abort or return None if no source found."""
286 286 if util.safehasattr(repo, '_subparent'):
287 287 source = util.url(repo._subsource)
288 288 if source.isabs():
289 289 return str(source)
290 290 source.path = posixpath.normpath(source.path)
291 291 parent = _abssource(repo._subparent, push, abort=False)
292 292 if parent:
293 293 parent = util.url(util.pconvert(parent))
294 294 parent.path = posixpath.join(parent.path or '', source.path)
295 295 parent.path = posixpath.normpath(parent.path)
296 296 return str(parent)
297 297 else: # recursion reached top repo
298 298 if util.safehasattr(repo, '_subtoppath'):
299 299 return repo._subtoppath
300 300 if push and repo.ui.config('paths', 'default-push'):
301 301 return repo.ui.config('paths', 'default-push')
302 302 if repo.ui.config('paths', 'default'):
303 303 return repo.ui.config('paths', 'default')
304 304 if repo.shared():
305 305 # chop off the .hg component to get the default path form
306 306 return os.path.dirname(repo.sharedpath)
307 307 if abort:
308 308 raise util.Abort(_("default path for subrepository not found"))
309 309
310 310 def _sanitize(ui, vfs, ignore):
311 311 for dirname, dirs, names in vfs.walk():
312 312 for i, d in enumerate(dirs):
313 313 if d.lower() == ignore:
314 314 del dirs[i]
315 315 break
316 316 if vfs.basename(dirname).lower() != '.hg':
317 317 continue
318 318 for f in names:
319 319 if f.lower() == 'hgrc':
320 320 ui.warn(_("warning: removing potentially hostile 'hgrc' "
321 321 "in '%s'\n") % vfs.join(dirname))
322 322 vfs.unlink(vfs.reljoin(dirname, f))
323 323
324 324 def subrepo(ctx, path, allowwdir=False):
325 325 """return instance of the right subrepo class for subrepo in path"""
326 326 # subrepo inherently violates our import layering rules
327 327 # because it wants to make repo objects from deep inside the stack
328 328 # so we manually delay the circular imports to not break
329 329 # scripts that don't use our demand-loading
330 330 global hg
331 331 import hg as h
332 332 hg = h
333 333
334 334 pathutil.pathauditor(ctx.repo().root)(path)
335 335 state = ctx.substate[path]
336 336 if state[2] not in types:
337 337 raise util.Abort(_('unknown subrepo type %s') % state[2])
338 338 if allowwdir:
339 339 state = (state[0], ctx.subrev(path), state[2])
340 340 return types[state[2]](ctx, path, state[:2])
341 341
342 342 def nullsubrepo(ctx, path, pctx):
343 343 """return an empty subrepo in pctx for the extant subrepo in ctx"""
344 344 # subrepo inherently violates our import layering rules
345 345 # because it wants to make repo objects from deep inside the stack
346 346 # so we manually delay the circular imports to not break
347 347 # scripts that don't use our demand-loading
348 348 global hg
349 349 import hg as h
350 350 hg = h
351 351
352 352 pathutil.pathauditor(ctx.repo().root)(path)
353 353 state = ctx.substate[path]
354 354 if state[2] not in types:
355 355 raise util.Abort(_('unknown subrepo type %s') % state[2])
356 356 subrev = ''
357 357 if state[2] == 'hg':
358 358 subrev = "0" * 40
359 359 return types[state[2]](pctx, path, (state[0], subrev))
360 360
361 361 def newcommitphase(ui, ctx):
362 362 commitphase = phases.newcommitphase(ui)
363 363 substate = getattr(ctx, "substate", None)
364 364 if not substate:
365 365 return commitphase
366 366 check = ui.config('phases', 'checksubrepos', 'follow')
367 367 if check not in ('ignore', 'follow', 'abort'):
368 368 raise util.Abort(_('invalid phases.checksubrepos configuration: %s')
369 369 % (check))
370 370 if check == 'ignore':
371 371 return commitphase
372 372 maxphase = phases.public
373 373 maxsub = None
374 374 for s in sorted(substate):
375 375 sub = ctx.sub(s)
376 376 subphase = sub.phase(substate[s][1])
377 377 if maxphase < subphase:
378 378 maxphase = subphase
379 379 maxsub = s
380 380 if commitphase < maxphase:
381 381 if check == 'abort':
382 382 raise util.Abort(_("can't commit in %s phase"
383 383 " conflicting %s from subrepository %s") %
384 384 (phases.phasenames[commitphase],
385 385 phases.phasenames[maxphase], maxsub))
386 386 ui.warn(_("warning: changes are committed in"
387 387 " %s phase from subrepository %s\n") %
388 388 (phases.phasenames[maxphase], maxsub))
389 389 return maxphase
390 390 return commitphase
391 391
392 392 # subrepo classes need to implement the following abstract class:
393 393
394 394 class abstractsubrepo(object):
395 395
396 396 def __init__(self, ctx, path):
397 397 """Initialize abstractsubrepo part
398 398
399 399 ``ctx`` is the context referring this subrepository in the
400 400 parent repository.
401 401
402 402 ``path`` is the path to this subrepositiry as seen from
403 403 innermost repository.
404 404 """
405 405 self.ui = ctx.repo().ui
406 406 self._ctx = ctx
407 407 self._path = path
408 408
409 409 def storeclean(self, path):
410 410 """
411 411 returns true if the repository has not changed since it was last
412 412 cloned from or pushed to a given repository.
413 413 """
414 414 return False
415 415
416 416 def dirty(self, ignoreupdate=False):
417 417 """returns true if the dirstate of the subrepo is dirty or does not
418 418 match current stored state. If ignoreupdate is true, only check
419 419 whether the subrepo has uncommitted changes in its dirstate.
420 420 """
421 421 raise NotImplementedError
422 422
423 423 def dirtyreason(self, ignoreupdate=False):
424 424 """return reason string if it is ``dirty()``
425 425
426 426 Returned string should have enough information for the message
427 427 of exception.
428 428
429 429 This returns None, otherwise.
430 430 """
431 431 if self.dirty(ignoreupdate=ignoreupdate):
432 432 return _("uncommitted changes in subrepository '%s'"
433 433 ) % subrelpath(self)
434 434
435 435 def bailifchanged(self, ignoreupdate=False):
436 436 """raise Abort if subrepository is ``dirty()``
437 437 """
438 438 dirtyreason = self.dirtyreason(ignoreupdate=ignoreupdate)
439 439 if dirtyreason:
440 440 raise util.Abort(dirtyreason)
441 441
442 442 def basestate(self):
443 443 """current working directory base state, disregarding .hgsubstate
444 444 state and working directory modifications"""
445 445 raise NotImplementedError
446 446
447 447 def checknested(self, path):
448 448 """check if path is a subrepository within this repository"""
449 449 return False
450 450
451 451 def commit(self, text, user, date):
452 452 """commit the current changes to the subrepo with the given
453 453 log message. Use given user and date if possible. Return the
454 454 new state of the subrepo.
455 455 """
456 456 raise NotImplementedError
457 457
458 458 def phase(self, state):
459 459 """returns phase of specified state in the subrepository.
460 460 """
461 461 return phases.public
462 462
463 463 def remove(self):
464 464 """remove the subrepo
465 465
466 466 (should verify the dirstate is not dirty first)
467 467 """
468 468 raise NotImplementedError
469 469
470 470 def get(self, state, overwrite=False):
471 471 """run whatever commands are needed to put the subrepo into
472 472 this state
473 473 """
474 474 raise NotImplementedError
475 475
476 476 def merge(self, state):
477 477 """merge currently-saved state with the new state."""
478 478 raise NotImplementedError
479 479
480 480 def push(self, opts):
481 481 """perform whatever action is analogous to 'hg push'
482 482
483 483 This may be a no-op on some systems.
484 484 """
485 485 raise NotImplementedError
486 486
487 487 def add(self, ui, match, prefix, explicitonly, **opts):
488 488 return []
489 489
490 490 def addremove(self, matcher, prefix, opts, dry_run, similarity):
491 491 self.ui.warn("%s: %s" % (prefix, _("addremove is not supported")))
492 492 return 1
493 493
494 494 def cat(self, match, prefix, **opts):
495 495 return 1
496 496
497 497 def status(self, rev2, **opts):
498 498 return scmutil.status([], [], [], [], [], [], [])
499 499
500 500 def diff(self, ui, diffopts, node2, match, prefix, **opts):
501 501 pass
502 502
503 503 def outgoing(self, ui, dest, opts):
504 504 return 1
505 505
506 506 def incoming(self, ui, source, opts):
507 507 return 1
508 508
509 509 def files(self):
510 510 """return filename iterator"""
511 511 raise NotImplementedError
512 512
513 513 def filedata(self, name):
514 514 """return file data"""
515 515 raise NotImplementedError
516 516
517 517 def fileflags(self, name):
518 518 """return file flags"""
519 519 return ''
520 520
521 521 def getfileset(self, expr):
522 522 """Resolve the fileset expression for this repo"""
523 523 return set()
524 524
525 525 def printfiles(self, ui, m, fm, fmt, subrepos):
526 526 """handle the files command for this subrepo"""
527 527 return 1
528 528
529 529 def archive(self, archiver, prefix, match=None):
530 530 if match is not None:
531 531 files = [f for f in self.files() if match(f)]
532 532 else:
533 533 files = self.files()
534 534 total = len(files)
535 535 relpath = subrelpath(self)
536 536 self.ui.progress(_('archiving (%s)') % relpath, 0,
537 537 unit=_('files'), total=total)
538 538 for i, name in enumerate(files):
539 539 flags = self.fileflags(name)
540 540 mode = 'x' in flags and 0o755 or 0o644
541 541 symlink = 'l' in flags
542 542 archiver.addfile(prefix + self._path + '/' + name,
543 543 mode, symlink, self.filedata(name))
544 544 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
545 545 unit=_('files'), total=total)
546 546 self.ui.progress(_('archiving (%s)') % relpath, None)
547 547 return total
548 548
549 549 def walk(self, match):
550 550 '''
551 551 walk recursively through the directory tree, finding all files
552 552 matched by the match function
553 553 '''
554 554 pass
555 555
556 556 def forget(self, match, prefix):
557 557 return ([], [])
558 558
559 559 def removefiles(self, matcher, prefix, after, force, subrepos):
560 560 """remove the matched files from the subrepository and the filesystem,
561 561 possibly by force and/or after the file has been removed from the
562 562 filesystem. Return 0 on success, 1 on any warning.
563 563 """
564 564 return 1
565 565
566 566 def revert(self, substate, *pats, **opts):
567 567 self.ui.warn('%s: reverting %s subrepos is unsupported\n' \
568 568 % (substate[0], substate[2]))
569 569 return []
570 570
571 571 def shortid(self, revid):
572 572 return revid
573 573
574 574 def verify(self):
575 575 '''verify the integrity of the repository. Return 0 on success or
576 576 warning, 1 on any error.
577 577 '''
578 578 return 0
579 579
580 580 @propertycache
581 581 def wvfs(self):
582 582 """return vfs to access the working directory of this subrepository
583 583 """
584 584 return scmutil.vfs(self._ctx.repo().wvfs.join(self._path))
585 585
586 586 @propertycache
587 587 def _relpath(self):
588 588 """return path to this subrepository as seen from outermost repository
589 589 """
590 590 return self.wvfs.reljoin(reporelpath(self._ctx.repo()), self._path)
591 591
592 592 class hgsubrepo(abstractsubrepo):
593 593 def __init__(self, ctx, path, state):
594 594 super(hgsubrepo, self).__init__(ctx, path)
595 595 self._state = state
596 596 r = ctx.repo()
597 597 root = r.wjoin(path)
598 598 create = not r.wvfs.exists('%s/.hg' % path)
599 599 self._repo = hg.repository(r.baseui, root, create=create)
600 600
601 601 # Propagate the parent's --hidden option
602 602 if r is r.unfiltered():
603 603 self._repo = self._repo.unfiltered()
604 604
605 605 self.ui = self._repo.ui
606 606 for s, k in [('ui', 'commitsubrepos')]:
607 607 v = r.ui.config(s, k)
608 608 if v:
609 609 self.ui.setconfig(s, k, v, 'subrepo')
610 610 self.ui.setconfig('ui', '_usedassubrepo', 'True', 'subrepo')
611 611 self._initrepo(r, state[0], create)
612 612
613 613 def storeclean(self, path):
614 614 lock = self._repo.lock()
615 615 try:
616 616 return self._storeclean(path)
617 617 finally:
618 618 lock.release()
619 619
620 620 def _storeclean(self, path):
621 621 clean = True
622 622 itercache = self._calcstorehash(path)
623 623 for filehash in self._readstorehashcache(path):
624 624 if filehash != next(itercache, None):
625 625 clean = False
626 626 break
627 627 if clean:
628 628 # if not empty:
629 629 # the cached and current pull states have a different size
630 630 clean = next(itercache, None) is None
631 631 return clean
632 632
633 633 def _calcstorehash(self, remotepath):
634 634 '''calculate a unique "store hash"
635 635
636 636 This method is used to to detect when there are changes that may
637 637 require a push to a given remote path.'''
638 638 # sort the files that will be hashed in increasing (likely) file size
639 639 filelist = ('bookmarks', 'store/phaseroots', 'store/00changelog.i')
640 640 yield '# %s\n' % _expandedabspath(remotepath)
641 641 vfs = self._repo.vfs
642 642 for relname in filelist:
643 643 filehash = util.sha1(vfs.tryread(relname)).hexdigest()
644 644 yield '%s = %s\n' % (relname, filehash)
645 645
646 646 @propertycache
647 647 def _cachestorehashvfs(self):
648 648 return scmutil.vfs(self._repo.join('cache/storehash'))
649 649
650 650 def _readstorehashcache(self, remotepath):
651 651 '''read the store hash cache for a given remote repository'''
652 652 cachefile = _getstorehashcachename(remotepath)
653 653 return self._cachestorehashvfs.tryreadlines(cachefile, 'r')
654 654
655 655 def _cachestorehash(self, remotepath):
656 656 '''cache the current store hash
657 657
658 658 Each remote repo requires its own store hash cache, because a subrepo
659 659 store may be "clean" versus a given remote repo, but not versus another
660 660 '''
661 661 cachefile = _getstorehashcachename(remotepath)
662 662 lock = self._repo.lock()
663 663 try:
664 664 storehash = list(self._calcstorehash(remotepath))
665 665 vfs = self._cachestorehashvfs
666 666 vfs.writelines(cachefile, storehash, mode='w', notindexed=True)
667 667 finally:
668 668 lock.release()
669 669
670 670 def _getctx(self):
671 671 '''fetch the context for this subrepo revision, possibly a workingctx
672 672 '''
673 673 if self._ctx.rev() is None:
674 674 return self._repo[None] # workingctx if parent is workingctx
675 675 else:
676 676 rev = self._state[1]
677 677 return self._repo[rev]
678 678
679 679 @annotatesubrepoerror
680 680 def _initrepo(self, parentrepo, source, create):
681 681 self._repo._subparent = parentrepo
682 682 self._repo._subsource = source
683 683
684 684 if create:
685 685 lines = ['[paths]\n']
686 686
687 687 def addpathconfig(key, value):
688 688 if value:
689 689 lines.append('%s = %s\n' % (key, value))
690 690 self.ui.setconfig('paths', key, value, 'subrepo')
691 691
692 692 defpath = _abssource(self._repo, abort=False)
693 693 defpushpath = _abssource(self._repo, True, abort=False)
694 694 addpathconfig('default', defpath)
695 695 if defpath != defpushpath:
696 696 addpathconfig('default-push', defpushpath)
697 697
698 698 fp = self._repo.vfs("hgrc", "w", text=True)
699 699 try:
700 700 fp.write(''.join(lines))
701 701 finally:
702 702 fp.close()
703 703
704 704 @annotatesubrepoerror
705 705 def add(self, ui, match, prefix, explicitonly, **opts):
706 706 return cmdutil.add(ui, self._repo, match,
707 707 self.wvfs.reljoin(prefix, self._path),
708 708 explicitonly, **opts)
709 709
710 710 @annotatesubrepoerror
711 711 def addremove(self, m, prefix, opts, dry_run, similarity):
712 712 # In the same way as sub directories are processed, once in a subrepo,
713 713 # always entry any of its subrepos. Don't corrupt the options that will
714 714 # be used to process sibling subrepos however.
715 715 opts = copy.copy(opts)
716 716 opts['subrepos'] = True
717 717 return scmutil.addremove(self._repo, m,
718 718 self.wvfs.reljoin(prefix, self._path), opts,
719 719 dry_run, similarity)
720 720
721 721 @annotatesubrepoerror
722 722 def cat(self, match, prefix, **opts):
723 723 rev = self._state[1]
724 724 ctx = self._repo[rev]
725 725 return cmdutil.cat(self.ui, self._repo, ctx, match, prefix, **opts)
726 726
727 727 @annotatesubrepoerror
728 728 def status(self, rev2, **opts):
729 729 try:
730 730 rev1 = self._state[1]
731 731 ctx1 = self._repo[rev1]
732 732 ctx2 = self._repo[rev2]
733 733 return self._repo.status(ctx1, ctx2, **opts)
734 734 except error.RepoLookupError as inst:
735 735 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
736 736 % (inst, subrelpath(self)))
737 737 return scmutil.status([], [], [], [], [], [], [])
738 738
739 739 @annotatesubrepoerror
740 740 def diff(self, ui, diffopts, node2, match, prefix, **opts):
741 741 try:
742 742 node1 = node.bin(self._state[1])
743 743 # We currently expect node2 to come from substate and be
744 744 # in hex format
745 745 if node2 is not None:
746 746 node2 = node.bin(node2)
747 747 cmdutil.diffordiffstat(ui, self._repo, diffopts,
748 748 node1, node2, match,
749 749 prefix=posixpath.join(prefix, self._path),
750 750 listsubrepos=True, **opts)
751 751 except error.RepoLookupError as inst:
752 752 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
753 753 % (inst, subrelpath(self)))
754 754
755 755 @annotatesubrepoerror
756 756 def archive(self, archiver, prefix, match=None):
757 757 self._get(self._state + ('hg',))
758 758 total = abstractsubrepo.archive(self, archiver, prefix, match)
759 759 rev = self._state[1]
760 760 ctx = self._repo[rev]
761 761 for subpath in ctx.substate:
762 762 s = subrepo(ctx, subpath, True)
763 763 submatch = matchmod.narrowmatcher(subpath, match)
764 764 total += s.archive(archiver, prefix + self._path + '/', submatch)
765 765 return total
766 766
767 767 @annotatesubrepoerror
768 768 def dirty(self, ignoreupdate=False):
769 769 r = self._state[1]
770 770 if r == '' and not ignoreupdate: # no state recorded
771 771 return True
772 772 w = self._repo[None]
773 773 if r != w.p1().hex() and not ignoreupdate:
774 774 # different version checked out
775 775 return True
776 776 return w.dirty() # working directory changed
777 777
778 778 def basestate(self):
779 779 return self._repo['.'].hex()
780 780
781 781 def checknested(self, path):
782 782 return self._repo._checknested(self._repo.wjoin(path))
783 783
784 784 @annotatesubrepoerror
785 785 def commit(self, text, user, date):
786 786 # don't bother committing in the subrepo if it's only been
787 787 # updated
788 788 if not self.dirty(True):
789 789 return self._repo['.'].hex()
790 790 self.ui.debug("committing subrepo %s\n" % subrelpath(self))
791 791 n = self._repo.commit(text, user, date)
792 792 if not n:
793 793 return self._repo['.'].hex() # different version checked out
794 794 return node.hex(n)
795 795
796 796 @annotatesubrepoerror
797 797 def phase(self, state):
798 798 return self._repo[state].phase()
799 799
800 800 @annotatesubrepoerror
801 801 def remove(self):
802 802 # we can't fully delete the repository as it may contain
803 803 # local-only history
804 804 self.ui.note(_('removing subrepo %s\n') % subrelpath(self))
805 805 hg.clean(self._repo, node.nullid, False)
806 806
807 807 def _get(self, state):
808 808 source, revision, kind = state
809 809 if revision in self._repo.unfiltered():
810 810 return True
811 811 self._repo._subsource = source
812 812 srcurl = _abssource(self._repo)
813 813 other = hg.peer(self._repo, {}, srcurl)
814 814 if len(self._repo) == 0:
815 815 self.ui.status(_('cloning subrepo %s from %s\n')
816 816 % (subrelpath(self), srcurl))
817 817 parentrepo = self._repo._subparent
818 818 # use self._repo.vfs instead of self.wvfs to remove .hg only
819 819 self._repo.vfs.rmtree()
820 820 other, cloned = hg.clone(self._repo._subparent.baseui, {},
821 821 other, self._repo.root,
822 822 update=False)
823 823 self._repo = cloned.local()
824 824 self._initrepo(parentrepo, source, create=True)
825 825 self._cachestorehash(srcurl)
826 826 else:
827 827 self.ui.status(_('pulling subrepo %s from %s\n')
828 828 % (subrelpath(self), srcurl))
829 829 cleansub = self.storeclean(srcurl)
830 830 exchange.pull(self._repo, other)
831 831 if cleansub:
832 832 # keep the repo clean after pull
833 833 self._cachestorehash(srcurl)
834 834 return False
835 835
836 836 @annotatesubrepoerror
837 837 def get(self, state, overwrite=False):
838 838 inrepo = self._get(state)
839 839 source, revision, kind = state
840 840 repo = self._repo
841 841 repo.ui.debug("getting subrepo %s\n" % self._path)
842 842 if inrepo:
843 843 urepo = repo.unfiltered()
844 844 ctx = urepo[revision]
845 845 if ctx.hidden():
846 846 urepo.ui.warn(
847 847 _('revision %s in subrepo %s is hidden\n') \
848 848 % (revision[0:12], self._path))
849 849 repo = urepo
850 850 hg.updaterepo(repo, revision, overwrite)
851 851
852 852 @annotatesubrepoerror
853 853 def merge(self, state):
854 854 self._get(state)
855 855 cur = self._repo['.']
856 856 dst = self._repo[state[1]]
857 857 anc = dst.ancestor(cur)
858 858
859 859 def mergefunc():
860 860 if anc == cur and dst.branch() == cur.branch():
861 861 self.ui.debug("updating subrepo %s\n" % subrelpath(self))
862 862 hg.update(self._repo, state[1])
863 863 elif anc == dst:
864 864 self.ui.debug("skipping subrepo %s\n" % subrelpath(self))
865 865 else:
866 866 self.ui.debug("merging subrepo %s\n" % subrelpath(self))
867 867 hg.merge(self._repo, state[1], remind=False)
868 868
869 869 wctx = self._repo[None]
870 870 if self.dirty():
871 871 if anc != dst:
872 872 if _updateprompt(self.ui, self, wctx.dirty(), cur, dst):
873 873 mergefunc()
874 874 else:
875 875 mergefunc()
876 876 else:
877 877 mergefunc()
878 878
879 879 @annotatesubrepoerror
880 880 def push(self, opts):
881 881 force = opts.get('force')
882 882 newbranch = opts.get('new_branch')
883 883 ssh = opts.get('ssh')
884 884
885 885 # push subrepos depth-first for coherent ordering
886 886 c = self._repo['']
887 887 subs = c.substate # only repos that are committed
888 888 for s in sorted(subs):
889 889 if c.sub(s).push(opts) == 0:
890 890 return False
891 891
892 892 dsturl = _abssource(self._repo, True)
893 893 if not force:
894 894 if self.storeclean(dsturl):
895 895 self.ui.status(
896 896 _('no changes made to subrepo %s since last push to %s\n')
897 897 % (subrelpath(self), dsturl))
898 898 return None
899 899 self.ui.status(_('pushing subrepo %s to %s\n') %
900 900 (subrelpath(self), dsturl))
901 901 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
902 902 res = exchange.push(self._repo, other, force, newbranch=newbranch)
903 903
904 904 # the repo is now clean
905 905 self._cachestorehash(dsturl)
906 906 return res.cgresult
907 907
908 908 @annotatesubrepoerror
909 909 def outgoing(self, ui, dest, opts):
910 910 if 'rev' in opts or 'branch' in opts:
911 911 opts = copy.copy(opts)
912 912 opts.pop('rev', None)
913 913 opts.pop('branch', None)
914 914 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
915 915
916 916 @annotatesubrepoerror
917 917 def incoming(self, ui, source, opts):
918 918 if 'rev' in opts or 'branch' in opts:
919 919 opts = copy.copy(opts)
920 920 opts.pop('rev', None)
921 921 opts.pop('branch', None)
922 922 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
923 923
924 924 @annotatesubrepoerror
925 925 def files(self):
926 926 rev = self._state[1]
927 927 ctx = self._repo[rev]
928 928 return ctx.manifest().keys()
929 929
930 930 def filedata(self, name):
931 931 rev = self._state[1]
932 932 return self._repo[rev][name].data()
933 933
934 934 def fileflags(self, name):
935 935 rev = self._state[1]
936 936 ctx = self._repo[rev]
937 937 return ctx.flags(name)
938 938
939 939 @annotatesubrepoerror
940 940 def printfiles(self, ui, m, fm, fmt, subrepos):
941 941 # If the parent context is a workingctx, use the workingctx here for
942 942 # consistency.
943 943 if self._ctx.rev() is None:
944 944 ctx = self._repo[None]
945 945 else:
946 946 rev = self._state[1]
947 947 ctx = self._repo[rev]
948 948 return cmdutil.files(ui, ctx, m, fm, fmt, subrepos)
949 949
950 950 @annotatesubrepoerror
951 951 def getfileset(self, expr):
952 952 if self._ctx.rev() is None:
953 953 ctx = self._repo[None]
954 954 else:
955 955 rev = self._state[1]
956 956 ctx = self._repo[rev]
957 957
958 958 files = ctx.getfileset(expr)
959 959
960 960 for subpath in ctx.substate:
961 961 sub = ctx.sub(subpath)
962 962
963 963 try:
964 964 files.extend(subpath + '/' + f for f in sub.getfileset(expr))
965 965 except error.LookupError:
966 966 self.ui.status(_("skipping missing subrepository: %s\n")
967 967 % self.wvfs.reljoin(reporelpath(self), subpath))
968 968 return files
969 969
970 970 def walk(self, match):
971 971 ctx = self._repo[None]
972 972 return ctx.walk(match)
973 973
974 974 @annotatesubrepoerror
975 975 def forget(self, match, prefix):
976 976 return cmdutil.forget(self.ui, self._repo, match,
977 977 self.wvfs.reljoin(prefix, self._path), True)
978 978
979 979 @annotatesubrepoerror
980 980 def removefiles(self, matcher, prefix, after, force, subrepos):
981 981 return cmdutil.remove(self.ui, self._repo, matcher,
982 982 self.wvfs.reljoin(prefix, self._path),
983 983 after, force, subrepos)
984 984
985 985 @annotatesubrepoerror
986 986 def revert(self, substate, *pats, **opts):
987 987 # reverting a subrepo is a 2 step process:
988 988 # 1. if the no_backup is not set, revert all modified
989 989 # files inside the subrepo
990 990 # 2. update the subrepo to the revision specified in
991 991 # the corresponding substate dictionary
992 992 self.ui.status(_('reverting subrepo %s\n') % substate[0])
993 993 if not opts.get('no_backup'):
994 994 # Revert all files on the subrepo, creating backups
995 995 # Note that this will not recursively revert subrepos
996 996 # We could do it if there was a set:subrepos() predicate
997 997 opts = opts.copy()
998 998 opts['date'] = None
999 999 opts['rev'] = substate[1]
1000 1000
1001 1001 self.filerevert(*pats, **opts)
1002 1002
1003 1003 # Update the repo to the revision specified in the given substate
1004 1004 if not opts.get('dry_run'):
1005 1005 self.get(substate, overwrite=True)
1006 1006
1007 1007 def filerevert(self, *pats, **opts):
1008 1008 ctx = self._repo[opts['rev']]
1009 1009 parents = self._repo.dirstate.parents()
1010 1010 if opts.get('all'):
1011 1011 pats = ['set:modified()']
1012 1012 else:
1013 1013 pats = []
1014 1014 cmdutil.revert(self.ui, self._repo, ctx, parents, *pats, **opts)
1015 1015
1016 1016 def shortid(self, revid):
1017 1017 return revid[:12]
1018 1018
1019 1019 def verify(self):
1020 1020 try:
1021 1021 rev = self._state[1]
1022 1022 ctx = self._repo.unfiltered()[rev]
1023 1023 if ctx.hidden():
1024 1024 # Since hidden revisions aren't pushed/pulled, it seems worth an
1025 1025 # explicit warning.
1026 1026 ui = self._repo.ui
1027 1027 ui.warn(_("subrepo '%s' is hidden in revision %s\n") %
1028 1028 (self._relpath, node.short(self._ctx.node())))
1029 1029 return 0
1030 1030 except error.RepoLookupError:
1031 1031 # A missing subrepo revision may be a case of needing to pull it, so
1032 1032 # don't treat this as an error.
1033 1033 self._repo.ui.warn(_("subrepo '%s' not found in revision %s\n") %
1034 1034 (self._relpath, node.short(self._ctx.node())))
1035 1035 return 0
1036 1036
1037 1037 @propertycache
1038 1038 def wvfs(self):
1039 1039 """return own wvfs for efficiency and consitency
1040 1040 """
1041 1041 return self._repo.wvfs
1042 1042
1043 1043 @propertycache
1044 1044 def _relpath(self):
1045 1045 """return path to this subrepository as seen from outermost repository
1046 1046 """
1047 1047 # Keep consistent dir separators by avoiding vfs.join(self._path)
1048 1048 return reporelpath(self._repo)
1049 1049
1050 1050 class svnsubrepo(abstractsubrepo):
1051 1051 def __init__(self, ctx, path, state):
1052 1052 super(svnsubrepo, self).__init__(ctx, path)
1053 1053 self._state = state
1054 1054 self._exe = util.findexe('svn')
1055 1055 if not self._exe:
1056 1056 raise util.Abort(_("'svn' executable not found for subrepo '%s'")
1057 1057 % self._path)
1058 1058
1059 1059 def _svncommand(self, commands, filename='', failok=False):
1060 1060 cmd = [self._exe]
1061 1061 extrakw = {}
1062 1062 if not self.ui.interactive():
1063 1063 # Making stdin be a pipe should prevent svn from behaving
1064 1064 # interactively even if we can't pass --non-interactive.
1065 1065 extrakw['stdin'] = subprocess.PIPE
1066 1066 # Starting in svn 1.5 --non-interactive is a global flag
1067 1067 # instead of being per-command, but we need to support 1.4 so
1068 1068 # we have to be intelligent about what commands take
1069 1069 # --non-interactive.
1070 1070 if commands[0] in ('update', 'checkout', 'commit'):
1071 1071 cmd.append('--non-interactive')
1072 1072 cmd.extend(commands)
1073 1073 if filename is not None:
1074 1074 path = self.wvfs.reljoin(self._ctx.repo().origroot,
1075 1075 self._path, filename)
1076 1076 cmd.append(path)
1077 1077 env = dict(os.environ)
1078 1078 # Avoid localized output, preserve current locale for everything else.
1079 1079 lc_all = env.get('LC_ALL')
1080 1080 if lc_all:
1081 1081 env['LANG'] = lc_all
1082 1082 del env['LC_ALL']
1083 1083 env['LC_MESSAGES'] = 'C'
1084 1084 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
1085 1085 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
1086 1086 universal_newlines=True, env=env, **extrakw)
1087 1087 stdout, stderr = p.communicate()
1088 1088 stderr = stderr.strip()
1089 1089 if not failok:
1090 1090 if p.returncode:
1091 1091 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
1092 1092 if stderr:
1093 1093 self.ui.warn(stderr + '\n')
1094 1094 return stdout, stderr
1095 1095
1096 1096 @propertycache
1097 1097 def _svnversion(self):
1098 1098 output, err = self._svncommand(['--version', '--quiet'], filename=None)
1099 1099 m = re.search(r'^(\d+)\.(\d+)', output)
1100 1100 if not m:
1101 1101 raise util.Abort(_('cannot retrieve svn tool version'))
1102 1102 return (int(m.group(1)), int(m.group(2)))
1103 1103
1104 1104 def _wcrevs(self):
1105 1105 # Get the working directory revision as well as the last
1106 1106 # commit revision so we can compare the subrepo state with
1107 1107 # both. We used to store the working directory one.
1108 1108 output, err = self._svncommand(['info', '--xml'])
1109 1109 doc = xml.dom.minidom.parseString(output)
1110 1110 entries = doc.getElementsByTagName('entry')
1111 1111 lastrev, rev = '0', '0'
1112 1112 if entries:
1113 1113 rev = str(entries[0].getAttribute('revision')) or '0'
1114 1114 commits = entries[0].getElementsByTagName('commit')
1115 1115 if commits:
1116 1116 lastrev = str(commits[0].getAttribute('revision')) or '0'
1117 1117 return (lastrev, rev)
1118 1118
1119 1119 def _wcrev(self):
1120 1120 return self._wcrevs()[0]
1121 1121
1122 1122 def _wcchanged(self):
1123 1123 """Return (changes, extchanges, missing) where changes is True
1124 1124 if the working directory was changed, extchanges is
1125 1125 True if any of these changes concern an external entry and missing
1126 1126 is True if any change is a missing entry.
1127 1127 """
1128 1128 output, err = self._svncommand(['status', '--xml'])
1129 1129 externals, changes, missing = [], [], []
1130 1130 doc = xml.dom.minidom.parseString(output)
1131 1131 for e in doc.getElementsByTagName('entry'):
1132 1132 s = e.getElementsByTagName('wc-status')
1133 1133 if not s:
1134 1134 continue
1135 1135 item = s[0].getAttribute('item')
1136 1136 props = s[0].getAttribute('props')
1137 1137 path = e.getAttribute('path')
1138 1138 if item == 'external':
1139 1139 externals.append(path)
1140 1140 elif item == 'missing':
1141 1141 missing.append(path)
1142 1142 if (item not in ('', 'normal', 'unversioned', 'external')
1143 1143 or props not in ('', 'none', 'normal')):
1144 1144 changes.append(path)
1145 1145 for path in changes:
1146 1146 for ext in externals:
1147 1147 if path == ext or path.startswith(ext + os.sep):
1148 1148 return True, True, bool(missing)
1149 1149 return bool(changes), False, bool(missing)
1150 1150
1151 1151 def dirty(self, ignoreupdate=False):
1152 1152 if not self._wcchanged()[0]:
1153 1153 if self._state[1] in self._wcrevs() or ignoreupdate:
1154 1154 return False
1155 1155 return True
1156 1156
1157 1157 def basestate(self):
1158 1158 lastrev, rev = self._wcrevs()
1159 1159 if lastrev != rev:
1160 1160 # Last committed rev is not the same than rev. We would
1161 1161 # like to take lastrev but we do not know if the subrepo
1162 1162 # URL exists at lastrev. Test it and fallback to rev it
1163 1163 # is not there.
1164 1164 try:
1165 1165 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
1166 1166 return lastrev
1167 1167 except error.Abort:
1168 1168 pass
1169 1169 return rev
1170 1170
1171 1171 @annotatesubrepoerror
1172 1172 def commit(self, text, user, date):
1173 1173 # user and date are out of our hands since svn is centralized
1174 1174 changed, extchanged, missing = self._wcchanged()
1175 1175 if not changed:
1176 1176 return self.basestate()
1177 1177 if extchanged:
1178 1178 # Do not try to commit externals
1179 1179 raise util.Abort(_('cannot commit svn externals'))
1180 1180 if missing:
1181 1181 # svn can commit with missing entries but aborting like hg
1182 1182 # seems a better approach.
1183 1183 raise util.Abort(_('cannot commit missing svn entries'))
1184 1184 commitinfo, err = self._svncommand(['commit', '-m', text])
1185 1185 self.ui.status(commitinfo)
1186 1186 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
1187 1187 if not newrev:
1188 1188 if not commitinfo.strip():
1189 1189 # Sometimes, our definition of "changed" differs from
1190 1190 # svn one. For instance, svn ignores missing files
1191 1191 # when committing. If there are only missing files, no
1192 1192 # commit is made, no output and no error code.
1193 1193 raise util.Abort(_('failed to commit svn changes'))
1194 1194 raise util.Abort(commitinfo.splitlines()[-1])
1195 1195 newrev = newrev.groups()[0]
1196 1196 self.ui.status(self._svncommand(['update', '-r', newrev])[0])
1197 1197 return newrev
1198 1198
1199 1199 @annotatesubrepoerror
1200 1200 def remove(self):
1201 1201 if self.dirty():
1202 1202 self.ui.warn(_('not removing repo %s because '
1203 1203 'it has changes.\n') % self._path)
1204 1204 return
1205 1205 self.ui.note(_('removing subrepo %s\n') % self._path)
1206 1206
1207 1207 self.wvfs.rmtree(forcibly=True)
1208 1208 try:
1209 self._ctx.repo().wvfs.removedirs(os.path.dirname(self._path))
1209 pwvfs = self._ctx.repo().wvfs
1210 pwvfs.removedirs(pwvfs.dirname(self._path))
1210 1211 except OSError:
1211 1212 pass
1212 1213
1213 1214 @annotatesubrepoerror
1214 1215 def get(self, state, overwrite=False):
1215 1216 if overwrite:
1216 1217 self._svncommand(['revert', '--recursive'])
1217 1218 args = ['checkout']
1218 1219 if self._svnversion >= (1, 5):
1219 1220 args.append('--force')
1220 1221 # The revision must be specified at the end of the URL to properly
1221 1222 # update to a directory which has since been deleted and recreated.
1222 1223 args.append('%s@%s' % (state[0], state[1]))
1223 1224 status, err = self._svncommand(args, failok=True)
1224 1225 _sanitize(self.ui, self.wvfs, '.svn')
1225 1226 if not re.search('Checked out revision [0-9]+.', status):
1226 1227 if ('is already a working copy for a different URL' in err
1227 1228 and (self._wcchanged()[:2] == (False, False))):
1228 1229 # obstructed but clean working copy, so just blow it away.
1229 1230 self.remove()
1230 1231 self.get(state, overwrite=False)
1231 1232 return
1232 1233 raise util.Abort((status or err).splitlines()[-1])
1233 1234 self.ui.status(status)
1234 1235
1235 1236 @annotatesubrepoerror
1236 1237 def merge(self, state):
1237 1238 old = self._state[1]
1238 1239 new = state[1]
1239 1240 wcrev = self._wcrev()
1240 1241 if new != wcrev:
1241 1242 dirty = old == wcrev or self._wcchanged()[0]
1242 1243 if _updateprompt(self.ui, self, dirty, wcrev, new):
1243 1244 self.get(state, False)
1244 1245
1245 1246 def push(self, opts):
1246 1247 # push is a no-op for SVN
1247 1248 return True
1248 1249
1249 1250 @annotatesubrepoerror
1250 1251 def files(self):
1251 1252 output = self._svncommand(['list', '--recursive', '--xml'])[0]
1252 1253 doc = xml.dom.minidom.parseString(output)
1253 1254 paths = []
1254 1255 for e in doc.getElementsByTagName('entry'):
1255 1256 kind = str(e.getAttribute('kind'))
1256 1257 if kind != 'file':
1257 1258 continue
1258 1259 name = ''.join(c.data for c
1259 1260 in e.getElementsByTagName('name')[0].childNodes
1260 1261 if c.nodeType == c.TEXT_NODE)
1261 1262 paths.append(name.encode('utf-8'))
1262 1263 return paths
1263 1264
1264 1265 def filedata(self, name):
1265 1266 return self._svncommand(['cat'], name)[0]
1266 1267
1267 1268
1268 1269 class gitsubrepo(abstractsubrepo):
1269 1270 def __init__(self, ctx, path, state):
1270 1271 super(gitsubrepo, self).__init__(ctx, path)
1271 1272 self._state = state
1272 1273 self._abspath = ctx.repo().wjoin(path)
1273 1274 self._subparent = ctx.repo()
1274 1275 self._ensuregit()
1275 1276
1276 1277 def _ensuregit(self):
1277 1278 try:
1278 1279 self._gitexecutable = 'git'
1279 1280 out, err = self._gitnodir(['--version'])
1280 1281 except OSError as e:
1281 1282 if e.errno != 2 or os.name != 'nt':
1282 1283 raise
1283 1284 self._gitexecutable = 'git.cmd'
1284 1285 out, err = self._gitnodir(['--version'])
1285 1286 versionstatus = self._checkversion(out)
1286 1287 if versionstatus == 'unknown':
1287 1288 self.ui.warn(_('cannot retrieve git version\n'))
1288 1289 elif versionstatus == 'abort':
1289 1290 raise util.Abort(_('git subrepo requires at least 1.6.0 or later'))
1290 1291 elif versionstatus == 'warning':
1291 1292 self.ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
1292 1293
1293 1294 @staticmethod
1294 1295 def _gitversion(out):
1295 1296 m = re.search(r'^git version (\d+)\.(\d+)\.(\d+)', out)
1296 1297 if m:
1297 1298 return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
1298 1299
1299 1300 m = re.search(r'^git version (\d+)\.(\d+)', out)
1300 1301 if m:
1301 1302 return (int(m.group(1)), int(m.group(2)), 0)
1302 1303
1303 1304 return -1
1304 1305
1305 1306 @staticmethod
1306 1307 def _checkversion(out):
1307 1308 '''ensure git version is new enough
1308 1309
1309 1310 >>> _checkversion = gitsubrepo._checkversion
1310 1311 >>> _checkversion('git version 1.6.0')
1311 1312 'ok'
1312 1313 >>> _checkversion('git version 1.8.5')
1313 1314 'ok'
1314 1315 >>> _checkversion('git version 1.4.0')
1315 1316 'abort'
1316 1317 >>> _checkversion('git version 1.5.0')
1317 1318 'warning'
1318 1319 >>> _checkversion('git version 1.9-rc0')
1319 1320 'ok'
1320 1321 >>> _checkversion('git version 1.9.0.265.g81cdec2')
1321 1322 'ok'
1322 1323 >>> _checkversion('git version 1.9.0.GIT')
1323 1324 'ok'
1324 1325 >>> _checkversion('git version 12345')
1325 1326 'unknown'
1326 1327 >>> _checkversion('no')
1327 1328 'unknown'
1328 1329 '''
1329 1330 version = gitsubrepo._gitversion(out)
1330 1331 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1331 1332 # despite the docstring comment. For now, error on 1.4.0, warn on
1332 1333 # 1.5.0 but attempt to continue.
1333 1334 if version == -1:
1334 1335 return 'unknown'
1335 1336 if version < (1, 5, 0):
1336 1337 return 'abort'
1337 1338 elif version < (1, 6, 0):
1338 1339 return 'warning'
1339 1340 return 'ok'
1340 1341
1341 1342 def _gitcommand(self, commands, env=None, stream=False):
1342 1343 return self._gitdir(commands, env=env, stream=stream)[0]
1343 1344
1344 1345 def _gitdir(self, commands, env=None, stream=False):
1345 1346 return self._gitnodir(commands, env=env, stream=stream,
1346 1347 cwd=self._abspath)
1347 1348
1348 1349 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1349 1350 """Calls the git command
1350 1351
1351 1352 The methods tries to call the git command. versions prior to 1.6.0
1352 1353 are not supported and very probably fail.
1353 1354 """
1354 1355 self.ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1355 1356 # unless ui.quiet is set, print git's stderr,
1356 1357 # which is mostly progress and useful info
1357 1358 errpipe = None
1358 1359 if self.ui.quiet:
1359 1360 errpipe = open(os.devnull, 'w')
1360 1361 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
1361 1362 cwd=cwd, env=env, close_fds=util.closefds,
1362 1363 stdout=subprocess.PIPE, stderr=errpipe)
1363 1364 if stream:
1364 1365 return p.stdout, None
1365 1366
1366 1367 retdata = p.stdout.read().strip()
1367 1368 # wait for the child to exit to avoid race condition.
1368 1369 p.wait()
1369 1370
1370 1371 if p.returncode != 0 and p.returncode != 1:
1371 1372 # there are certain error codes that are ok
1372 1373 command = commands[0]
1373 1374 if command in ('cat-file', 'symbolic-ref'):
1374 1375 return retdata, p.returncode
1375 1376 # for all others, abort
1376 1377 raise util.Abort('git %s error %d in %s' %
1377 1378 (command, p.returncode, self._relpath))
1378 1379
1379 1380 return retdata, p.returncode
1380 1381
1381 1382 def _gitmissing(self):
1382 1383 return not self.wvfs.exists('.git')
1383 1384
1384 1385 def _gitstate(self):
1385 1386 return self._gitcommand(['rev-parse', 'HEAD'])
1386 1387
1387 1388 def _gitcurrentbranch(self):
1388 1389 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1389 1390 if err:
1390 1391 current = None
1391 1392 return current
1392 1393
1393 1394 def _gitremote(self, remote):
1394 1395 out = self._gitcommand(['remote', 'show', '-n', remote])
1395 1396 line = out.split('\n')[1]
1396 1397 i = line.index('URL: ') + len('URL: ')
1397 1398 return line[i:]
1398 1399
1399 1400 def _githavelocally(self, revision):
1400 1401 out, code = self._gitdir(['cat-file', '-e', revision])
1401 1402 return code == 0
1402 1403
1403 1404 def _gitisancestor(self, r1, r2):
1404 1405 base = self._gitcommand(['merge-base', r1, r2])
1405 1406 return base == r1
1406 1407
1407 1408 def _gitisbare(self):
1408 1409 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1409 1410
1410 1411 def _gitupdatestat(self):
1411 1412 """This must be run before git diff-index.
1412 1413 diff-index only looks at changes to file stat;
1413 1414 this command looks at file contents and updates the stat."""
1414 1415 self._gitcommand(['update-index', '-q', '--refresh'])
1415 1416
1416 1417 def _gitbranchmap(self):
1417 1418 '''returns 2 things:
1418 1419 a map from git branch to revision
1419 1420 a map from revision to branches'''
1420 1421 branch2rev = {}
1421 1422 rev2branch = {}
1422 1423
1423 1424 out = self._gitcommand(['for-each-ref', '--format',
1424 1425 '%(objectname) %(refname)'])
1425 1426 for line in out.split('\n'):
1426 1427 revision, ref = line.split(' ')
1427 1428 if (not ref.startswith('refs/heads/') and
1428 1429 not ref.startswith('refs/remotes/')):
1429 1430 continue
1430 1431 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1431 1432 continue # ignore remote/HEAD redirects
1432 1433 branch2rev[ref] = revision
1433 1434 rev2branch.setdefault(revision, []).append(ref)
1434 1435 return branch2rev, rev2branch
1435 1436
1436 1437 def _gittracking(self, branches):
1437 1438 'return map of remote branch to local tracking branch'
1438 1439 # assumes no more than one local tracking branch for each remote
1439 1440 tracking = {}
1440 1441 for b in branches:
1441 1442 if b.startswith('refs/remotes/'):
1442 1443 continue
1443 1444 bname = b.split('/', 2)[2]
1444 1445 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1445 1446 if remote:
1446 1447 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1447 1448 tracking['refs/remotes/%s/%s' %
1448 1449 (remote, ref.split('/', 2)[2])] = b
1449 1450 return tracking
1450 1451
1451 1452 def _abssource(self, source):
1452 1453 if '://' not in source:
1453 1454 # recognize the scp syntax as an absolute source
1454 1455 colon = source.find(':')
1455 1456 if colon != -1 and '/' not in source[:colon]:
1456 1457 return source
1457 1458 self._subsource = source
1458 1459 return _abssource(self)
1459 1460
1460 1461 def _fetch(self, source, revision):
1461 1462 if self._gitmissing():
1462 1463 source = self._abssource(source)
1463 1464 self.ui.status(_('cloning subrepo %s from %s\n') %
1464 1465 (self._relpath, source))
1465 1466 self._gitnodir(['clone', source, self._abspath])
1466 1467 if self._githavelocally(revision):
1467 1468 return
1468 1469 self.ui.status(_('pulling subrepo %s from %s\n') %
1469 1470 (self._relpath, self._gitremote('origin')))
1470 1471 # try only origin: the originally cloned repo
1471 1472 self._gitcommand(['fetch'])
1472 1473 if not self._githavelocally(revision):
1473 1474 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
1474 1475 (revision, self._relpath))
1475 1476
1476 1477 @annotatesubrepoerror
1477 1478 def dirty(self, ignoreupdate=False):
1478 1479 if self._gitmissing():
1479 1480 return self._state[1] != ''
1480 1481 if self._gitisbare():
1481 1482 return True
1482 1483 if not ignoreupdate and self._state[1] != self._gitstate():
1483 1484 # different version checked out
1484 1485 return True
1485 1486 # check for staged changes or modified files; ignore untracked files
1486 1487 self._gitupdatestat()
1487 1488 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1488 1489 return code == 1
1489 1490
1490 1491 def basestate(self):
1491 1492 return self._gitstate()
1492 1493
1493 1494 @annotatesubrepoerror
1494 1495 def get(self, state, overwrite=False):
1495 1496 source, revision, kind = state
1496 1497 if not revision:
1497 1498 self.remove()
1498 1499 return
1499 1500 self._fetch(source, revision)
1500 1501 # if the repo was set to be bare, unbare it
1501 1502 if self._gitisbare():
1502 1503 self._gitcommand(['config', 'core.bare', 'false'])
1503 1504 if self._gitstate() == revision:
1504 1505 self._gitcommand(['reset', '--hard', 'HEAD'])
1505 1506 return
1506 1507 elif self._gitstate() == revision:
1507 1508 if overwrite:
1508 1509 # first reset the index to unmark new files for commit, because
1509 1510 # reset --hard will otherwise throw away files added for commit,
1510 1511 # not just unmark them.
1511 1512 self._gitcommand(['reset', 'HEAD'])
1512 1513 self._gitcommand(['reset', '--hard', 'HEAD'])
1513 1514 return
1514 1515 branch2rev, rev2branch = self._gitbranchmap()
1515 1516
1516 1517 def checkout(args):
1517 1518 cmd = ['checkout']
1518 1519 if overwrite:
1519 1520 # first reset the index to unmark new files for commit, because
1520 1521 # the -f option will otherwise throw away files added for
1521 1522 # commit, not just unmark them.
1522 1523 self._gitcommand(['reset', 'HEAD'])
1523 1524 cmd.append('-f')
1524 1525 self._gitcommand(cmd + args)
1525 1526 _sanitize(self.ui, self.wvfs, '.git')
1526 1527
1527 1528 def rawcheckout():
1528 1529 # no branch to checkout, check it out with no branch
1529 1530 self.ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1530 1531 self._relpath)
1531 1532 self.ui.warn(_('check out a git branch if you intend '
1532 1533 'to make changes\n'))
1533 1534 checkout(['-q', revision])
1534 1535
1535 1536 if revision not in rev2branch:
1536 1537 rawcheckout()
1537 1538 return
1538 1539 branches = rev2branch[revision]
1539 1540 firstlocalbranch = None
1540 1541 for b in branches:
1541 1542 if b == 'refs/heads/master':
1542 1543 # master trumps all other branches
1543 1544 checkout(['refs/heads/master'])
1544 1545 return
1545 1546 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1546 1547 firstlocalbranch = b
1547 1548 if firstlocalbranch:
1548 1549 checkout([firstlocalbranch])
1549 1550 return
1550 1551
1551 1552 tracking = self._gittracking(branch2rev.keys())
1552 1553 # choose a remote branch already tracked if possible
1553 1554 remote = branches[0]
1554 1555 if remote not in tracking:
1555 1556 for b in branches:
1556 1557 if b in tracking:
1557 1558 remote = b
1558 1559 break
1559 1560
1560 1561 if remote not in tracking:
1561 1562 # create a new local tracking branch
1562 1563 local = remote.split('/', 3)[3]
1563 1564 checkout(['-b', local, remote])
1564 1565 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1565 1566 # When updating to a tracked remote branch,
1566 1567 # if the local tracking branch is downstream of it,
1567 1568 # a normal `git pull` would have performed a "fast-forward merge"
1568 1569 # which is equivalent to updating the local branch to the remote.
1569 1570 # Since we are only looking at branching at update, we need to
1570 1571 # detect this situation and perform this action lazily.
1571 1572 if tracking[remote] != self._gitcurrentbranch():
1572 1573 checkout([tracking[remote]])
1573 1574 self._gitcommand(['merge', '--ff', remote])
1574 1575 _sanitize(self.ui, self.wvfs, '.git')
1575 1576 else:
1576 1577 # a real merge would be required, just checkout the revision
1577 1578 rawcheckout()
1578 1579
1579 1580 @annotatesubrepoerror
1580 1581 def commit(self, text, user, date):
1581 1582 if self._gitmissing():
1582 1583 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1583 1584 cmd = ['commit', '-a', '-m', text]
1584 1585 env = os.environ.copy()
1585 1586 if user:
1586 1587 cmd += ['--author', user]
1587 1588 if date:
1588 1589 # git's date parser silently ignores when seconds < 1e9
1589 1590 # convert to ISO8601
1590 1591 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1591 1592 '%Y-%m-%dT%H:%M:%S %1%2')
1592 1593 self._gitcommand(cmd, env=env)
1593 1594 # make sure commit works otherwise HEAD might not exist under certain
1594 1595 # circumstances
1595 1596 return self._gitstate()
1596 1597
1597 1598 @annotatesubrepoerror
1598 1599 def merge(self, state):
1599 1600 source, revision, kind = state
1600 1601 self._fetch(source, revision)
1601 1602 base = self._gitcommand(['merge-base', revision, self._state[1]])
1602 1603 self._gitupdatestat()
1603 1604 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1604 1605
1605 1606 def mergefunc():
1606 1607 if base == revision:
1607 1608 self.get(state) # fast forward merge
1608 1609 elif base != self._state[1]:
1609 1610 self._gitcommand(['merge', '--no-commit', revision])
1610 1611 _sanitize(self.ui, self.wvfs, '.git')
1611 1612
1612 1613 if self.dirty():
1613 1614 if self._gitstate() != revision:
1614 1615 dirty = self._gitstate() == self._state[1] or code != 0
1615 1616 if _updateprompt(self.ui, self, dirty,
1616 1617 self._state[1][:7], revision[:7]):
1617 1618 mergefunc()
1618 1619 else:
1619 1620 mergefunc()
1620 1621
1621 1622 @annotatesubrepoerror
1622 1623 def push(self, opts):
1623 1624 force = opts.get('force')
1624 1625
1625 1626 if not self._state[1]:
1626 1627 return True
1627 1628 if self._gitmissing():
1628 1629 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1629 1630 # if a branch in origin contains the revision, nothing to do
1630 1631 branch2rev, rev2branch = self._gitbranchmap()
1631 1632 if self._state[1] in rev2branch:
1632 1633 for b in rev2branch[self._state[1]]:
1633 1634 if b.startswith('refs/remotes/origin/'):
1634 1635 return True
1635 1636 for b, revision in branch2rev.iteritems():
1636 1637 if b.startswith('refs/remotes/origin/'):
1637 1638 if self._gitisancestor(self._state[1], revision):
1638 1639 return True
1639 1640 # otherwise, try to push the currently checked out branch
1640 1641 cmd = ['push']
1641 1642 if force:
1642 1643 cmd.append('--force')
1643 1644
1644 1645 current = self._gitcurrentbranch()
1645 1646 if current:
1646 1647 # determine if the current branch is even useful
1647 1648 if not self._gitisancestor(self._state[1], current):
1648 1649 self.ui.warn(_('unrelated git branch checked out '
1649 1650 'in subrepo %s\n') % self._relpath)
1650 1651 return False
1651 1652 self.ui.status(_('pushing branch %s of subrepo %s\n') %
1652 1653 (current.split('/', 2)[2], self._relpath))
1653 1654 ret = self._gitdir(cmd + ['origin', current])
1654 1655 return ret[1] == 0
1655 1656 else:
1656 1657 self.ui.warn(_('no branch checked out in subrepo %s\n'
1657 1658 'cannot push revision %s\n') %
1658 1659 (self._relpath, self._state[1]))
1659 1660 return False
1660 1661
1661 1662 @annotatesubrepoerror
1662 1663 def add(self, ui, match, prefix, explicitonly, **opts):
1663 1664 if self._gitmissing():
1664 1665 return []
1665 1666
1666 1667 (modified, added, removed,
1667 1668 deleted, unknown, ignored, clean) = self.status(None, unknown=True,
1668 1669 clean=True)
1669 1670
1670 1671 tracked = set()
1671 1672 # dirstates 'amn' warn, 'r' is added again
1672 1673 for l in (modified, added, deleted, clean):
1673 1674 tracked.update(l)
1674 1675
1675 1676 # Unknown files not of interest will be rejected by the matcher
1676 1677 files = unknown
1677 1678 files.extend(match.files())
1678 1679
1679 1680 rejected = []
1680 1681
1681 1682 files = [f for f in sorted(set(files)) if match(f)]
1682 1683 for f in files:
1683 1684 exact = match.exact(f)
1684 1685 command = ["add"]
1685 1686 if exact:
1686 1687 command.append("-f") #should be added, even if ignored
1687 1688 if ui.verbose or not exact:
1688 1689 ui.status(_('adding %s\n') % match.rel(f))
1689 1690
1690 1691 if f in tracked: # hg prints 'adding' even if already tracked
1691 1692 if exact:
1692 1693 rejected.append(f)
1693 1694 continue
1694 1695 if not opts.get('dry_run'):
1695 1696 self._gitcommand(command + [f])
1696 1697
1697 1698 for f in rejected:
1698 1699 ui.warn(_("%s already tracked!\n") % match.abs(f))
1699 1700
1700 1701 return rejected
1701 1702
1702 1703 @annotatesubrepoerror
1703 1704 def remove(self):
1704 1705 if self._gitmissing():
1705 1706 return
1706 1707 if self.dirty():
1707 1708 self.ui.warn(_('not removing repo %s because '
1708 1709 'it has changes.\n') % self._relpath)
1709 1710 return
1710 1711 # we can't fully delete the repository as it may contain
1711 1712 # local-only history
1712 1713 self.ui.note(_('removing subrepo %s\n') % self._relpath)
1713 1714 self._gitcommand(['config', 'core.bare', 'true'])
1714 1715 for f, kind in self.wvfs.readdir():
1715 1716 if f == '.git':
1716 1717 continue
1717 1718 if kind == stat.S_IFDIR:
1718 1719 self.wvfs.rmtree(f)
1719 1720 else:
1720 1721 self.wvfs.unlink(f)
1721 1722
1722 1723 def archive(self, archiver, prefix, match=None):
1723 1724 total = 0
1724 1725 source, revision = self._state
1725 1726 if not revision:
1726 1727 return total
1727 1728 self._fetch(source, revision)
1728 1729
1729 1730 # Parse git's native archive command.
1730 1731 # This should be much faster than manually traversing the trees
1731 1732 # and objects with many subprocess calls.
1732 1733 tarstream = self._gitcommand(['archive', revision], stream=True)
1733 1734 tar = tarfile.open(fileobj=tarstream, mode='r|')
1734 1735 relpath = subrelpath(self)
1735 1736 self.ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1736 1737 for i, info in enumerate(tar):
1737 1738 if info.isdir():
1738 1739 continue
1739 1740 if match and not match(info.name):
1740 1741 continue
1741 1742 if info.issym():
1742 1743 data = info.linkname
1743 1744 else:
1744 1745 data = tar.extractfile(info).read()
1745 1746 archiver.addfile(prefix + self._path + '/' + info.name,
1746 1747 info.mode, info.issym(), data)
1747 1748 total += 1
1748 1749 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
1749 1750 unit=_('files'))
1750 1751 self.ui.progress(_('archiving (%s)') % relpath, None)
1751 1752 return total
1752 1753
1753 1754
1754 1755 @annotatesubrepoerror
1755 1756 def cat(self, match, prefix, **opts):
1756 1757 rev = self._state[1]
1757 1758 if match.anypats():
1758 1759 return 1 #No support for include/exclude yet
1759 1760
1760 1761 if not match.files():
1761 1762 return 1
1762 1763
1763 1764 for f in match.files():
1764 1765 output = self._gitcommand(["show", "%s:%s" % (rev, f)])
1765 1766 fp = cmdutil.makefileobj(self._subparent, opts.get('output'),
1766 1767 self._ctx.node(),
1767 1768 pathname=self.wvfs.reljoin(prefix, f))
1768 1769 fp.write(output)
1769 1770 fp.close()
1770 1771 return 0
1771 1772
1772 1773
1773 1774 @annotatesubrepoerror
1774 1775 def status(self, rev2, **opts):
1775 1776 rev1 = self._state[1]
1776 1777 if self._gitmissing() or not rev1:
1777 1778 # if the repo is missing, return no results
1778 1779 return scmutil.status([], [], [], [], [], [], [])
1779 1780 modified, added, removed = [], [], []
1780 1781 self._gitupdatestat()
1781 1782 if rev2:
1782 1783 command = ['diff-tree', '-r', rev1, rev2]
1783 1784 else:
1784 1785 command = ['diff-index', rev1]
1785 1786 out = self._gitcommand(command)
1786 1787 for line in out.split('\n'):
1787 1788 tab = line.find('\t')
1788 1789 if tab == -1:
1789 1790 continue
1790 1791 status, f = line[tab - 1], line[tab + 1:]
1791 1792 if status == 'M':
1792 1793 modified.append(f)
1793 1794 elif status == 'A':
1794 1795 added.append(f)
1795 1796 elif status == 'D':
1796 1797 removed.append(f)
1797 1798
1798 1799 deleted, unknown, ignored, clean = [], [], [], []
1799 1800
1800 1801 command = ['status', '--porcelain', '-z']
1801 1802 if opts.get('unknown'):
1802 1803 command += ['--untracked-files=all']
1803 1804 if opts.get('ignored'):
1804 1805 command += ['--ignored']
1805 1806 out = self._gitcommand(command)
1806 1807
1807 1808 changedfiles = set()
1808 1809 changedfiles.update(modified)
1809 1810 changedfiles.update(added)
1810 1811 changedfiles.update(removed)
1811 1812 for line in out.split('\0'):
1812 1813 if not line:
1813 1814 continue
1814 1815 st = line[0:2]
1815 1816 #moves and copies show 2 files on one line
1816 1817 if line.find('\0') >= 0:
1817 1818 filename1, filename2 = line[3:].split('\0')
1818 1819 else:
1819 1820 filename1 = line[3:]
1820 1821 filename2 = None
1821 1822
1822 1823 changedfiles.add(filename1)
1823 1824 if filename2:
1824 1825 changedfiles.add(filename2)
1825 1826
1826 1827 if st == '??':
1827 1828 unknown.append(filename1)
1828 1829 elif st == '!!':
1829 1830 ignored.append(filename1)
1830 1831
1831 1832 if opts.get('clean'):
1832 1833 out = self._gitcommand(['ls-files'])
1833 1834 for f in out.split('\n'):
1834 1835 if not f in changedfiles:
1835 1836 clean.append(f)
1836 1837
1837 1838 return scmutil.status(modified, added, removed, deleted,
1838 1839 unknown, ignored, clean)
1839 1840
1840 1841 @annotatesubrepoerror
1841 1842 def diff(self, ui, diffopts, node2, match, prefix, **opts):
1842 1843 node1 = self._state[1]
1843 1844 cmd = ['diff']
1844 1845 if opts['stat']:
1845 1846 cmd.append('--stat')
1846 1847 else:
1847 1848 # for Git, this also implies '-p'
1848 1849 cmd.append('-U%d' % diffopts.context)
1849 1850
1850 1851 gitprefix = self.wvfs.reljoin(prefix, self._path)
1851 1852
1852 1853 if diffopts.noprefix:
1853 1854 cmd.extend(['--src-prefix=%s/' % gitprefix,
1854 1855 '--dst-prefix=%s/' % gitprefix])
1855 1856 else:
1856 1857 cmd.extend(['--src-prefix=a/%s/' % gitprefix,
1857 1858 '--dst-prefix=b/%s/' % gitprefix])
1858 1859
1859 1860 if diffopts.ignorews:
1860 1861 cmd.append('--ignore-all-space')
1861 1862 if diffopts.ignorewsamount:
1862 1863 cmd.append('--ignore-space-change')
1863 1864 if self._gitversion(self._gitcommand(['--version'])) >= (1, 8, 4) \
1864 1865 and diffopts.ignoreblanklines:
1865 1866 cmd.append('--ignore-blank-lines')
1866 1867
1867 1868 cmd.append(node1)
1868 1869 if node2:
1869 1870 cmd.append(node2)
1870 1871
1871 1872 output = ""
1872 1873 if match.always():
1873 1874 output += self._gitcommand(cmd) + '\n'
1874 1875 else:
1875 1876 st = self.status(node2)[:3]
1876 1877 files = [f for sublist in st for f in sublist]
1877 1878 for f in files:
1878 1879 if match(f):
1879 1880 output += self._gitcommand(cmd + ['--', f]) + '\n'
1880 1881
1881 1882 if output.strip():
1882 1883 ui.write(output)
1883 1884
1884 1885 @annotatesubrepoerror
1885 1886 def revert(self, substate, *pats, **opts):
1886 1887 self.ui.status(_('reverting subrepo %s\n') % substate[0])
1887 1888 if not opts.get('no_backup'):
1888 1889 status = self.status(None)
1889 1890 names = status.modified
1890 1891 for name in names:
1891 1892 bakname = "%s.orig" % name
1892 1893 self.ui.note(_('saving current version of %s as %s\n') %
1893 1894 (name, bakname))
1894 1895 self.wvfs.rename(name, bakname)
1895 1896
1896 1897 if not opts.get('dry_run'):
1897 1898 self.get(substate, overwrite=True)
1898 1899 return []
1899 1900
1900 1901 def shortid(self, revid):
1901 1902 return revid[:7]
1902 1903
1903 1904 types = {
1904 1905 'hg': hgsubrepo,
1905 1906 'svn': svnsubrepo,
1906 1907 'git': gitsubrepo,
1907 1908 }
General Comments 0
You need to be logged in to leave comments. Login now