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