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