##// END OF EJS Templates
with: use context manager in subrepo _cachestorehash
Bryan O'Sullivan -
r27843:b2efdb66 default
parent child Browse files
Show More
@@ -1,1929 +1,1926 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):
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 return 1
584 584
585 585 def revert(self, substate, *pats, **opts):
586 586 self.ui.warn('%s: reverting %s subrepos is unsupported\n' \
587 587 % (substate[0], substate[2]))
588 588 return []
589 589
590 590 def shortid(self, revid):
591 591 return revid
592 592
593 593 def verify(self):
594 594 '''verify the integrity of the repository. Return 0 on success or
595 595 warning, 1 on any error.
596 596 '''
597 597 return 0
598 598
599 599 @propertycache
600 600 def wvfs(self):
601 601 """return vfs to access the working directory of this subrepository
602 602 """
603 603 return scmutil.vfs(self._ctx.repo().wvfs.join(self._path))
604 604
605 605 @propertycache
606 606 def _relpath(self):
607 607 """return path to this subrepository as seen from outermost repository
608 608 """
609 609 return self.wvfs.reljoin(reporelpath(self._ctx.repo()), self._path)
610 610
611 611 class hgsubrepo(abstractsubrepo):
612 612 def __init__(self, ctx, path, state):
613 613 super(hgsubrepo, self).__init__(ctx, path)
614 614 self._state = state
615 615 r = ctx.repo()
616 616 root = r.wjoin(path)
617 617 create = not r.wvfs.exists('%s/.hg' % path)
618 618 self._repo = hg.repository(r.baseui, root, create=create)
619 619
620 620 # Propagate the parent's --hidden option
621 621 if r is r.unfiltered():
622 622 self._repo = self._repo.unfiltered()
623 623
624 624 self.ui = self._repo.ui
625 625 for s, k in [('ui', 'commitsubrepos')]:
626 626 v = r.ui.config(s, k)
627 627 if v:
628 628 self.ui.setconfig(s, k, v, 'subrepo')
629 629 # internal config: ui._usedassubrepo
630 630 self.ui.setconfig('ui', '_usedassubrepo', 'True', 'subrepo')
631 631 self._initrepo(r, state[0], create)
632 632
633 633 def storeclean(self, path):
634 634 lock = self._repo.lock()
635 635 try:
636 636 return self._storeclean(path)
637 637 finally:
638 638 lock.release()
639 639
640 640 def _storeclean(self, path):
641 641 clean = True
642 642 itercache = self._calcstorehash(path)
643 643 for filehash in self._readstorehashcache(path):
644 644 if filehash != next(itercache, None):
645 645 clean = False
646 646 break
647 647 if clean:
648 648 # if not empty:
649 649 # the cached and current pull states have a different size
650 650 clean = next(itercache, None) is None
651 651 return clean
652 652
653 653 def _calcstorehash(self, remotepath):
654 654 '''calculate a unique "store hash"
655 655
656 656 This method is used to to detect when there are changes that may
657 657 require a push to a given remote path.'''
658 658 # sort the files that will be hashed in increasing (likely) file size
659 659 filelist = ('bookmarks', 'store/phaseroots', 'store/00changelog.i')
660 660 yield '# %s\n' % _expandedabspath(remotepath)
661 661 vfs = self._repo.vfs
662 662 for relname in filelist:
663 663 filehash = util.sha1(vfs.tryread(relname)).hexdigest()
664 664 yield '%s = %s\n' % (relname, filehash)
665 665
666 666 @propertycache
667 667 def _cachestorehashvfs(self):
668 668 return scmutil.vfs(self._repo.join('cache/storehash'))
669 669
670 670 def _readstorehashcache(self, remotepath):
671 671 '''read the store hash cache for a given remote repository'''
672 672 cachefile = _getstorehashcachename(remotepath)
673 673 return self._cachestorehashvfs.tryreadlines(cachefile, 'r')
674 674
675 675 def _cachestorehash(self, remotepath):
676 676 '''cache the current store hash
677 677
678 678 Each remote repo requires its own store hash cache, because a subrepo
679 679 store may be "clean" versus a given remote repo, but not versus another
680 680 '''
681 681 cachefile = _getstorehashcachename(remotepath)
682 lock = self._repo.lock()
683 try:
682 with self._repo.lock():
684 683 storehash = list(self._calcstorehash(remotepath))
685 684 vfs = self._cachestorehashvfs
686 685 vfs.writelines(cachefile, storehash, mode='w', notindexed=True)
687 finally:
688 lock.release()
689 686
690 687 def _getctx(self):
691 688 '''fetch the context for this subrepo revision, possibly a workingctx
692 689 '''
693 690 if self._ctx.rev() is None:
694 691 return self._repo[None] # workingctx if parent is workingctx
695 692 else:
696 693 rev = self._state[1]
697 694 return self._repo[rev]
698 695
699 696 @annotatesubrepoerror
700 697 def _initrepo(self, parentrepo, source, create):
701 698 self._repo._subparent = parentrepo
702 699 self._repo._subsource = source
703 700
704 701 if create:
705 702 lines = ['[paths]\n']
706 703
707 704 def addpathconfig(key, value):
708 705 if value:
709 706 lines.append('%s = %s\n' % (key, value))
710 707 self.ui.setconfig('paths', key, value, 'subrepo')
711 708
712 709 defpath = _abssource(self._repo, abort=False)
713 710 defpushpath = _abssource(self._repo, True, abort=False)
714 711 addpathconfig('default', defpath)
715 712 if defpath != defpushpath:
716 713 addpathconfig('default-push', defpushpath)
717 714
718 715 fp = self._repo.vfs("hgrc", "w", text=True)
719 716 try:
720 717 fp.write(''.join(lines))
721 718 finally:
722 719 fp.close()
723 720
724 721 @annotatesubrepoerror
725 722 def add(self, ui, match, prefix, explicitonly, **opts):
726 723 return cmdutil.add(ui, self._repo, match,
727 724 self.wvfs.reljoin(prefix, self._path),
728 725 explicitonly, **opts)
729 726
730 727 @annotatesubrepoerror
731 728 def addremove(self, m, prefix, opts, dry_run, similarity):
732 729 # In the same way as sub directories are processed, once in a subrepo,
733 730 # always entry any of its subrepos. Don't corrupt the options that will
734 731 # be used to process sibling subrepos however.
735 732 opts = copy.copy(opts)
736 733 opts['subrepos'] = True
737 734 return scmutil.addremove(self._repo, m,
738 735 self.wvfs.reljoin(prefix, self._path), opts,
739 736 dry_run, similarity)
740 737
741 738 @annotatesubrepoerror
742 739 def cat(self, match, prefix, **opts):
743 740 rev = self._state[1]
744 741 ctx = self._repo[rev]
745 742 return cmdutil.cat(self.ui, self._repo, ctx, match, prefix, **opts)
746 743
747 744 @annotatesubrepoerror
748 745 def status(self, rev2, **opts):
749 746 try:
750 747 rev1 = self._state[1]
751 748 ctx1 = self._repo[rev1]
752 749 ctx2 = self._repo[rev2]
753 750 return self._repo.status(ctx1, ctx2, **opts)
754 751 except error.RepoLookupError as inst:
755 752 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
756 753 % (inst, subrelpath(self)))
757 754 return scmutil.status([], [], [], [], [], [], [])
758 755
759 756 @annotatesubrepoerror
760 757 def diff(self, ui, diffopts, node2, match, prefix, **opts):
761 758 try:
762 759 node1 = node.bin(self._state[1])
763 760 # We currently expect node2 to come from substate and be
764 761 # in hex format
765 762 if node2 is not None:
766 763 node2 = node.bin(node2)
767 764 cmdutil.diffordiffstat(ui, self._repo, diffopts,
768 765 node1, node2, match,
769 766 prefix=posixpath.join(prefix, self._path),
770 767 listsubrepos=True, **opts)
771 768 except error.RepoLookupError as inst:
772 769 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
773 770 % (inst, subrelpath(self)))
774 771
775 772 @annotatesubrepoerror
776 773 def archive(self, archiver, prefix, match=None):
777 774 self._get(self._state + ('hg',))
778 775 total = abstractsubrepo.archive(self, archiver, prefix, match)
779 776 rev = self._state[1]
780 777 ctx = self._repo[rev]
781 778 for subpath in ctx.substate:
782 779 s = subrepo(ctx, subpath, True)
783 780 submatch = matchmod.narrowmatcher(subpath, match)
784 781 total += s.archive(archiver, prefix + self._path + '/', submatch)
785 782 return total
786 783
787 784 @annotatesubrepoerror
788 785 def dirty(self, ignoreupdate=False):
789 786 r = self._state[1]
790 787 if r == '' and not ignoreupdate: # no state recorded
791 788 return True
792 789 w = self._repo[None]
793 790 if r != w.p1().hex() and not ignoreupdate:
794 791 # different version checked out
795 792 return True
796 793 return w.dirty() # working directory changed
797 794
798 795 def basestate(self):
799 796 return self._repo['.'].hex()
800 797
801 798 def checknested(self, path):
802 799 return self._repo._checknested(self._repo.wjoin(path))
803 800
804 801 @annotatesubrepoerror
805 802 def commit(self, text, user, date):
806 803 # don't bother committing in the subrepo if it's only been
807 804 # updated
808 805 if not self.dirty(True):
809 806 return self._repo['.'].hex()
810 807 self.ui.debug("committing subrepo %s\n" % subrelpath(self))
811 808 n = self._repo.commit(text, user, date)
812 809 if not n:
813 810 return self._repo['.'].hex() # different version checked out
814 811 return node.hex(n)
815 812
816 813 @annotatesubrepoerror
817 814 def phase(self, state):
818 815 return self._repo[state].phase()
819 816
820 817 @annotatesubrepoerror
821 818 def remove(self):
822 819 # we can't fully delete the repository as it may contain
823 820 # local-only history
824 821 self.ui.note(_('removing subrepo %s\n') % subrelpath(self))
825 822 hg.clean(self._repo, node.nullid, False)
826 823
827 824 def _get(self, state):
828 825 source, revision, kind = state
829 826 if revision in self._repo.unfiltered():
830 827 return True
831 828 self._repo._subsource = source
832 829 srcurl = _abssource(self._repo)
833 830 other = hg.peer(self._repo, {}, srcurl)
834 831 if len(self._repo) == 0:
835 832 self.ui.status(_('cloning subrepo %s from %s\n')
836 833 % (subrelpath(self), srcurl))
837 834 parentrepo = self._repo._subparent
838 835 # use self._repo.vfs instead of self.wvfs to remove .hg only
839 836 self._repo.vfs.rmtree()
840 837 other, cloned = hg.clone(self._repo._subparent.baseui, {},
841 838 other, self._repo.root,
842 839 update=False)
843 840 self._repo = cloned.local()
844 841 self._initrepo(parentrepo, source, create=True)
845 842 self._cachestorehash(srcurl)
846 843 else:
847 844 self.ui.status(_('pulling subrepo %s from %s\n')
848 845 % (subrelpath(self), srcurl))
849 846 cleansub = self.storeclean(srcurl)
850 847 exchange.pull(self._repo, other)
851 848 if cleansub:
852 849 # keep the repo clean after pull
853 850 self._cachestorehash(srcurl)
854 851 return False
855 852
856 853 @annotatesubrepoerror
857 854 def get(self, state, overwrite=False):
858 855 inrepo = self._get(state)
859 856 source, revision, kind = state
860 857 repo = self._repo
861 858 repo.ui.debug("getting subrepo %s\n" % self._path)
862 859 if inrepo:
863 860 urepo = repo.unfiltered()
864 861 ctx = urepo[revision]
865 862 if ctx.hidden():
866 863 urepo.ui.warn(
867 864 _('revision %s in subrepo %s is hidden\n') \
868 865 % (revision[0:12], self._path))
869 866 repo = urepo
870 867 hg.updaterepo(repo, revision, overwrite)
871 868
872 869 @annotatesubrepoerror
873 870 def merge(self, state):
874 871 self._get(state)
875 872 cur = self._repo['.']
876 873 dst = self._repo[state[1]]
877 874 anc = dst.ancestor(cur)
878 875
879 876 def mergefunc():
880 877 if anc == cur and dst.branch() == cur.branch():
881 878 self.ui.debug("updating subrepo %s\n" % subrelpath(self))
882 879 hg.update(self._repo, state[1])
883 880 elif anc == dst:
884 881 self.ui.debug("skipping subrepo %s\n" % subrelpath(self))
885 882 else:
886 883 self.ui.debug("merging subrepo %s\n" % subrelpath(self))
887 884 hg.merge(self._repo, state[1], remind=False)
888 885
889 886 wctx = self._repo[None]
890 887 if self.dirty():
891 888 if anc != dst:
892 889 if _updateprompt(self.ui, self, wctx.dirty(), cur, dst):
893 890 mergefunc()
894 891 else:
895 892 mergefunc()
896 893 else:
897 894 mergefunc()
898 895
899 896 @annotatesubrepoerror
900 897 def push(self, opts):
901 898 force = opts.get('force')
902 899 newbranch = opts.get('new_branch')
903 900 ssh = opts.get('ssh')
904 901
905 902 # push subrepos depth-first for coherent ordering
906 903 c = self._repo['']
907 904 subs = c.substate # only repos that are committed
908 905 for s in sorted(subs):
909 906 if c.sub(s).push(opts) == 0:
910 907 return False
911 908
912 909 dsturl = _abssource(self._repo, True)
913 910 if not force:
914 911 if self.storeclean(dsturl):
915 912 self.ui.status(
916 913 _('no changes made to subrepo %s since last push to %s\n')
917 914 % (subrelpath(self), dsturl))
918 915 return None
919 916 self.ui.status(_('pushing subrepo %s to %s\n') %
920 917 (subrelpath(self), dsturl))
921 918 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
922 919 res = exchange.push(self._repo, other, force, newbranch=newbranch)
923 920
924 921 # the repo is now clean
925 922 self._cachestorehash(dsturl)
926 923 return res.cgresult
927 924
928 925 @annotatesubrepoerror
929 926 def outgoing(self, ui, dest, opts):
930 927 if 'rev' in opts or 'branch' in opts:
931 928 opts = copy.copy(opts)
932 929 opts.pop('rev', None)
933 930 opts.pop('branch', None)
934 931 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
935 932
936 933 @annotatesubrepoerror
937 934 def incoming(self, ui, source, opts):
938 935 if 'rev' in opts or 'branch' in opts:
939 936 opts = copy.copy(opts)
940 937 opts.pop('rev', None)
941 938 opts.pop('branch', None)
942 939 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
943 940
944 941 @annotatesubrepoerror
945 942 def files(self):
946 943 rev = self._state[1]
947 944 ctx = self._repo[rev]
948 945 return ctx.manifest().keys()
949 946
950 947 def filedata(self, name):
951 948 rev = self._state[1]
952 949 return self._repo[rev][name].data()
953 950
954 951 def fileflags(self, name):
955 952 rev = self._state[1]
956 953 ctx = self._repo[rev]
957 954 return ctx.flags(name)
958 955
959 956 @annotatesubrepoerror
960 957 def printfiles(self, ui, m, fm, fmt, subrepos):
961 958 # If the parent context is a workingctx, use the workingctx here for
962 959 # consistency.
963 960 if self._ctx.rev() is None:
964 961 ctx = self._repo[None]
965 962 else:
966 963 rev = self._state[1]
967 964 ctx = self._repo[rev]
968 965 return cmdutil.files(ui, ctx, m, fm, fmt, subrepos)
969 966
970 967 @annotatesubrepoerror
971 968 def getfileset(self, expr):
972 969 if self._ctx.rev() is None:
973 970 ctx = self._repo[None]
974 971 else:
975 972 rev = self._state[1]
976 973 ctx = self._repo[rev]
977 974
978 975 files = ctx.getfileset(expr)
979 976
980 977 for subpath in ctx.substate:
981 978 sub = ctx.sub(subpath)
982 979
983 980 try:
984 981 files.extend(subpath + '/' + f for f in sub.getfileset(expr))
985 982 except error.LookupError:
986 983 self.ui.status(_("skipping missing subrepository: %s\n")
987 984 % self.wvfs.reljoin(reporelpath(self), subpath))
988 985 return files
989 986
990 987 def walk(self, match):
991 988 ctx = self._repo[None]
992 989 return ctx.walk(match)
993 990
994 991 @annotatesubrepoerror
995 992 def forget(self, match, prefix):
996 993 return cmdutil.forget(self.ui, self._repo, match,
997 994 self.wvfs.reljoin(prefix, self._path), True)
998 995
999 996 @annotatesubrepoerror
1000 997 def removefiles(self, matcher, prefix, after, force, subrepos):
1001 998 return cmdutil.remove(self.ui, self._repo, matcher,
1002 999 self.wvfs.reljoin(prefix, self._path),
1003 1000 after, force, subrepos)
1004 1001
1005 1002 @annotatesubrepoerror
1006 1003 def revert(self, substate, *pats, **opts):
1007 1004 # reverting a subrepo is a 2 step process:
1008 1005 # 1. if the no_backup is not set, revert all modified
1009 1006 # files inside the subrepo
1010 1007 # 2. update the subrepo to the revision specified in
1011 1008 # the corresponding substate dictionary
1012 1009 self.ui.status(_('reverting subrepo %s\n') % substate[0])
1013 1010 if not opts.get('no_backup'):
1014 1011 # Revert all files on the subrepo, creating backups
1015 1012 # Note that this will not recursively revert subrepos
1016 1013 # We could do it if there was a set:subrepos() predicate
1017 1014 opts = opts.copy()
1018 1015 opts['date'] = None
1019 1016 opts['rev'] = substate[1]
1020 1017
1021 1018 self.filerevert(*pats, **opts)
1022 1019
1023 1020 # Update the repo to the revision specified in the given substate
1024 1021 if not opts.get('dry_run'):
1025 1022 self.get(substate, overwrite=True)
1026 1023
1027 1024 def filerevert(self, *pats, **opts):
1028 1025 ctx = self._repo[opts['rev']]
1029 1026 parents = self._repo.dirstate.parents()
1030 1027 if opts.get('all'):
1031 1028 pats = ['set:modified()']
1032 1029 else:
1033 1030 pats = []
1034 1031 cmdutil.revert(self.ui, self._repo, ctx, parents, *pats, **opts)
1035 1032
1036 1033 def shortid(self, revid):
1037 1034 return revid[:12]
1038 1035
1039 1036 def verify(self):
1040 1037 try:
1041 1038 rev = self._state[1]
1042 1039 ctx = self._repo.unfiltered()[rev]
1043 1040 if ctx.hidden():
1044 1041 # Since hidden revisions aren't pushed/pulled, it seems worth an
1045 1042 # explicit warning.
1046 1043 ui = self._repo.ui
1047 1044 ui.warn(_("subrepo '%s' is hidden in revision %s\n") %
1048 1045 (self._relpath, node.short(self._ctx.node())))
1049 1046 return 0
1050 1047 except error.RepoLookupError:
1051 1048 # A missing subrepo revision may be a case of needing to pull it, so
1052 1049 # don't treat this as an error.
1053 1050 self._repo.ui.warn(_("subrepo '%s' not found in revision %s\n") %
1054 1051 (self._relpath, node.short(self._ctx.node())))
1055 1052 return 0
1056 1053
1057 1054 @propertycache
1058 1055 def wvfs(self):
1059 1056 """return own wvfs for efficiency and consistency
1060 1057 """
1061 1058 return self._repo.wvfs
1062 1059
1063 1060 @propertycache
1064 1061 def _relpath(self):
1065 1062 """return path to this subrepository as seen from outermost repository
1066 1063 """
1067 1064 # Keep consistent dir separators by avoiding vfs.join(self._path)
1068 1065 return reporelpath(self._repo)
1069 1066
1070 1067 class svnsubrepo(abstractsubrepo):
1071 1068 def __init__(self, ctx, path, state):
1072 1069 super(svnsubrepo, self).__init__(ctx, path)
1073 1070 self._state = state
1074 1071 self._exe = util.findexe('svn')
1075 1072 if not self._exe:
1076 1073 raise error.Abort(_("'svn' executable not found for subrepo '%s'")
1077 1074 % self._path)
1078 1075
1079 1076 def _svncommand(self, commands, filename='', failok=False):
1080 1077 cmd = [self._exe]
1081 1078 extrakw = {}
1082 1079 if not self.ui.interactive():
1083 1080 # Making stdin be a pipe should prevent svn from behaving
1084 1081 # interactively even if we can't pass --non-interactive.
1085 1082 extrakw['stdin'] = subprocess.PIPE
1086 1083 # Starting in svn 1.5 --non-interactive is a global flag
1087 1084 # instead of being per-command, but we need to support 1.4 so
1088 1085 # we have to be intelligent about what commands take
1089 1086 # --non-interactive.
1090 1087 if commands[0] in ('update', 'checkout', 'commit'):
1091 1088 cmd.append('--non-interactive')
1092 1089 cmd.extend(commands)
1093 1090 if filename is not None:
1094 1091 path = self.wvfs.reljoin(self._ctx.repo().origroot,
1095 1092 self._path, filename)
1096 1093 cmd.append(path)
1097 1094 env = dict(os.environ)
1098 1095 # Avoid localized output, preserve current locale for everything else.
1099 1096 lc_all = env.get('LC_ALL')
1100 1097 if lc_all:
1101 1098 env['LANG'] = lc_all
1102 1099 del env['LC_ALL']
1103 1100 env['LC_MESSAGES'] = 'C'
1104 1101 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
1105 1102 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
1106 1103 universal_newlines=True, env=env, **extrakw)
1107 1104 stdout, stderr = p.communicate()
1108 1105 stderr = stderr.strip()
1109 1106 if not failok:
1110 1107 if p.returncode:
1111 1108 raise error.Abort(stderr or 'exited with code %d'
1112 1109 % p.returncode)
1113 1110 if stderr:
1114 1111 self.ui.warn(stderr + '\n')
1115 1112 return stdout, stderr
1116 1113
1117 1114 @propertycache
1118 1115 def _svnversion(self):
1119 1116 output, err = self._svncommand(['--version', '--quiet'], filename=None)
1120 1117 m = re.search(r'^(\d+)\.(\d+)', output)
1121 1118 if not m:
1122 1119 raise error.Abort(_('cannot retrieve svn tool version'))
1123 1120 return (int(m.group(1)), int(m.group(2)))
1124 1121
1125 1122 def _wcrevs(self):
1126 1123 # Get the working directory revision as well as the last
1127 1124 # commit revision so we can compare the subrepo state with
1128 1125 # both. We used to store the working directory one.
1129 1126 output, err = self._svncommand(['info', '--xml'])
1130 1127 doc = xml.dom.minidom.parseString(output)
1131 1128 entries = doc.getElementsByTagName('entry')
1132 1129 lastrev, rev = '0', '0'
1133 1130 if entries:
1134 1131 rev = str(entries[0].getAttribute('revision')) or '0'
1135 1132 commits = entries[0].getElementsByTagName('commit')
1136 1133 if commits:
1137 1134 lastrev = str(commits[0].getAttribute('revision')) or '0'
1138 1135 return (lastrev, rev)
1139 1136
1140 1137 def _wcrev(self):
1141 1138 return self._wcrevs()[0]
1142 1139
1143 1140 def _wcchanged(self):
1144 1141 """Return (changes, extchanges, missing) where changes is True
1145 1142 if the working directory was changed, extchanges is
1146 1143 True if any of these changes concern an external entry and missing
1147 1144 is True if any change is a missing entry.
1148 1145 """
1149 1146 output, err = self._svncommand(['status', '--xml'])
1150 1147 externals, changes, missing = [], [], []
1151 1148 doc = xml.dom.minidom.parseString(output)
1152 1149 for e in doc.getElementsByTagName('entry'):
1153 1150 s = e.getElementsByTagName('wc-status')
1154 1151 if not s:
1155 1152 continue
1156 1153 item = s[0].getAttribute('item')
1157 1154 props = s[0].getAttribute('props')
1158 1155 path = e.getAttribute('path')
1159 1156 if item == 'external':
1160 1157 externals.append(path)
1161 1158 elif item == 'missing':
1162 1159 missing.append(path)
1163 1160 if (item not in ('', 'normal', 'unversioned', 'external')
1164 1161 or props not in ('', 'none', 'normal')):
1165 1162 changes.append(path)
1166 1163 for path in changes:
1167 1164 for ext in externals:
1168 1165 if path == ext or path.startswith(ext + os.sep):
1169 1166 return True, True, bool(missing)
1170 1167 return bool(changes), False, bool(missing)
1171 1168
1172 1169 def dirty(self, ignoreupdate=False):
1173 1170 if not self._wcchanged()[0]:
1174 1171 if self._state[1] in self._wcrevs() or ignoreupdate:
1175 1172 return False
1176 1173 return True
1177 1174
1178 1175 def basestate(self):
1179 1176 lastrev, rev = self._wcrevs()
1180 1177 if lastrev != rev:
1181 1178 # Last committed rev is not the same than rev. We would
1182 1179 # like to take lastrev but we do not know if the subrepo
1183 1180 # URL exists at lastrev. Test it and fallback to rev it
1184 1181 # is not there.
1185 1182 try:
1186 1183 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
1187 1184 return lastrev
1188 1185 except error.Abort:
1189 1186 pass
1190 1187 return rev
1191 1188
1192 1189 @annotatesubrepoerror
1193 1190 def commit(self, text, user, date):
1194 1191 # user and date are out of our hands since svn is centralized
1195 1192 changed, extchanged, missing = self._wcchanged()
1196 1193 if not changed:
1197 1194 return self.basestate()
1198 1195 if extchanged:
1199 1196 # Do not try to commit externals
1200 1197 raise error.Abort(_('cannot commit svn externals'))
1201 1198 if missing:
1202 1199 # svn can commit with missing entries but aborting like hg
1203 1200 # seems a better approach.
1204 1201 raise error.Abort(_('cannot commit missing svn entries'))
1205 1202 commitinfo, err = self._svncommand(['commit', '-m', text])
1206 1203 self.ui.status(commitinfo)
1207 1204 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
1208 1205 if not newrev:
1209 1206 if not commitinfo.strip():
1210 1207 # Sometimes, our definition of "changed" differs from
1211 1208 # svn one. For instance, svn ignores missing files
1212 1209 # when committing. If there are only missing files, no
1213 1210 # commit is made, no output and no error code.
1214 1211 raise error.Abort(_('failed to commit svn changes'))
1215 1212 raise error.Abort(commitinfo.splitlines()[-1])
1216 1213 newrev = newrev.groups()[0]
1217 1214 self.ui.status(self._svncommand(['update', '-r', newrev])[0])
1218 1215 return newrev
1219 1216
1220 1217 @annotatesubrepoerror
1221 1218 def remove(self):
1222 1219 if self.dirty():
1223 1220 self.ui.warn(_('not removing repo %s because '
1224 1221 'it has changes.\n') % self._path)
1225 1222 return
1226 1223 self.ui.note(_('removing subrepo %s\n') % self._path)
1227 1224
1228 1225 self.wvfs.rmtree(forcibly=True)
1229 1226 try:
1230 1227 pwvfs = self._ctx.repo().wvfs
1231 1228 pwvfs.removedirs(pwvfs.dirname(self._path))
1232 1229 except OSError:
1233 1230 pass
1234 1231
1235 1232 @annotatesubrepoerror
1236 1233 def get(self, state, overwrite=False):
1237 1234 if overwrite:
1238 1235 self._svncommand(['revert', '--recursive'])
1239 1236 args = ['checkout']
1240 1237 if self._svnversion >= (1, 5):
1241 1238 args.append('--force')
1242 1239 # The revision must be specified at the end of the URL to properly
1243 1240 # update to a directory which has since been deleted and recreated.
1244 1241 args.append('%s@%s' % (state[0], state[1]))
1245 1242 status, err = self._svncommand(args, failok=True)
1246 1243 _sanitize(self.ui, self.wvfs, '.svn')
1247 1244 if not re.search('Checked out revision [0-9]+.', status):
1248 1245 if ('is already a working copy for a different URL' in err
1249 1246 and (self._wcchanged()[:2] == (False, False))):
1250 1247 # obstructed but clean working copy, so just blow it away.
1251 1248 self.remove()
1252 1249 self.get(state, overwrite=False)
1253 1250 return
1254 1251 raise error.Abort((status or err).splitlines()[-1])
1255 1252 self.ui.status(status)
1256 1253
1257 1254 @annotatesubrepoerror
1258 1255 def merge(self, state):
1259 1256 old = self._state[1]
1260 1257 new = state[1]
1261 1258 wcrev = self._wcrev()
1262 1259 if new != wcrev:
1263 1260 dirty = old == wcrev or self._wcchanged()[0]
1264 1261 if _updateprompt(self.ui, self, dirty, wcrev, new):
1265 1262 self.get(state, False)
1266 1263
1267 1264 def push(self, opts):
1268 1265 # push is a no-op for SVN
1269 1266 return True
1270 1267
1271 1268 @annotatesubrepoerror
1272 1269 def files(self):
1273 1270 output = self._svncommand(['list', '--recursive', '--xml'])[0]
1274 1271 doc = xml.dom.minidom.parseString(output)
1275 1272 paths = []
1276 1273 for e in doc.getElementsByTagName('entry'):
1277 1274 kind = str(e.getAttribute('kind'))
1278 1275 if kind != 'file':
1279 1276 continue
1280 1277 name = ''.join(c.data for c
1281 1278 in e.getElementsByTagName('name')[0].childNodes
1282 1279 if c.nodeType == c.TEXT_NODE)
1283 1280 paths.append(name.encode('utf-8'))
1284 1281 return paths
1285 1282
1286 1283 def filedata(self, name):
1287 1284 return self._svncommand(['cat'], name)[0]
1288 1285
1289 1286
1290 1287 class gitsubrepo(abstractsubrepo):
1291 1288 def __init__(self, ctx, path, state):
1292 1289 super(gitsubrepo, self).__init__(ctx, path)
1293 1290 self._state = state
1294 1291 self._abspath = ctx.repo().wjoin(path)
1295 1292 self._subparent = ctx.repo()
1296 1293 self._ensuregit()
1297 1294
1298 1295 def _ensuregit(self):
1299 1296 try:
1300 1297 self._gitexecutable = 'git'
1301 1298 out, err = self._gitnodir(['--version'])
1302 1299 except OSError as e:
1303 1300 if e.errno != 2 or os.name != 'nt':
1304 1301 raise
1305 1302 self._gitexecutable = 'git.cmd'
1306 1303 out, err = self._gitnodir(['--version'])
1307 1304 versionstatus = self._checkversion(out)
1308 1305 if versionstatus == 'unknown':
1309 1306 self.ui.warn(_('cannot retrieve git version\n'))
1310 1307 elif versionstatus == 'abort':
1311 1308 raise error.Abort(_('git subrepo requires at least 1.6.0 or later'))
1312 1309 elif versionstatus == 'warning':
1313 1310 self.ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
1314 1311
1315 1312 @staticmethod
1316 1313 def _gitversion(out):
1317 1314 m = re.search(r'^git version (\d+)\.(\d+)\.(\d+)', out)
1318 1315 if m:
1319 1316 return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
1320 1317
1321 1318 m = re.search(r'^git version (\d+)\.(\d+)', out)
1322 1319 if m:
1323 1320 return (int(m.group(1)), int(m.group(2)), 0)
1324 1321
1325 1322 return -1
1326 1323
1327 1324 @staticmethod
1328 1325 def _checkversion(out):
1329 1326 '''ensure git version is new enough
1330 1327
1331 1328 >>> _checkversion = gitsubrepo._checkversion
1332 1329 >>> _checkversion('git version 1.6.0')
1333 1330 'ok'
1334 1331 >>> _checkversion('git version 1.8.5')
1335 1332 'ok'
1336 1333 >>> _checkversion('git version 1.4.0')
1337 1334 'abort'
1338 1335 >>> _checkversion('git version 1.5.0')
1339 1336 'warning'
1340 1337 >>> _checkversion('git version 1.9-rc0')
1341 1338 'ok'
1342 1339 >>> _checkversion('git version 1.9.0.265.g81cdec2')
1343 1340 'ok'
1344 1341 >>> _checkversion('git version 1.9.0.GIT')
1345 1342 'ok'
1346 1343 >>> _checkversion('git version 12345')
1347 1344 'unknown'
1348 1345 >>> _checkversion('no')
1349 1346 'unknown'
1350 1347 '''
1351 1348 version = gitsubrepo._gitversion(out)
1352 1349 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1353 1350 # despite the docstring comment. For now, error on 1.4.0, warn on
1354 1351 # 1.5.0 but attempt to continue.
1355 1352 if version == -1:
1356 1353 return 'unknown'
1357 1354 if version < (1, 5, 0):
1358 1355 return 'abort'
1359 1356 elif version < (1, 6, 0):
1360 1357 return 'warning'
1361 1358 return 'ok'
1362 1359
1363 1360 def _gitcommand(self, commands, env=None, stream=False):
1364 1361 return self._gitdir(commands, env=env, stream=stream)[0]
1365 1362
1366 1363 def _gitdir(self, commands, env=None, stream=False):
1367 1364 return self._gitnodir(commands, env=env, stream=stream,
1368 1365 cwd=self._abspath)
1369 1366
1370 1367 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1371 1368 """Calls the git command
1372 1369
1373 1370 The methods tries to call the git command. versions prior to 1.6.0
1374 1371 are not supported and very probably fail.
1375 1372 """
1376 1373 self.ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1377 1374 # unless ui.quiet is set, print git's stderr,
1378 1375 # which is mostly progress and useful info
1379 1376 errpipe = None
1380 1377 if self.ui.quiet:
1381 1378 errpipe = open(os.devnull, 'w')
1382 1379 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
1383 1380 cwd=cwd, env=env, close_fds=util.closefds,
1384 1381 stdout=subprocess.PIPE, stderr=errpipe)
1385 1382 if stream:
1386 1383 return p.stdout, None
1387 1384
1388 1385 retdata = p.stdout.read().strip()
1389 1386 # wait for the child to exit to avoid race condition.
1390 1387 p.wait()
1391 1388
1392 1389 if p.returncode != 0 and p.returncode != 1:
1393 1390 # there are certain error codes that are ok
1394 1391 command = commands[0]
1395 1392 if command in ('cat-file', 'symbolic-ref'):
1396 1393 return retdata, p.returncode
1397 1394 # for all others, abort
1398 1395 raise error.Abort('git %s error %d in %s' %
1399 1396 (command, p.returncode, self._relpath))
1400 1397
1401 1398 return retdata, p.returncode
1402 1399
1403 1400 def _gitmissing(self):
1404 1401 return not self.wvfs.exists('.git')
1405 1402
1406 1403 def _gitstate(self):
1407 1404 return self._gitcommand(['rev-parse', 'HEAD'])
1408 1405
1409 1406 def _gitcurrentbranch(self):
1410 1407 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1411 1408 if err:
1412 1409 current = None
1413 1410 return current
1414 1411
1415 1412 def _gitremote(self, remote):
1416 1413 out = self._gitcommand(['remote', 'show', '-n', remote])
1417 1414 line = out.split('\n')[1]
1418 1415 i = line.index('URL: ') + len('URL: ')
1419 1416 return line[i:]
1420 1417
1421 1418 def _githavelocally(self, revision):
1422 1419 out, code = self._gitdir(['cat-file', '-e', revision])
1423 1420 return code == 0
1424 1421
1425 1422 def _gitisancestor(self, r1, r2):
1426 1423 base = self._gitcommand(['merge-base', r1, r2])
1427 1424 return base == r1
1428 1425
1429 1426 def _gitisbare(self):
1430 1427 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1431 1428
1432 1429 def _gitupdatestat(self):
1433 1430 """This must be run before git diff-index.
1434 1431 diff-index only looks at changes to file stat;
1435 1432 this command looks at file contents and updates the stat."""
1436 1433 self._gitcommand(['update-index', '-q', '--refresh'])
1437 1434
1438 1435 def _gitbranchmap(self):
1439 1436 '''returns 2 things:
1440 1437 a map from git branch to revision
1441 1438 a map from revision to branches'''
1442 1439 branch2rev = {}
1443 1440 rev2branch = {}
1444 1441
1445 1442 out = self._gitcommand(['for-each-ref', '--format',
1446 1443 '%(objectname) %(refname)'])
1447 1444 for line in out.split('\n'):
1448 1445 revision, ref = line.split(' ')
1449 1446 if (not ref.startswith('refs/heads/') and
1450 1447 not ref.startswith('refs/remotes/')):
1451 1448 continue
1452 1449 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1453 1450 continue # ignore remote/HEAD redirects
1454 1451 branch2rev[ref] = revision
1455 1452 rev2branch.setdefault(revision, []).append(ref)
1456 1453 return branch2rev, rev2branch
1457 1454
1458 1455 def _gittracking(self, branches):
1459 1456 'return map of remote branch to local tracking branch'
1460 1457 # assumes no more than one local tracking branch for each remote
1461 1458 tracking = {}
1462 1459 for b in branches:
1463 1460 if b.startswith('refs/remotes/'):
1464 1461 continue
1465 1462 bname = b.split('/', 2)[2]
1466 1463 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1467 1464 if remote:
1468 1465 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1469 1466 tracking['refs/remotes/%s/%s' %
1470 1467 (remote, ref.split('/', 2)[2])] = b
1471 1468 return tracking
1472 1469
1473 1470 def _abssource(self, source):
1474 1471 if '://' not in source:
1475 1472 # recognize the scp syntax as an absolute source
1476 1473 colon = source.find(':')
1477 1474 if colon != -1 and '/' not in source[:colon]:
1478 1475 return source
1479 1476 self._subsource = source
1480 1477 return _abssource(self)
1481 1478
1482 1479 def _fetch(self, source, revision):
1483 1480 if self._gitmissing():
1484 1481 source = self._abssource(source)
1485 1482 self.ui.status(_('cloning subrepo %s from %s\n') %
1486 1483 (self._relpath, source))
1487 1484 self._gitnodir(['clone', source, self._abspath])
1488 1485 if self._githavelocally(revision):
1489 1486 return
1490 1487 self.ui.status(_('pulling subrepo %s from %s\n') %
1491 1488 (self._relpath, self._gitremote('origin')))
1492 1489 # try only origin: the originally cloned repo
1493 1490 self._gitcommand(['fetch'])
1494 1491 if not self._githavelocally(revision):
1495 1492 raise error.Abort(_("revision %s does not exist in subrepo %s\n") %
1496 1493 (revision, self._relpath))
1497 1494
1498 1495 @annotatesubrepoerror
1499 1496 def dirty(self, ignoreupdate=False):
1500 1497 if self._gitmissing():
1501 1498 return self._state[1] != ''
1502 1499 if self._gitisbare():
1503 1500 return True
1504 1501 if not ignoreupdate and self._state[1] != self._gitstate():
1505 1502 # different version checked out
1506 1503 return True
1507 1504 # check for staged changes or modified files; ignore untracked files
1508 1505 self._gitupdatestat()
1509 1506 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1510 1507 return code == 1
1511 1508
1512 1509 def basestate(self):
1513 1510 return self._gitstate()
1514 1511
1515 1512 @annotatesubrepoerror
1516 1513 def get(self, state, overwrite=False):
1517 1514 source, revision, kind = state
1518 1515 if not revision:
1519 1516 self.remove()
1520 1517 return
1521 1518 self._fetch(source, revision)
1522 1519 # if the repo was set to be bare, unbare it
1523 1520 if self._gitisbare():
1524 1521 self._gitcommand(['config', 'core.bare', 'false'])
1525 1522 if self._gitstate() == revision:
1526 1523 self._gitcommand(['reset', '--hard', 'HEAD'])
1527 1524 return
1528 1525 elif self._gitstate() == revision:
1529 1526 if overwrite:
1530 1527 # first reset the index to unmark new files for commit, because
1531 1528 # reset --hard will otherwise throw away files added for commit,
1532 1529 # not just unmark them.
1533 1530 self._gitcommand(['reset', 'HEAD'])
1534 1531 self._gitcommand(['reset', '--hard', 'HEAD'])
1535 1532 return
1536 1533 branch2rev, rev2branch = self._gitbranchmap()
1537 1534
1538 1535 def checkout(args):
1539 1536 cmd = ['checkout']
1540 1537 if overwrite:
1541 1538 # first reset the index to unmark new files for commit, because
1542 1539 # the -f option will otherwise throw away files added for
1543 1540 # commit, not just unmark them.
1544 1541 self._gitcommand(['reset', 'HEAD'])
1545 1542 cmd.append('-f')
1546 1543 self._gitcommand(cmd + args)
1547 1544 _sanitize(self.ui, self.wvfs, '.git')
1548 1545
1549 1546 def rawcheckout():
1550 1547 # no branch to checkout, check it out with no branch
1551 1548 self.ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1552 1549 self._relpath)
1553 1550 self.ui.warn(_('check out a git branch if you intend '
1554 1551 'to make changes\n'))
1555 1552 checkout(['-q', revision])
1556 1553
1557 1554 if revision not in rev2branch:
1558 1555 rawcheckout()
1559 1556 return
1560 1557 branches = rev2branch[revision]
1561 1558 firstlocalbranch = None
1562 1559 for b in branches:
1563 1560 if b == 'refs/heads/master':
1564 1561 # master trumps all other branches
1565 1562 checkout(['refs/heads/master'])
1566 1563 return
1567 1564 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1568 1565 firstlocalbranch = b
1569 1566 if firstlocalbranch:
1570 1567 checkout([firstlocalbranch])
1571 1568 return
1572 1569
1573 1570 tracking = self._gittracking(branch2rev.keys())
1574 1571 # choose a remote branch already tracked if possible
1575 1572 remote = branches[0]
1576 1573 if remote not in tracking:
1577 1574 for b in branches:
1578 1575 if b in tracking:
1579 1576 remote = b
1580 1577 break
1581 1578
1582 1579 if remote not in tracking:
1583 1580 # create a new local tracking branch
1584 1581 local = remote.split('/', 3)[3]
1585 1582 checkout(['-b', local, remote])
1586 1583 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1587 1584 # When updating to a tracked remote branch,
1588 1585 # if the local tracking branch is downstream of it,
1589 1586 # a normal `git pull` would have performed a "fast-forward merge"
1590 1587 # which is equivalent to updating the local branch to the remote.
1591 1588 # Since we are only looking at branching at update, we need to
1592 1589 # detect this situation and perform this action lazily.
1593 1590 if tracking[remote] != self._gitcurrentbranch():
1594 1591 checkout([tracking[remote]])
1595 1592 self._gitcommand(['merge', '--ff', remote])
1596 1593 _sanitize(self.ui, self.wvfs, '.git')
1597 1594 else:
1598 1595 # a real merge would be required, just checkout the revision
1599 1596 rawcheckout()
1600 1597
1601 1598 @annotatesubrepoerror
1602 1599 def commit(self, text, user, date):
1603 1600 if self._gitmissing():
1604 1601 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1605 1602 cmd = ['commit', '-a', '-m', text]
1606 1603 env = os.environ.copy()
1607 1604 if user:
1608 1605 cmd += ['--author', user]
1609 1606 if date:
1610 1607 # git's date parser silently ignores when seconds < 1e9
1611 1608 # convert to ISO8601
1612 1609 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1613 1610 '%Y-%m-%dT%H:%M:%S %1%2')
1614 1611 self._gitcommand(cmd, env=env)
1615 1612 # make sure commit works otherwise HEAD might not exist under certain
1616 1613 # circumstances
1617 1614 return self._gitstate()
1618 1615
1619 1616 @annotatesubrepoerror
1620 1617 def merge(self, state):
1621 1618 source, revision, kind = state
1622 1619 self._fetch(source, revision)
1623 1620 base = self._gitcommand(['merge-base', revision, self._state[1]])
1624 1621 self._gitupdatestat()
1625 1622 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1626 1623
1627 1624 def mergefunc():
1628 1625 if base == revision:
1629 1626 self.get(state) # fast forward merge
1630 1627 elif base != self._state[1]:
1631 1628 self._gitcommand(['merge', '--no-commit', revision])
1632 1629 _sanitize(self.ui, self.wvfs, '.git')
1633 1630
1634 1631 if self.dirty():
1635 1632 if self._gitstate() != revision:
1636 1633 dirty = self._gitstate() == self._state[1] or code != 0
1637 1634 if _updateprompt(self.ui, self, dirty,
1638 1635 self._state[1][:7], revision[:7]):
1639 1636 mergefunc()
1640 1637 else:
1641 1638 mergefunc()
1642 1639
1643 1640 @annotatesubrepoerror
1644 1641 def push(self, opts):
1645 1642 force = opts.get('force')
1646 1643
1647 1644 if not self._state[1]:
1648 1645 return True
1649 1646 if self._gitmissing():
1650 1647 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1651 1648 # if a branch in origin contains the revision, nothing to do
1652 1649 branch2rev, rev2branch = self._gitbranchmap()
1653 1650 if self._state[1] in rev2branch:
1654 1651 for b in rev2branch[self._state[1]]:
1655 1652 if b.startswith('refs/remotes/origin/'):
1656 1653 return True
1657 1654 for b, revision in branch2rev.iteritems():
1658 1655 if b.startswith('refs/remotes/origin/'):
1659 1656 if self._gitisancestor(self._state[1], revision):
1660 1657 return True
1661 1658 # otherwise, try to push the currently checked out branch
1662 1659 cmd = ['push']
1663 1660 if force:
1664 1661 cmd.append('--force')
1665 1662
1666 1663 current = self._gitcurrentbranch()
1667 1664 if current:
1668 1665 # determine if the current branch is even useful
1669 1666 if not self._gitisancestor(self._state[1], current):
1670 1667 self.ui.warn(_('unrelated git branch checked out '
1671 1668 'in subrepo %s\n') % self._relpath)
1672 1669 return False
1673 1670 self.ui.status(_('pushing branch %s of subrepo %s\n') %
1674 1671 (current.split('/', 2)[2], self._relpath))
1675 1672 ret = self._gitdir(cmd + ['origin', current])
1676 1673 return ret[1] == 0
1677 1674 else:
1678 1675 self.ui.warn(_('no branch checked out in subrepo %s\n'
1679 1676 'cannot push revision %s\n') %
1680 1677 (self._relpath, self._state[1]))
1681 1678 return False
1682 1679
1683 1680 @annotatesubrepoerror
1684 1681 def add(self, ui, match, prefix, explicitonly, **opts):
1685 1682 if self._gitmissing():
1686 1683 return []
1687 1684
1688 1685 (modified, added, removed,
1689 1686 deleted, unknown, ignored, clean) = self.status(None, unknown=True,
1690 1687 clean=True)
1691 1688
1692 1689 tracked = set()
1693 1690 # dirstates 'amn' warn, 'r' is added again
1694 1691 for l in (modified, added, deleted, clean):
1695 1692 tracked.update(l)
1696 1693
1697 1694 # Unknown files not of interest will be rejected by the matcher
1698 1695 files = unknown
1699 1696 files.extend(match.files())
1700 1697
1701 1698 rejected = []
1702 1699
1703 1700 files = [f for f in sorted(set(files)) if match(f)]
1704 1701 for f in files:
1705 1702 exact = match.exact(f)
1706 1703 command = ["add"]
1707 1704 if exact:
1708 1705 command.append("-f") #should be added, even if ignored
1709 1706 if ui.verbose or not exact:
1710 1707 ui.status(_('adding %s\n') % match.rel(f))
1711 1708
1712 1709 if f in tracked: # hg prints 'adding' even if already tracked
1713 1710 if exact:
1714 1711 rejected.append(f)
1715 1712 continue
1716 1713 if not opts.get('dry_run'):
1717 1714 self._gitcommand(command + [f])
1718 1715
1719 1716 for f in rejected:
1720 1717 ui.warn(_("%s already tracked!\n") % match.abs(f))
1721 1718
1722 1719 return rejected
1723 1720
1724 1721 @annotatesubrepoerror
1725 1722 def remove(self):
1726 1723 if self._gitmissing():
1727 1724 return
1728 1725 if self.dirty():
1729 1726 self.ui.warn(_('not removing repo %s because '
1730 1727 'it has changes.\n') % self._relpath)
1731 1728 return
1732 1729 # we can't fully delete the repository as it may contain
1733 1730 # local-only history
1734 1731 self.ui.note(_('removing subrepo %s\n') % self._relpath)
1735 1732 self._gitcommand(['config', 'core.bare', 'true'])
1736 1733 for f, kind in self.wvfs.readdir():
1737 1734 if f == '.git':
1738 1735 continue
1739 1736 if kind == stat.S_IFDIR:
1740 1737 self.wvfs.rmtree(f)
1741 1738 else:
1742 1739 self.wvfs.unlink(f)
1743 1740
1744 1741 def archive(self, archiver, prefix, match=None):
1745 1742 total = 0
1746 1743 source, revision = self._state
1747 1744 if not revision:
1748 1745 return total
1749 1746 self._fetch(source, revision)
1750 1747
1751 1748 # Parse git's native archive command.
1752 1749 # This should be much faster than manually traversing the trees
1753 1750 # and objects with many subprocess calls.
1754 1751 tarstream = self._gitcommand(['archive', revision], stream=True)
1755 1752 tar = tarfile.open(fileobj=tarstream, mode='r|')
1756 1753 relpath = subrelpath(self)
1757 1754 self.ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1758 1755 for i, info in enumerate(tar):
1759 1756 if info.isdir():
1760 1757 continue
1761 1758 if match and not match(info.name):
1762 1759 continue
1763 1760 if info.issym():
1764 1761 data = info.linkname
1765 1762 else:
1766 1763 data = tar.extractfile(info).read()
1767 1764 archiver.addfile(prefix + self._path + '/' + info.name,
1768 1765 info.mode, info.issym(), data)
1769 1766 total += 1
1770 1767 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
1771 1768 unit=_('files'))
1772 1769 self.ui.progress(_('archiving (%s)') % relpath, None)
1773 1770 return total
1774 1771
1775 1772
1776 1773 @annotatesubrepoerror
1777 1774 def cat(self, match, prefix, **opts):
1778 1775 rev = self._state[1]
1779 1776 if match.anypats():
1780 1777 return 1 #No support for include/exclude yet
1781 1778
1782 1779 if not match.files():
1783 1780 return 1
1784 1781
1785 1782 for f in match.files():
1786 1783 output = self._gitcommand(["show", "%s:%s" % (rev, f)])
1787 1784 fp = cmdutil.makefileobj(self._subparent, opts.get('output'),
1788 1785 self._ctx.node(),
1789 1786 pathname=self.wvfs.reljoin(prefix, f))
1790 1787 fp.write(output)
1791 1788 fp.close()
1792 1789 return 0
1793 1790
1794 1791
1795 1792 @annotatesubrepoerror
1796 1793 def status(self, rev2, **opts):
1797 1794 rev1 = self._state[1]
1798 1795 if self._gitmissing() or not rev1:
1799 1796 # if the repo is missing, return no results
1800 1797 return scmutil.status([], [], [], [], [], [], [])
1801 1798 modified, added, removed = [], [], []
1802 1799 self._gitupdatestat()
1803 1800 if rev2:
1804 1801 command = ['diff-tree', '-r', rev1, rev2]
1805 1802 else:
1806 1803 command = ['diff-index', rev1]
1807 1804 out = self._gitcommand(command)
1808 1805 for line in out.split('\n'):
1809 1806 tab = line.find('\t')
1810 1807 if tab == -1:
1811 1808 continue
1812 1809 status, f = line[tab - 1], line[tab + 1:]
1813 1810 if status == 'M':
1814 1811 modified.append(f)
1815 1812 elif status == 'A':
1816 1813 added.append(f)
1817 1814 elif status == 'D':
1818 1815 removed.append(f)
1819 1816
1820 1817 deleted, unknown, ignored, clean = [], [], [], []
1821 1818
1822 1819 command = ['status', '--porcelain', '-z']
1823 1820 if opts.get('unknown'):
1824 1821 command += ['--untracked-files=all']
1825 1822 if opts.get('ignored'):
1826 1823 command += ['--ignored']
1827 1824 out = self._gitcommand(command)
1828 1825
1829 1826 changedfiles = set()
1830 1827 changedfiles.update(modified)
1831 1828 changedfiles.update(added)
1832 1829 changedfiles.update(removed)
1833 1830 for line in out.split('\0'):
1834 1831 if not line:
1835 1832 continue
1836 1833 st = line[0:2]
1837 1834 #moves and copies show 2 files on one line
1838 1835 if line.find('\0') >= 0:
1839 1836 filename1, filename2 = line[3:].split('\0')
1840 1837 else:
1841 1838 filename1 = line[3:]
1842 1839 filename2 = None
1843 1840
1844 1841 changedfiles.add(filename1)
1845 1842 if filename2:
1846 1843 changedfiles.add(filename2)
1847 1844
1848 1845 if st == '??':
1849 1846 unknown.append(filename1)
1850 1847 elif st == '!!':
1851 1848 ignored.append(filename1)
1852 1849
1853 1850 if opts.get('clean'):
1854 1851 out = self._gitcommand(['ls-files'])
1855 1852 for f in out.split('\n'):
1856 1853 if not f in changedfiles:
1857 1854 clean.append(f)
1858 1855
1859 1856 return scmutil.status(modified, added, removed, deleted,
1860 1857 unknown, ignored, clean)
1861 1858
1862 1859 @annotatesubrepoerror
1863 1860 def diff(self, ui, diffopts, node2, match, prefix, **opts):
1864 1861 node1 = self._state[1]
1865 1862 cmd = ['diff']
1866 1863 if opts['stat']:
1867 1864 cmd.append('--stat')
1868 1865 else:
1869 1866 # for Git, this also implies '-p'
1870 1867 cmd.append('-U%d' % diffopts.context)
1871 1868
1872 1869 gitprefix = self.wvfs.reljoin(prefix, self._path)
1873 1870
1874 1871 if diffopts.noprefix:
1875 1872 cmd.extend(['--src-prefix=%s/' % gitprefix,
1876 1873 '--dst-prefix=%s/' % gitprefix])
1877 1874 else:
1878 1875 cmd.extend(['--src-prefix=a/%s/' % gitprefix,
1879 1876 '--dst-prefix=b/%s/' % gitprefix])
1880 1877
1881 1878 if diffopts.ignorews:
1882 1879 cmd.append('--ignore-all-space')
1883 1880 if diffopts.ignorewsamount:
1884 1881 cmd.append('--ignore-space-change')
1885 1882 if self._gitversion(self._gitcommand(['--version'])) >= (1, 8, 4) \
1886 1883 and diffopts.ignoreblanklines:
1887 1884 cmd.append('--ignore-blank-lines')
1888 1885
1889 1886 cmd.append(node1)
1890 1887 if node2:
1891 1888 cmd.append(node2)
1892 1889
1893 1890 output = ""
1894 1891 if match.always():
1895 1892 output += self._gitcommand(cmd) + '\n'
1896 1893 else:
1897 1894 st = self.status(node2)[:3]
1898 1895 files = [f for sublist in st for f in sublist]
1899 1896 for f in files:
1900 1897 if match(f):
1901 1898 output += self._gitcommand(cmd + ['--', f]) + '\n'
1902 1899
1903 1900 if output.strip():
1904 1901 ui.write(output)
1905 1902
1906 1903 @annotatesubrepoerror
1907 1904 def revert(self, substate, *pats, **opts):
1908 1905 self.ui.status(_('reverting subrepo %s\n') % substate[0])
1909 1906 if not opts.get('no_backup'):
1910 1907 status = self.status(None)
1911 1908 names = status.modified
1912 1909 for name in names:
1913 1910 bakname = scmutil.origpath(self.ui, self._subparent, name)
1914 1911 self.ui.note(_('saving current version of %s as %s\n') %
1915 1912 (name, bakname))
1916 1913 self.wvfs.rename(name, bakname)
1917 1914
1918 1915 if not opts.get('dry_run'):
1919 1916 self.get(substate, overwrite=True)
1920 1917 return []
1921 1918
1922 1919 def shortid(self, revid):
1923 1920 return revid[:7]
1924 1921
1925 1922 types = {
1926 1923 'hg': hgsubrepo,
1927 1924 'svn': svnsubrepo,
1928 1925 'git': gitsubrepo,
1929 1926 }
General Comments 0
You need to be logged in to leave comments. Login now