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