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