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