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