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