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