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