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