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