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