##// END OF EJS Templates
subrepo: don't attempt to share remote sources (issue5793)...
Matt Harbison -
r36705:eed02e19 stable
parent child Browse files
Show More
@@ -1,2150 +1,2156 b''
1 1 # subrepo.py - sub-repository handling for Mercurial
2 2 #
3 3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 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 if parentrepo.shared():
985
986 # A remote subrepo could be shared if there is a local copy
987 # relative to the parent's share source. But clone pooling doesn't
988 # assemble the repos in a tree, so that can't be consistently done.
989 # A simpler option is for the user to configure clone pooling, and
990 # work with that.
991 if parentrepo.shared() and hg.islocal(srcurl):
986 992 self.ui.status(_('sharing subrepo %s from %s\n')
987 993 % (subrelpath(self), srcurl))
988 994 shared = hg.share(self._repo._subparent.baseui,
989 995 other, self._repo.root,
990 996 update=False, bookmarks=False)
991 997 self._repo = shared.local()
992 998 else:
993 999 self.ui.status(_('cloning subrepo %s from %s\n')
994 1000 % (subrelpath(self), srcurl))
995 1001 other, cloned = hg.clone(self._repo._subparent.baseui, {},
996 1002 other, self._repo.root,
997 1003 update=False)
998 1004 self._repo = cloned.local()
999 1005 self._initrepo(parentrepo, source, create=True)
1000 1006 self._cachestorehash(srcurl)
1001 1007 else:
1002 1008 self.ui.status(_('pulling subrepo %s from %s\n')
1003 1009 % (subrelpath(self), srcurl))
1004 1010 cleansub = self.storeclean(srcurl)
1005 1011 exchange.pull(self._repo, other)
1006 1012 if cleansub:
1007 1013 # keep the repo clean after pull
1008 1014 self._cachestorehash(srcurl)
1009 1015 return False
1010 1016
1011 1017 @annotatesubrepoerror
1012 1018 def get(self, state, overwrite=False):
1013 1019 inrepo = self._get(state)
1014 1020 source, revision, kind = state
1015 1021 repo = self._repo
1016 1022 repo.ui.debug("getting subrepo %s\n" % self._path)
1017 1023 if inrepo:
1018 1024 urepo = repo.unfiltered()
1019 1025 ctx = urepo[revision]
1020 1026 if ctx.hidden():
1021 1027 urepo.ui.warn(
1022 1028 _('revision %s in subrepository "%s" is hidden\n') \
1023 1029 % (revision[0:12], self._path))
1024 1030 repo = urepo
1025 1031 hg.updaterepo(repo, revision, overwrite)
1026 1032
1027 1033 @annotatesubrepoerror
1028 1034 def merge(self, state):
1029 1035 self._get(state)
1030 1036 cur = self._repo['.']
1031 1037 dst = self._repo[state[1]]
1032 1038 anc = dst.ancestor(cur)
1033 1039
1034 1040 def mergefunc():
1035 1041 if anc == cur and dst.branch() == cur.branch():
1036 1042 self.ui.debug('updating subrepository "%s"\n'
1037 1043 % subrelpath(self))
1038 1044 hg.update(self._repo, state[1])
1039 1045 elif anc == dst:
1040 1046 self.ui.debug('skipping subrepository "%s"\n'
1041 1047 % subrelpath(self))
1042 1048 else:
1043 1049 self.ui.debug('merging subrepository "%s"\n' % subrelpath(self))
1044 1050 hg.merge(self._repo, state[1], remind=False)
1045 1051
1046 1052 wctx = self._repo[None]
1047 1053 if self.dirty():
1048 1054 if anc != dst:
1049 1055 if _updateprompt(self.ui, self, wctx.dirty(), cur, dst):
1050 1056 mergefunc()
1051 1057 else:
1052 1058 mergefunc()
1053 1059 else:
1054 1060 mergefunc()
1055 1061
1056 1062 @annotatesubrepoerror
1057 1063 def push(self, opts):
1058 1064 force = opts.get('force')
1059 1065 newbranch = opts.get('new_branch')
1060 1066 ssh = opts.get('ssh')
1061 1067
1062 1068 # push subrepos depth-first for coherent ordering
1063 1069 c = self._repo['']
1064 1070 subs = c.substate # only repos that are committed
1065 1071 for s in sorted(subs):
1066 1072 if c.sub(s).push(opts) == 0:
1067 1073 return False
1068 1074
1069 1075 dsturl = _abssource(self._repo, True)
1070 1076 if not force:
1071 1077 if self.storeclean(dsturl):
1072 1078 self.ui.status(
1073 1079 _('no changes made to subrepo %s since last push to %s\n')
1074 1080 % (subrelpath(self), dsturl))
1075 1081 return None
1076 1082 self.ui.status(_('pushing subrepo %s to %s\n') %
1077 1083 (subrelpath(self), dsturl))
1078 1084 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
1079 1085 res = exchange.push(self._repo, other, force, newbranch=newbranch)
1080 1086
1081 1087 # the repo is now clean
1082 1088 self._cachestorehash(dsturl)
1083 1089 return res.cgresult
1084 1090
1085 1091 @annotatesubrepoerror
1086 1092 def outgoing(self, ui, dest, opts):
1087 1093 if 'rev' in opts or 'branch' in opts:
1088 1094 opts = copy.copy(opts)
1089 1095 opts.pop('rev', None)
1090 1096 opts.pop('branch', None)
1091 1097 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
1092 1098
1093 1099 @annotatesubrepoerror
1094 1100 def incoming(self, ui, source, opts):
1095 1101 if 'rev' in opts or 'branch' in opts:
1096 1102 opts = copy.copy(opts)
1097 1103 opts.pop('rev', None)
1098 1104 opts.pop('branch', None)
1099 1105 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
1100 1106
1101 1107 @annotatesubrepoerror
1102 1108 def files(self):
1103 1109 rev = self._state[1]
1104 1110 ctx = self._repo[rev]
1105 1111 return ctx.manifest().keys()
1106 1112
1107 1113 def filedata(self, name, decode):
1108 1114 rev = self._state[1]
1109 1115 data = self._repo[rev][name].data()
1110 1116 if decode:
1111 1117 data = self._repo.wwritedata(name, data)
1112 1118 return data
1113 1119
1114 1120 def fileflags(self, name):
1115 1121 rev = self._state[1]
1116 1122 ctx = self._repo[rev]
1117 1123 return ctx.flags(name)
1118 1124
1119 1125 @annotatesubrepoerror
1120 1126 def printfiles(self, ui, m, fm, fmt, subrepos):
1121 1127 # If the parent context is a workingctx, use the workingctx here for
1122 1128 # consistency.
1123 1129 if self._ctx.rev() is None:
1124 1130 ctx = self._repo[None]
1125 1131 else:
1126 1132 rev = self._state[1]
1127 1133 ctx = self._repo[rev]
1128 1134 return cmdutil.files(ui, ctx, m, fm, fmt, subrepos)
1129 1135
1130 1136 @annotatesubrepoerror
1131 1137 def getfileset(self, expr):
1132 1138 if self._ctx.rev() is None:
1133 1139 ctx = self._repo[None]
1134 1140 else:
1135 1141 rev = self._state[1]
1136 1142 ctx = self._repo[rev]
1137 1143
1138 1144 files = ctx.getfileset(expr)
1139 1145
1140 1146 for subpath in ctx.substate:
1141 1147 sub = ctx.sub(subpath)
1142 1148
1143 1149 try:
1144 1150 files.extend(subpath + '/' + f for f in sub.getfileset(expr))
1145 1151 except error.LookupError:
1146 1152 self.ui.status(_("skipping missing subrepository: %s\n")
1147 1153 % self.wvfs.reljoin(reporelpath(self), subpath))
1148 1154 return files
1149 1155
1150 1156 def walk(self, match):
1151 1157 ctx = self._repo[None]
1152 1158 return ctx.walk(match)
1153 1159
1154 1160 @annotatesubrepoerror
1155 1161 def forget(self, match, prefix):
1156 1162 return cmdutil.forget(self.ui, self._repo, match,
1157 1163 self.wvfs.reljoin(prefix, self._path), True)
1158 1164
1159 1165 @annotatesubrepoerror
1160 1166 def removefiles(self, matcher, prefix, after, force, subrepos, warnings):
1161 1167 return cmdutil.remove(self.ui, self._repo, matcher,
1162 1168 self.wvfs.reljoin(prefix, self._path),
1163 1169 after, force, subrepos)
1164 1170
1165 1171 @annotatesubrepoerror
1166 1172 def revert(self, substate, *pats, **opts):
1167 1173 # reverting a subrepo is a 2 step process:
1168 1174 # 1. if the no_backup is not set, revert all modified
1169 1175 # files inside the subrepo
1170 1176 # 2. update the subrepo to the revision specified in
1171 1177 # the corresponding substate dictionary
1172 1178 self.ui.status(_('reverting subrepo %s\n') % substate[0])
1173 1179 if not opts.get(r'no_backup'):
1174 1180 # Revert all files on the subrepo, creating backups
1175 1181 # Note that this will not recursively revert subrepos
1176 1182 # We could do it if there was a set:subrepos() predicate
1177 1183 opts = opts.copy()
1178 1184 opts[r'date'] = None
1179 1185 opts[r'rev'] = substate[1]
1180 1186
1181 1187 self.filerevert(*pats, **opts)
1182 1188
1183 1189 # Update the repo to the revision specified in the given substate
1184 1190 if not opts.get(r'dry_run'):
1185 1191 self.get(substate, overwrite=True)
1186 1192
1187 1193 def filerevert(self, *pats, **opts):
1188 1194 ctx = self._repo[opts[r'rev']]
1189 1195 parents = self._repo.dirstate.parents()
1190 1196 if opts.get(r'all'):
1191 1197 pats = ['set:modified()']
1192 1198 else:
1193 1199 pats = []
1194 1200 cmdutil.revert(self.ui, self._repo, ctx, parents, *pats, **opts)
1195 1201
1196 1202 def shortid(self, revid):
1197 1203 return revid[:12]
1198 1204
1199 1205 @annotatesubrepoerror
1200 1206 def unshare(self):
1201 1207 # subrepo inherently violates our import layering rules
1202 1208 # because it wants to make repo objects from deep inside the stack
1203 1209 # so we manually delay the circular imports to not break
1204 1210 # scripts that don't use our demand-loading
1205 1211 global hg
1206 1212 from . import hg as h
1207 1213 hg = h
1208 1214
1209 1215 # Nothing prevents a user from sharing in a repo, and then making that a
1210 1216 # subrepo. Alternately, the previous unshare attempt may have failed
1211 1217 # part way through. So recurse whether or not this layer is shared.
1212 1218 if self._repo.shared():
1213 1219 self.ui.status(_("unsharing subrepo '%s'\n") % self._relpath)
1214 1220
1215 1221 hg.unshare(self.ui, self._repo)
1216 1222
1217 1223 def verify(self):
1218 1224 try:
1219 1225 rev = self._state[1]
1220 1226 ctx = self._repo.unfiltered()[rev]
1221 1227 if ctx.hidden():
1222 1228 # Since hidden revisions aren't pushed/pulled, it seems worth an
1223 1229 # explicit warning.
1224 1230 ui = self._repo.ui
1225 1231 ui.warn(_("subrepo '%s' is hidden in revision %s\n") %
1226 1232 (self._relpath, node.short(self._ctx.node())))
1227 1233 return 0
1228 1234 except error.RepoLookupError:
1229 1235 # A missing subrepo revision may be a case of needing to pull it, so
1230 1236 # don't treat this as an error.
1231 1237 self._repo.ui.warn(_("subrepo '%s' not found in revision %s\n") %
1232 1238 (self._relpath, node.short(self._ctx.node())))
1233 1239 return 0
1234 1240
1235 1241 @propertycache
1236 1242 def wvfs(self):
1237 1243 """return own wvfs for efficiency and consistency
1238 1244 """
1239 1245 return self._repo.wvfs
1240 1246
1241 1247 @propertycache
1242 1248 def _relpath(self):
1243 1249 """return path to this subrepository as seen from outermost repository
1244 1250 """
1245 1251 # Keep consistent dir separators by avoiding vfs.join(self._path)
1246 1252 return reporelpath(self._repo)
1247 1253
1248 1254 class svnsubrepo(abstractsubrepo):
1249 1255 def __init__(self, ctx, path, state, allowcreate):
1250 1256 super(svnsubrepo, self).__init__(ctx, path)
1251 1257 self._state = state
1252 1258 self._exe = util.findexe('svn')
1253 1259 if not self._exe:
1254 1260 raise error.Abort(_("'svn' executable not found for subrepo '%s'")
1255 1261 % self._path)
1256 1262
1257 1263 def _svncommand(self, commands, filename='', failok=False):
1258 1264 cmd = [self._exe]
1259 1265 extrakw = {}
1260 1266 if not self.ui.interactive():
1261 1267 # Making stdin be a pipe should prevent svn from behaving
1262 1268 # interactively even if we can't pass --non-interactive.
1263 1269 extrakw[r'stdin'] = subprocess.PIPE
1264 1270 # Starting in svn 1.5 --non-interactive is a global flag
1265 1271 # instead of being per-command, but we need to support 1.4 so
1266 1272 # we have to be intelligent about what commands take
1267 1273 # --non-interactive.
1268 1274 if commands[0] in ('update', 'checkout', 'commit'):
1269 1275 cmd.append('--non-interactive')
1270 1276 cmd.extend(commands)
1271 1277 if filename is not None:
1272 1278 path = self.wvfs.reljoin(self._ctx.repo().origroot,
1273 1279 self._path, filename)
1274 1280 cmd.append(path)
1275 1281 env = dict(encoding.environ)
1276 1282 # Avoid localized output, preserve current locale for everything else.
1277 1283 lc_all = env.get('LC_ALL')
1278 1284 if lc_all:
1279 1285 env['LANG'] = lc_all
1280 1286 del env['LC_ALL']
1281 1287 env['LC_MESSAGES'] = 'C'
1282 1288 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
1283 1289 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
1284 1290 universal_newlines=True, env=env, **extrakw)
1285 1291 stdout, stderr = p.communicate()
1286 1292 stderr = stderr.strip()
1287 1293 if not failok:
1288 1294 if p.returncode:
1289 1295 raise error.Abort(stderr or 'exited with code %d'
1290 1296 % p.returncode)
1291 1297 if stderr:
1292 1298 self.ui.warn(stderr + '\n')
1293 1299 return stdout, stderr
1294 1300
1295 1301 @propertycache
1296 1302 def _svnversion(self):
1297 1303 output, err = self._svncommand(['--version', '--quiet'], filename=None)
1298 1304 m = re.search(br'^(\d+)\.(\d+)', output)
1299 1305 if not m:
1300 1306 raise error.Abort(_('cannot retrieve svn tool version'))
1301 1307 return (int(m.group(1)), int(m.group(2)))
1302 1308
1303 1309 def _svnmissing(self):
1304 1310 return not self.wvfs.exists('.svn')
1305 1311
1306 1312 def _wcrevs(self):
1307 1313 # Get the working directory revision as well as the last
1308 1314 # commit revision so we can compare the subrepo state with
1309 1315 # both. We used to store the working directory one.
1310 1316 output, err = self._svncommand(['info', '--xml'])
1311 1317 doc = xml.dom.minidom.parseString(output)
1312 1318 entries = doc.getElementsByTagName('entry')
1313 1319 lastrev, rev = '0', '0'
1314 1320 if entries:
1315 1321 rev = str(entries[0].getAttribute('revision')) or '0'
1316 1322 commits = entries[0].getElementsByTagName('commit')
1317 1323 if commits:
1318 1324 lastrev = str(commits[0].getAttribute('revision')) or '0'
1319 1325 return (lastrev, rev)
1320 1326
1321 1327 def _wcrev(self):
1322 1328 return self._wcrevs()[0]
1323 1329
1324 1330 def _wcchanged(self):
1325 1331 """Return (changes, extchanges, missing) where changes is True
1326 1332 if the working directory was changed, extchanges is
1327 1333 True if any of these changes concern an external entry and missing
1328 1334 is True if any change is a missing entry.
1329 1335 """
1330 1336 output, err = self._svncommand(['status', '--xml'])
1331 1337 externals, changes, missing = [], [], []
1332 1338 doc = xml.dom.minidom.parseString(output)
1333 1339 for e in doc.getElementsByTagName('entry'):
1334 1340 s = e.getElementsByTagName('wc-status')
1335 1341 if not s:
1336 1342 continue
1337 1343 item = s[0].getAttribute('item')
1338 1344 props = s[0].getAttribute('props')
1339 1345 path = e.getAttribute('path')
1340 1346 if item == 'external':
1341 1347 externals.append(path)
1342 1348 elif item == 'missing':
1343 1349 missing.append(path)
1344 1350 if (item not in ('', 'normal', 'unversioned', 'external')
1345 1351 or props not in ('', 'none', 'normal')):
1346 1352 changes.append(path)
1347 1353 for path in changes:
1348 1354 for ext in externals:
1349 1355 if path == ext or path.startswith(ext + pycompat.ossep):
1350 1356 return True, True, bool(missing)
1351 1357 return bool(changes), False, bool(missing)
1352 1358
1353 1359 @annotatesubrepoerror
1354 1360 def dirty(self, ignoreupdate=False, missing=False):
1355 1361 if self._svnmissing():
1356 1362 return self._state[1] != ''
1357 1363 wcchanged = self._wcchanged()
1358 1364 changed = wcchanged[0] or (missing and wcchanged[2])
1359 1365 if not changed:
1360 1366 if self._state[1] in self._wcrevs() or ignoreupdate:
1361 1367 return False
1362 1368 return True
1363 1369
1364 1370 def basestate(self):
1365 1371 lastrev, rev = self._wcrevs()
1366 1372 if lastrev != rev:
1367 1373 # Last committed rev is not the same than rev. We would
1368 1374 # like to take lastrev but we do not know if the subrepo
1369 1375 # URL exists at lastrev. Test it and fallback to rev it
1370 1376 # is not there.
1371 1377 try:
1372 1378 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
1373 1379 return lastrev
1374 1380 except error.Abort:
1375 1381 pass
1376 1382 return rev
1377 1383
1378 1384 @annotatesubrepoerror
1379 1385 def commit(self, text, user, date):
1380 1386 # user and date are out of our hands since svn is centralized
1381 1387 changed, extchanged, missing = self._wcchanged()
1382 1388 if not changed:
1383 1389 return self.basestate()
1384 1390 if extchanged:
1385 1391 # Do not try to commit externals
1386 1392 raise error.Abort(_('cannot commit svn externals'))
1387 1393 if missing:
1388 1394 # svn can commit with missing entries but aborting like hg
1389 1395 # seems a better approach.
1390 1396 raise error.Abort(_('cannot commit missing svn entries'))
1391 1397 commitinfo, err = self._svncommand(['commit', '-m', text])
1392 1398 self.ui.status(commitinfo)
1393 1399 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
1394 1400 if not newrev:
1395 1401 if not commitinfo.strip():
1396 1402 # Sometimes, our definition of "changed" differs from
1397 1403 # svn one. For instance, svn ignores missing files
1398 1404 # when committing. If there are only missing files, no
1399 1405 # commit is made, no output and no error code.
1400 1406 raise error.Abort(_('failed to commit svn changes'))
1401 1407 raise error.Abort(commitinfo.splitlines()[-1])
1402 1408 newrev = newrev.groups()[0]
1403 1409 self.ui.status(self._svncommand(['update', '-r', newrev])[0])
1404 1410 return newrev
1405 1411
1406 1412 @annotatesubrepoerror
1407 1413 def remove(self):
1408 1414 if self.dirty():
1409 1415 self.ui.warn(_('not removing repo %s because '
1410 1416 'it has changes.\n') % self._path)
1411 1417 return
1412 1418 self.ui.note(_('removing subrepo %s\n') % self._path)
1413 1419
1414 1420 self.wvfs.rmtree(forcibly=True)
1415 1421 try:
1416 1422 pwvfs = self._ctx.repo().wvfs
1417 1423 pwvfs.removedirs(pwvfs.dirname(self._path))
1418 1424 except OSError:
1419 1425 pass
1420 1426
1421 1427 @annotatesubrepoerror
1422 1428 def get(self, state, overwrite=False):
1423 1429 if overwrite:
1424 1430 self._svncommand(['revert', '--recursive'])
1425 1431 args = ['checkout']
1426 1432 if self._svnversion >= (1, 5):
1427 1433 args.append('--force')
1428 1434 # The revision must be specified at the end of the URL to properly
1429 1435 # update to a directory which has since been deleted and recreated.
1430 1436 args.append('%s@%s' % (state[0], state[1]))
1431 1437
1432 1438 # SEC: check that the ssh url is safe
1433 1439 util.checksafessh(state[0])
1434 1440
1435 1441 status, err = self._svncommand(args, failok=True)
1436 1442 _sanitize(self.ui, self.wvfs, '.svn')
1437 1443 if not re.search('Checked out revision [0-9]+.', status):
1438 1444 if ('is already a working copy for a different URL' in err
1439 1445 and (self._wcchanged()[:2] == (False, False))):
1440 1446 # obstructed but clean working copy, so just blow it away.
1441 1447 self.remove()
1442 1448 self.get(state, overwrite=False)
1443 1449 return
1444 1450 raise error.Abort((status or err).splitlines()[-1])
1445 1451 self.ui.status(status)
1446 1452
1447 1453 @annotatesubrepoerror
1448 1454 def merge(self, state):
1449 1455 old = self._state[1]
1450 1456 new = state[1]
1451 1457 wcrev = self._wcrev()
1452 1458 if new != wcrev:
1453 1459 dirty = old == wcrev or self._wcchanged()[0]
1454 1460 if _updateprompt(self.ui, self, dirty, wcrev, new):
1455 1461 self.get(state, False)
1456 1462
1457 1463 def push(self, opts):
1458 1464 # push is a no-op for SVN
1459 1465 return True
1460 1466
1461 1467 @annotatesubrepoerror
1462 1468 def files(self):
1463 1469 output = self._svncommand(['list', '--recursive', '--xml'])[0]
1464 1470 doc = xml.dom.minidom.parseString(output)
1465 1471 paths = []
1466 1472 for e in doc.getElementsByTagName('entry'):
1467 1473 kind = str(e.getAttribute('kind'))
1468 1474 if kind != 'file':
1469 1475 continue
1470 1476 name = ''.join(c.data for c
1471 1477 in e.getElementsByTagName('name')[0].childNodes
1472 1478 if c.nodeType == c.TEXT_NODE)
1473 1479 paths.append(name.encode('utf-8'))
1474 1480 return paths
1475 1481
1476 1482 def filedata(self, name, decode):
1477 1483 return self._svncommand(['cat'], name)[0]
1478 1484
1479 1485
1480 1486 class gitsubrepo(abstractsubrepo):
1481 1487 def __init__(self, ctx, path, state, allowcreate):
1482 1488 super(gitsubrepo, self).__init__(ctx, path)
1483 1489 self._state = state
1484 1490 self._abspath = ctx.repo().wjoin(path)
1485 1491 self._subparent = ctx.repo()
1486 1492 self._ensuregit()
1487 1493
1488 1494 def _ensuregit(self):
1489 1495 try:
1490 1496 self._gitexecutable = 'git'
1491 1497 out, err = self._gitnodir(['--version'])
1492 1498 except OSError as e:
1493 1499 genericerror = _("error executing git for subrepo '%s': %s")
1494 1500 notfoundhint = _("check git is installed and in your PATH")
1495 1501 if e.errno != errno.ENOENT:
1496 1502 raise error.Abort(genericerror % (
1497 1503 self._path, encoding.strtolocal(e.strerror)))
1498 1504 elif pycompat.iswindows:
1499 1505 try:
1500 1506 self._gitexecutable = 'git.cmd'
1501 1507 out, err = self._gitnodir(['--version'])
1502 1508 except OSError as e2:
1503 1509 if e2.errno == errno.ENOENT:
1504 1510 raise error.Abort(_("couldn't find 'git' or 'git.cmd'"
1505 1511 " for subrepo '%s'") % self._path,
1506 1512 hint=notfoundhint)
1507 1513 else:
1508 1514 raise error.Abort(genericerror % (self._path,
1509 1515 encoding.strtolocal(e2.strerror)))
1510 1516 else:
1511 1517 raise error.Abort(_("couldn't find git for subrepo '%s'")
1512 1518 % self._path, hint=notfoundhint)
1513 1519 versionstatus = self._checkversion(out)
1514 1520 if versionstatus == 'unknown':
1515 1521 self.ui.warn(_('cannot retrieve git version\n'))
1516 1522 elif versionstatus == 'abort':
1517 1523 raise error.Abort(_('git subrepo requires at least 1.6.0 or later'))
1518 1524 elif versionstatus == 'warning':
1519 1525 self.ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
1520 1526
1521 1527 @staticmethod
1522 1528 def _gitversion(out):
1523 1529 m = re.search(br'^git version (\d+)\.(\d+)\.(\d+)', out)
1524 1530 if m:
1525 1531 return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
1526 1532
1527 1533 m = re.search(br'^git version (\d+)\.(\d+)', out)
1528 1534 if m:
1529 1535 return (int(m.group(1)), int(m.group(2)), 0)
1530 1536
1531 1537 return -1
1532 1538
1533 1539 @staticmethod
1534 1540 def _checkversion(out):
1535 1541 '''ensure git version is new enough
1536 1542
1537 1543 >>> _checkversion = gitsubrepo._checkversion
1538 1544 >>> _checkversion(b'git version 1.6.0')
1539 1545 'ok'
1540 1546 >>> _checkversion(b'git version 1.8.5')
1541 1547 'ok'
1542 1548 >>> _checkversion(b'git version 1.4.0')
1543 1549 'abort'
1544 1550 >>> _checkversion(b'git version 1.5.0')
1545 1551 'warning'
1546 1552 >>> _checkversion(b'git version 1.9-rc0')
1547 1553 'ok'
1548 1554 >>> _checkversion(b'git version 1.9.0.265.g81cdec2')
1549 1555 'ok'
1550 1556 >>> _checkversion(b'git version 1.9.0.GIT')
1551 1557 'ok'
1552 1558 >>> _checkversion(b'git version 12345')
1553 1559 'unknown'
1554 1560 >>> _checkversion(b'no')
1555 1561 'unknown'
1556 1562 '''
1557 1563 version = gitsubrepo._gitversion(out)
1558 1564 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1559 1565 # despite the docstring comment. For now, error on 1.4.0, warn on
1560 1566 # 1.5.0 but attempt to continue.
1561 1567 if version == -1:
1562 1568 return 'unknown'
1563 1569 if version < (1, 5, 0):
1564 1570 return 'abort'
1565 1571 elif version < (1, 6, 0):
1566 1572 return 'warning'
1567 1573 return 'ok'
1568 1574
1569 1575 def _gitcommand(self, commands, env=None, stream=False):
1570 1576 return self._gitdir(commands, env=env, stream=stream)[0]
1571 1577
1572 1578 def _gitdir(self, commands, env=None, stream=False):
1573 1579 return self._gitnodir(commands, env=env, stream=stream,
1574 1580 cwd=self._abspath)
1575 1581
1576 1582 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1577 1583 """Calls the git command
1578 1584
1579 1585 The methods tries to call the git command. versions prior to 1.6.0
1580 1586 are not supported and very probably fail.
1581 1587 """
1582 1588 self.ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1583 1589 if env is None:
1584 1590 env = encoding.environ.copy()
1585 1591 # disable localization for Git output (issue5176)
1586 1592 env['LC_ALL'] = 'C'
1587 1593 # fix for Git CVE-2015-7545
1588 1594 if 'GIT_ALLOW_PROTOCOL' not in env:
1589 1595 env['GIT_ALLOW_PROTOCOL'] = 'file:git:http:https:ssh'
1590 1596 # unless ui.quiet is set, print git's stderr,
1591 1597 # which is mostly progress and useful info
1592 1598 errpipe = None
1593 1599 if self.ui.quiet:
1594 1600 errpipe = open(os.devnull, 'w')
1595 1601 if self.ui._colormode and len(commands) and commands[0] == "diff":
1596 1602 # insert the argument in the front,
1597 1603 # the end of git diff arguments is used for paths
1598 1604 commands.insert(1, '--color')
1599 1605 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
1600 1606 cwd=cwd, env=env, close_fds=util.closefds,
1601 1607 stdout=subprocess.PIPE, stderr=errpipe)
1602 1608 if stream:
1603 1609 return p.stdout, None
1604 1610
1605 1611 retdata = p.stdout.read().strip()
1606 1612 # wait for the child to exit to avoid race condition.
1607 1613 p.wait()
1608 1614
1609 1615 if p.returncode != 0 and p.returncode != 1:
1610 1616 # there are certain error codes that are ok
1611 1617 command = commands[0]
1612 1618 if command in ('cat-file', 'symbolic-ref'):
1613 1619 return retdata, p.returncode
1614 1620 # for all others, abort
1615 1621 raise error.Abort(_('git %s error %d in %s') %
1616 1622 (command, p.returncode, self._relpath))
1617 1623
1618 1624 return retdata, p.returncode
1619 1625
1620 1626 def _gitmissing(self):
1621 1627 return not self.wvfs.exists('.git')
1622 1628
1623 1629 def _gitstate(self):
1624 1630 return self._gitcommand(['rev-parse', 'HEAD'])
1625 1631
1626 1632 def _gitcurrentbranch(self):
1627 1633 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1628 1634 if err:
1629 1635 current = None
1630 1636 return current
1631 1637
1632 1638 def _gitremote(self, remote):
1633 1639 out = self._gitcommand(['remote', 'show', '-n', remote])
1634 1640 line = out.split('\n')[1]
1635 1641 i = line.index('URL: ') + len('URL: ')
1636 1642 return line[i:]
1637 1643
1638 1644 def _githavelocally(self, revision):
1639 1645 out, code = self._gitdir(['cat-file', '-e', revision])
1640 1646 return code == 0
1641 1647
1642 1648 def _gitisancestor(self, r1, r2):
1643 1649 base = self._gitcommand(['merge-base', r1, r2])
1644 1650 return base == r1
1645 1651
1646 1652 def _gitisbare(self):
1647 1653 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1648 1654
1649 1655 def _gitupdatestat(self):
1650 1656 """This must be run before git diff-index.
1651 1657 diff-index only looks at changes to file stat;
1652 1658 this command looks at file contents and updates the stat."""
1653 1659 self._gitcommand(['update-index', '-q', '--refresh'])
1654 1660
1655 1661 def _gitbranchmap(self):
1656 1662 '''returns 2 things:
1657 1663 a map from git branch to revision
1658 1664 a map from revision to branches'''
1659 1665 branch2rev = {}
1660 1666 rev2branch = {}
1661 1667
1662 1668 out = self._gitcommand(['for-each-ref', '--format',
1663 1669 '%(objectname) %(refname)'])
1664 1670 for line in out.split('\n'):
1665 1671 revision, ref = line.split(' ')
1666 1672 if (not ref.startswith('refs/heads/') and
1667 1673 not ref.startswith('refs/remotes/')):
1668 1674 continue
1669 1675 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1670 1676 continue # ignore remote/HEAD redirects
1671 1677 branch2rev[ref] = revision
1672 1678 rev2branch.setdefault(revision, []).append(ref)
1673 1679 return branch2rev, rev2branch
1674 1680
1675 1681 def _gittracking(self, branches):
1676 1682 'return map of remote branch to local tracking branch'
1677 1683 # assumes no more than one local tracking branch for each remote
1678 1684 tracking = {}
1679 1685 for b in branches:
1680 1686 if b.startswith('refs/remotes/'):
1681 1687 continue
1682 1688 bname = b.split('/', 2)[2]
1683 1689 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1684 1690 if remote:
1685 1691 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1686 1692 tracking['refs/remotes/%s/%s' %
1687 1693 (remote, ref.split('/', 2)[2])] = b
1688 1694 return tracking
1689 1695
1690 1696 def _abssource(self, source):
1691 1697 if '://' not in source:
1692 1698 # recognize the scp syntax as an absolute source
1693 1699 colon = source.find(':')
1694 1700 if colon != -1 and '/' not in source[:colon]:
1695 1701 return source
1696 1702 self._subsource = source
1697 1703 return _abssource(self)
1698 1704
1699 1705 def _fetch(self, source, revision):
1700 1706 if self._gitmissing():
1701 1707 # SEC: check for safe ssh url
1702 1708 util.checksafessh(source)
1703 1709
1704 1710 source = self._abssource(source)
1705 1711 self.ui.status(_('cloning subrepo %s from %s\n') %
1706 1712 (self._relpath, source))
1707 1713 self._gitnodir(['clone', source, self._abspath])
1708 1714 if self._githavelocally(revision):
1709 1715 return
1710 1716 self.ui.status(_('pulling subrepo %s from %s\n') %
1711 1717 (self._relpath, self._gitremote('origin')))
1712 1718 # try only origin: the originally cloned repo
1713 1719 self._gitcommand(['fetch'])
1714 1720 if not self._githavelocally(revision):
1715 1721 raise error.Abort(_('revision %s does not exist in subrepository '
1716 1722 '"%s"\n') % (revision, self._relpath))
1717 1723
1718 1724 @annotatesubrepoerror
1719 1725 def dirty(self, ignoreupdate=False, missing=False):
1720 1726 if self._gitmissing():
1721 1727 return self._state[1] != ''
1722 1728 if self._gitisbare():
1723 1729 return True
1724 1730 if not ignoreupdate and self._state[1] != self._gitstate():
1725 1731 # different version checked out
1726 1732 return True
1727 1733 # check for staged changes or modified files; ignore untracked files
1728 1734 self._gitupdatestat()
1729 1735 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1730 1736 return code == 1
1731 1737
1732 1738 def basestate(self):
1733 1739 return self._gitstate()
1734 1740
1735 1741 @annotatesubrepoerror
1736 1742 def get(self, state, overwrite=False):
1737 1743 source, revision, kind = state
1738 1744 if not revision:
1739 1745 self.remove()
1740 1746 return
1741 1747 self._fetch(source, revision)
1742 1748 # if the repo was set to be bare, unbare it
1743 1749 if self._gitisbare():
1744 1750 self._gitcommand(['config', 'core.bare', 'false'])
1745 1751 if self._gitstate() == revision:
1746 1752 self._gitcommand(['reset', '--hard', 'HEAD'])
1747 1753 return
1748 1754 elif self._gitstate() == revision:
1749 1755 if overwrite:
1750 1756 # first reset the index to unmark new files for commit, because
1751 1757 # reset --hard will otherwise throw away files added for commit,
1752 1758 # not just unmark them.
1753 1759 self._gitcommand(['reset', 'HEAD'])
1754 1760 self._gitcommand(['reset', '--hard', 'HEAD'])
1755 1761 return
1756 1762 branch2rev, rev2branch = self._gitbranchmap()
1757 1763
1758 1764 def checkout(args):
1759 1765 cmd = ['checkout']
1760 1766 if overwrite:
1761 1767 # first reset the index to unmark new files for commit, because
1762 1768 # the -f option will otherwise throw away files added for
1763 1769 # commit, not just unmark them.
1764 1770 self._gitcommand(['reset', 'HEAD'])
1765 1771 cmd.append('-f')
1766 1772 self._gitcommand(cmd + args)
1767 1773 _sanitize(self.ui, self.wvfs, '.git')
1768 1774
1769 1775 def rawcheckout():
1770 1776 # no branch to checkout, check it out with no branch
1771 1777 self.ui.warn(_('checking out detached HEAD in '
1772 1778 'subrepository "%s"\n') % self._relpath)
1773 1779 self.ui.warn(_('check out a git branch if you intend '
1774 1780 'to make changes\n'))
1775 1781 checkout(['-q', revision])
1776 1782
1777 1783 if revision not in rev2branch:
1778 1784 rawcheckout()
1779 1785 return
1780 1786 branches = rev2branch[revision]
1781 1787 firstlocalbranch = None
1782 1788 for b in branches:
1783 1789 if b == 'refs/heads/master':
1784 1790 # master trumps all other branches
1785 1791 checkout(['refs/heads/master'])
1786 1792 return
1787 1793 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1788 1794 firstlocalbranch = b
1789 1795 if firstlocalbranch:
1790 1796 checkout([firstlocalbranch])
1791 1797 return
1792 1798
1793 1799 tracking = self._gittracking(branch2rev.keys())
1794 1800 # choose a remote branch already tracked if possible
1795 1801 remote = branches[0]
1796 1802 if remote not in tracking:
1797 1803 for b in branches:
1798 1804 if b in tracking:
1799 1805 remote = b
1800 1806 break
1801 1807
1802 1808 if remote not in tracking:
1803 1809 # create a new local tracking branch
1804 1810 local = remote.split('/', 3)[3]
1805 1811 checkout(['-b', local, remote])
1806 1812 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1807 1813 # When updating to a tracked remote branch,
1808 1814 # if the local tracking branch is downstream of it,
1809 1815 # a normal `git pull` would have performed a "fast-forward merge"
1810 1816 # which is equivalent to updating the local branch to the remote.
1811 1817 # Since we are only looking at branching at update, we need to
1812 1818 # detect this situation and perform this action lazily.
1813 1819 if tracking[remote] != self._gitcurrentbranch():
1814 1820 checkout([tracking[remote]])
1815 1821 self._gitcommand(['merge', '--ff', remote])
1816 1822 _sanitize(self.ui, self.wvfs, '.git')
1817 1823 else:
1818 1824 # a real merge would be required, just checkout the revision
1819 1825 rawcheckout()
1820 1826
1821 1827 @annotatesubrepoerror
1822 1828 def commit(self, text, user, date):
1823 1829 if self._gitmissing():
1824 1830 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1825 1831 cmd = ['commit', '-a', '-m', text]
1826 1832 env = encoding.environ.copy()
1827 1833 if user:
1828 1834 cmd += ['--author', user]
1829 1835 if date:
1830 1836 # git's date parser silently ignores when seconds < 1e9
1831 1837 # convert to ISO8601
1832 1838 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1833 1839 '%Y-%m-%dT%H:%M:%S %1%2')
1834 1840 self._gitcommand(cmd, env=env)
1835 1841 # make sure commit works otherwise HEAD might not exist under certain
1836 1842 # circumstances
1837 1843 return self._gitstate()
1838 1844
1839 1845 @annotatesubrepoerror
1840 1846 def merge(self, state):
1841 1847 source, revision, kind = state
1842 1848 self._fetch(source, revision)
1843 1849 base = self._gitcommand(['merge-base', revision, self._state[1]])
1844 1850 self._gitupdatestat()
1845 1851 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1846 1852
1847 1853 def mergefunc():
1848 1854 if base == revision:
1849 1855 self.get(state) # fast forward merge
1850 1856 elif base != self._state[1]:
1851 1857 self._gitcommand(['merge', '--no-commit', revision])
1852 1858 _sanitize(self.ui, self.wvfs, '.git')
1853 1859
1854 1860 if self.dirty():
1855 1861 if self._gitstate() != revision:
1856 1862 dirty = self._gitstate() == self._state[1] or code != 0
1857 1863 if _updateprompt(self.ui, self, dirty,
1858 1864 self._state[1][:7], revision[:7]):
1859 1865 mergefunc()
1860 1866 else:
1861 1867 mergefunc()
1862 1868
1863 1869 @annotatesubrepoerror
1864 1870 def push(self, opts):
1865 1871 force = opts.get('force')
1866 1872
1867 1873 if not self._state[1]:
1868 1874 return True
1869 1875 if self._gitmissing():
1870 1876 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1871 1877 # if a branch in origin contains the revision, nothing to do
1872 1878 branch2rev, rev2branch = self._gitbranchmap()
1873 1879 if self._state[1] in rev2branch:
1874 1880 for b in rev2branch[self._state[1]]:
1875 1881 if b.startswith('refs/remotes/origin/'):
1876 1882 return True
1877 1883 for b, revision in branch2rev.iteritems():
1878 1884 if b.startswith('refs/remotes/origin/'):
1879 1885 if self._gitisancestor(self._state[1], revision):
1880 1886 return True
1881 1887 # otherwise, try to push the currently checked out branch
1882 1888 cmd = ['push']
1883 1889 if force:
1884 1890 cmd.append('--force')
1885 1891
1886 1892 current = self._gitcurrentbranch()
1887 1893 if current:
1888 1894 # determine if the current branch is even useful
1889 1895 if not self._gitisancestor(self._state[1], current):
1890 1896 self.ui.warn(_('unrelated git branch checked out '
1891 1897 'in subrepository "%s"\n') % self._relpath)
1892 1898 return False
1893 1899 self.ui.status(_('pushing branch %s of subrepository "%s"\n') %
1894 1900 (current.split('/', 2)[2], self._relpath))
1895 1901 ret = self._gitdir(cmd + ['origin', current])
1896 1902 return ret[1] == 0
1897 1903 else:
1898 1904 self.ui.warn(_('no branch checked out in subrepository "%s"\n'
1899 1905 'cannot push revision %s\n') %
1900 1906 (self._relpath, self._state[1]))
1901 1907 return False
1902 1908
1903 1909 @annotatesubrepoerror
1904 1910 def add(self, ui, match, prefix, explicitonly, **opts):
1905 1911 if self._gitmissing():
1906 1912 return []
1907 1913
1908 1914 (modified, added, removed,
1909 1915 deleted, unknown, ignored, clean) = self.status(None, unknown=True,
1910 1916 clean=True)
1911 1917
1912 1918 tracked = set()
1913 1919 # dirstates 'amn' warn, 'r' is added again
1914 1920 for l in (modified, added, deleted, clean):
1915 1921 tracked.update(l)
1916 1922
1917 1923 # Unknown files not of interest will be rejected by the matcher
1918 1924 files = unknown
1919 1925 files.extend(match.files())
1920 1926
1921 1927 rejected = []
1922 1928
1923 1929 files = [f for f in sorted(set(files)) if match(f)]
1924 1930 for f in files:
1925 1931 exact = match.exact(f)
1926 1932 command = ["add"]
1927 1933 if exact:
1928 1934 command.append("-f") #should be added, even if ignored
1929 1935 if ui.verbose or not exact:
1930 1936 ui.status(_('adding %s\n') % match.rel(f))
1931 1937
1932 1938 if f in tracked: # hg prints 'adding' even if already tracked
1933 1939 if exact:
1934 1940 rejected.append(f)
1935 1941 continue
1936 1942 if not opts.get(r'dry_run'):
1937 1943 self._gitcommand(command + [f])
1938 1944
1939 1945 for f in rejected:
1940 1946 ui.warn(_("%s already tracked!\n") % match.abs(f))
1941 1947
1942 1948 return rejected
1943 1949
1944 1950 @annotatesubrepoerror
1945 1951 def remove(self):
1946 1952 if self._gitmissing():
1947 1953 return
1948 1954 if self.dirty():
1949 1955 self.ui.warn(_('not removing repo %s because '
1950 1956 'it has changes.\n') % self._relpath)
1951 1957 return
1952 1958 # we can't fully delete the repository as it may contain
1953 1959 # local-only history
1954 1960 self.ui.note(_('removing subrepo %s\n') % self._relpath)
1955 1961 self._gitcommand(['config', 'core.bare', 'true'])
1956 1962 for f, kind in self.wvfs.readdir():
1957 1963 if f == '.git':
1958 1964 continue
1959 1965 if kind == stat.S_IFDIR:
1960 1966 self.wvfs.rmtree(f)
1961 1967 else:
1962 1968 self.wvfs.unlink(f)
1963 1969
1964 1970 def archive(self, archiver, prefix, match=None, decode=True):
1965 1971 total = 0
1966 1972 source, revision = self._state
1967 1973 if not revision:
1968 1974 return total
1969 1975 self._fetch(source, revision)
1970 1976
1971 1977 # Parse git's native archive command.
1972 1978 # This should be much faster than manually traversing the trees
1973 1979 # and objects with many subprocess calls.
1974 1980 tarstream = self._gitcommand(['archive', revision], stream=True)
1975 1981 tar = tarfile.open(fileobj=tarstream, mode='r|')
1976 1982 relpath = subrelpath(self)
1977 1983 self.ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1978 1984 for i, info in enumerate(tar):
1979 1985 if info.isdir():
1980 1986 continue
1981 1987 if match and not match(info.name):
1982 1988 continue
1983 1989 if info.issym():
1984 1990 data = info.linkname
1985 1991 else:
1986 1992 data = tar.extractfile(info).read()
1987 1993 archiver.addfile(prefix + self._path + '/' + info.name,
1988 1994 info.mode, info.issym(), data)
1989 1995 total += 1
1990 1996 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
1991 1997 unit=_('files'))
1992 1998 self.ui.progress(_('archiving (%s)') % relpath, None)
1993 1999 return total
1994 2000
1995 2001
1996 2002 @annotatesubrepoerror
1997 2003 def cat(self, match, fm, fntemplate, prefix, **opts):
1998 2004 rev = self._state[1]
1999 2005 if match.anypats():
2000 2006 return 1 #No support for include/exclude yet
2001 2007
2002 2008 if not match.files():
2003 2009 return 1
2004 2010
2005 2011 # TODO: add support for non-plain formatter (see cmdutil.cat())
2006 2012 for f in match.files():
2007 2013 output = self._gitcommand(["show", "%s:%s" % (rev, f)])
2008 2014 fp = cmdutil.makefileobj(self._subparent, fntemplate,
2009 2015 self._ctx.node(),
2010 2016 pathname=self.wvfs.reljoin(prefix, f))
2011 2017 fp.write(output)
2012 2018 fp.close()
2013 2019 return 0
2014 2020
2015 2021
2016 2022 @annotatesubrepoerror
2017 2023 def status(self, rev2, **opts):
2018 2024 rev1 = self._state[1]
2019 2025 if self._gitmissing() or not rev1:
2020 2026 # if the repo is missing, return no results
2021 2027 return scmutil.status([], [], [], [], [], [], [])
2022 2028 modified, added, removed = [], [], []
2023 2029 self._gitupdatestat()
2024 2030 if rev2:
2025 2031 command = ['diff-tree', '--no-renames', '-r', rev1, rev2]
2026 2032 else:
2027 2033 command = ['diff-index', '--no-renames', rev1]
2028 2034 out = self._gitcommand(command)
2029 2035 for line in out.split('\n'):
2030 2036 tab = line.find('\t')
2031 2037 if tab == -1:
2032 2038 continue
2033 2039 status, f = line[tab - 1], line[tab + 1:]
2034 2040 if status == 'M':
2035 2041 modified.append(f)
2036 2042 elif status == 'A':
2037 2043 added.append(f)
2038 2044 elif status == 'D':
2039 2045 removed.append(f)
2040 2046
2041 2047 deleted, unknown, ignored, clean = [], [], [], []
2042 2048
2043 2049 command = ['status', '--porcelain', '-z']
2044 2050 if opts.get(r'unknown'):
2045 2051 command += ['--untracked-files=all']
2046 2052 if opts.get(r'ignored'):
2047 2053 command += ['--ignored']
2048 2054 out = self._gitcommand(command)
2049 2055
2050 2056 changedfiles = set()
2051 2057 changedfiles.update(modified)
2052 2058 changedfiles.update(added)
2053 2059 changedfiles.update(removed)
2054 2060 for line in out.split('\0'):
2055 2061 if not line:
2056 2062 continue
2057 2063 st = line[0:2]
2058 2064 #moves and copies show 2 files on one line
2059 2065 if line.find('\0') >= 0:
2060 2066 filename1, filename2 = line[3:].split('\0')
2061 2067 else:
2062 2068 filename1 = line[3:]
2063 2069 filename2 = None
2064 2070
2065 2071 changedfiles.add(filename1)
2066 2072 if filename2:
2067 2073 changedfiles.add(filename2)
2068 2074
2069 2075 if st == '??':
2070 2076 unknown.append(filename1)
2071 2077 elif st == '!!':
2072 2078 ignored.append(filename1)
2073 2079
2074 2080 if opts.get(r'clean'):
2075 2081 out = self._gitcommand(['ls-files'])
2076 2082 for f in out.split('\n'):
2077 2083 if not f in changedfiles:
2078 2084 clean.append(f)
2079 2085
2080 2086 return scmutil.status(modified, added, removed, deleted,
2081 2087 unknown, ignored, clean)
2082 2088
2083 2089 @annotatesubrepoerror
2084 2090 def diff(self, ui, diffopts, node2, match, prefix, **opts):
2085 2091 node1 = self._state[1]
2086 2092 cmd = ['diff', '--no-renames']
2087 2093 if opts[r'stat']:
2088 2094 cmd.append('--stat')
2089 2095 else:
2090 2096 # for Git, this also implies '-p'
2091 2097 cmd.append('-U%d' % diffopts.context)
2092 2098
2093 2099 gitprefix = self.wvfs.reljoin(prefix, self._path)
2094 2100
2095 2101 if diffopts.noprefix:
2096 2102 cmd.extend(['--src-prefix=%s/' % gitprefix,
2097 2103 '--dst-prefix=%s/' % gitprefix])
2098 2104 else:
2099 2105 cmd.extend(['--src-prefix=a/%s/' % gitprefix,
2100 2106 '--dst-prefix=b/%s/' % gitprefix])
2101 2107
2102 2108 if diffopts.ignorews:
2103 2109 cmd.append('--ignore-all-space')
2104 2110 if diffopts.ignorewsamount:
2105 2111 cmd.append('--ignore-space-change')
2106 2112 if self._gitversion(self._gitcommand(['--version'])) >= (1, 8, 4) \
2107 2113 and diffopts.ignoreblanklines:
2108 2114 cmd.append('--ignore-blank-lines')
2109 2115
2110 2116 cmd.append(node1)
2111 2117 if node2:
2112 2118 cmd.append(node2)
2113 2119
2114 2120 output = ""
2115 2121 if match.always():
2116 2122 output += self._gitcommand(cmd) + '\n'
2117 2123 else:
2118 2124 st = self.status(node2)[:3]
2119 2125 files = [f for sublist in st for f in sublist]
2120 2126 for f in files:
2121 2127 if match(f):
2122 2128 output += self._gitcommand(cmd + ['--', f]) + '\n'
2123 2129
2124 2130 if output.strip():
2125 2131 ui.write(output)
2126 2132
2127 2133 @annotatesubrepoerror
2128 2134 def revert(self, substate, *pats, **opts):
2129 2135 self.ui.status(_('reverting subrepo %s\n') % substate[0])
2130 2136 if not opts.get(r'no_backup'):
2131 2137 status = self.status(None)
2132 2138 names = status.modified
2133 2139 for name in names:
2134 2140 bakname = scmutil.origpath(self.ui, self._subparent, name)
2135 2141 self.ui.note(_('saving current version of %s as %s\n') %
2136 2142 (name, bakname))
2137 2143 self.wvfs.rename(name, bakname)
2138 2144
2139 2145 if not opts.get(r'dry_run'):
2140 2146 self.get(substate, overwrite=True)
2141 2147 return []
2142 2148
2143 2149 def shortid(self, revid):
2144 2150 return revid[:7]
2145 2151
2146 2152 types = {
2147 2153 'hg': hgsubrepo,
2148 2154 'svn': svnsubrepo,
2149 2155 'git': gitsubrepo,
2150 2156 }
@@ -1,665 +1,682 b''
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 295 BUG: The remote subrepo should be cloned to the local pool, and then shared
296 296 from there.
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 sharing subrepo foo from http://localhost:$HGPORT/foo
311 abort: can only share local repositories (in subrepository "foo")
312 [255]
310 cloning subrepo foo from http://localhost:$HGPORT/foo
311 requesting all changes
312 adding changesets
313 adding manifests
314 adding file changes
315 added 4 changesets with 7 changes to 3 files
316 new changesets af048e97ade2:65903cebad86
317 cloning subrepo foo/bar from http://localhost:$HGPORT/foo/bar
318 requesting all changes
319 adding changesets
320 adding manifests
321 adding file changes
322 added 3 changesets with 3 changes to 1 files
323 new changesets 4904098473f9:31ecbdafd357
324 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
313 325
314 326 $ cat access.log
315 327 * "GET /?cmd=capabilities HTTP/1.1" 200 - (glob)
316 328 * "GET /?cmd=batch HTTP/1.1" 200 - * (glob)
317 329 * "GET /?cmd=getbundle HTTP/1.1" 200 - * (glob)
318 330 * "GET /foo?cmd=capabilities HTTP/1.1" 200 - (glob)
319 331 * "GET /foo?cmd=batch HTTP/1.1" 200 - * (glob)
320 332 * "GET /foo?cmd=getbundle HTTP/1.1" 200 - * (glob)
321 333 * "GET /foo/bar?cmd=capabilities HTTP/1.1" 200 - (glob)
322 334 * "GET /foo/bar?cmd=batch HTTP/1.1" 200 - * (glob)
323 335 * "GET /foo/bar?cmd=getbundle HTTP/1.1" 200 - * (glob)
324 336 $LOCALIP - - [$LOGDATE$] "GET /?cmd=capabilities HTTP/1.1" 200 - (glob)
325 337 $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)
326 338 $LOCALIP - - [$LOGDATE$] "GET /?cmd=capabilities HTTP/1.1" 200 - (glob)
327 339 $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)
328 340 $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)
329 341 $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)
330 342 $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)
331 343 $LOCALIP - - [$LOGDATE$] "GET /foo?cmd=capabilities HTTP/1.1" 200 - (glob)
344 $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 $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)
346 $LOCALIP - - [$LOGDATE$] "GET /foo/bar?cmd=capabilities HTTP/1.1" 200 - (glob)
347 $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 $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)
332 349
333 350 $ killdaemons.py
334 351 $ rm hg1.pid error.log access.log
335 352 $ cd repo
336 353 #endif
337 354
338 355 Enable progress extension for archive tests:
339 356
340 357 $ cp $HGRCPATH $HGRCPATH.no-progress
341 358 $ cat >> $HGRCPATH <<EOF
342 359 > [progress]
343 360 > disable=False
344 361 > assume-tty = 1
345 362 > delay = 0
346 363 > # set changedelay really large so we don't see nested topics
347 364 > changedelay = 30000
348 365 > format = topic bar number
349 366 > refresh = 0
350 367 > width = 60
351 368 > EOF
352 369
353 370 Test archiving to a directory tree (the doubled lines in the output
354 371 only show up in the test output, not in real usage):
355 372
356 373 $ hg archive --subrepos ../archive
357 374 \r (no-eol) (esc)
358 375 archiving [ ] 0/3\r (no-eol) (esc)
359 376 archiving [=============> ] 1/3\r (no-eol) (esc)
360 377 archiving [===========================> ] 2/3\r (no-eol) (esc)
361 378 archiving [==========================================>] 3/3\r (no-eol) (esc)
362 379 \r (no-eol) (esc)
363 380 \r (no-eol) (esc)
364 381 archiving (foo) [ ] 0/3\r (no-eol) (esc)
365 382 archiving (foo) [===========> ] 1/3\r (no-eol) (esc)
366 383 archiving (foo) [=======================> ] 2/3\r (no-eol) (esc)
367 384 archiving (foo) [====================================>] 3/3\r (no-eol) (esc)
368 385 \r (no-eol) (esc)
369 386 \r (no-eol) (esc)
370 387 archiving (foo/bar) [ ] 0/1\r (no-eol) (esc)
371 388 archiving (foo/bar) [================================>] 1/1\r (no-eol) (esc)
372 389 \r (no-eol) (esc)
373 390 $ find ../archive | sort
374 391 ../archive
375 392 ../archive/.hg_archival.txt
376 393 ../archive/.hgsub
377 394 ../archive/.hgsubstate
378 395 ../archive/foo
379 396 ../archive/foo/.hgsub
380 397 ../archive/foo/.hgsubstate
381 398 ../archive/foo/bar
382 399 ../archive/foo/bar/z.txt
383 400 ../archive/foo/y.txt
384 401 ../archive/x.txt
385 402
386 403 Test archiving to zip file (unzip output is unstable):
387 404
388 405 $ hg archive --subrepos --prefix '.' ../archive.zip
389 406 \r (no-eol) (esc)
390 407 archiving [ ] 0/3\r (no-eol) (esc)
391 408 archiving [=============> ] 1/3\r (no-eol) (esc)
392 409 archiving [===========================> ] 2/3\r (no-eol) (esc)
393 410 archiving [==========================================>] 3/3\r (no-eol) (esc)
394 411 \r (no-eol) (esc)
395 412 \r (no-eol) (esc)
396 413 archiving (foo) [ ] 0/3\r (no-eol) (esc)
397 414 archiving (foo) [===========> ] 1/3\r (no-eol) (esc)
398 415 archiving (foo) [=======================> ] 2/3\r (no-eol) (esc)
399 416 archiving (foo) [====================================>] 3/3\r (no-eol) (esc)
400 417 \r (no-eol) (esc)
401 418 \r (no-eol) (esc)
402 419 archiving (foo/bar) [ ] 0/1\r (no-eol) (esc)
403 420 archiving (foo/bar) [================================>] 1/1\r (no-eol) (esc)
404 421 \r (no-eol) (esc)
405 422
406 423 (unzip date formating is unstable, we do not care about it and glob it out)
407 424
408 425 $ unzip -l ../archive.zip | grep -v -- ----- | egrep -v files$
409 426 Archive: ../archive.zip
410 427 Length [ ]* Date [ ]* Time [ ]* Name (re)
411 428 172 [0-9:\- ]* .hg_archival.txt (re)
412 429 10 [0-9:\- ]* .hgsub (re)
413 430 45 [0-9:\- ]* .hgsubstate (re)
414 431 3 [0-9:\- ]* x.txt (re)
415 432 10 [0-9:\- ]* foo/.hgsub (re)
416 433 45 [0-9:\- ]* foo/.hgsubstate (re)
417 434 9 [0-9:\- ]* foo/y.txt (re)
418 435 9 [0-9:\- ]* foo/bar/z.txt (re)
419 436
420 437 Test archiving a revision that references a subrepo that is not yet
421 438 cloned:
422 439
423 440 #if hardlink
424 441 $ hg clone -U . ../empty
425 442 \r (no-eol) (esc)
426 443 linking [ <=> ] 1\r (no-eol) (esc)
427 444 linking [ <=> ] 2\r (no-eol) (esc)
428 445 linking [ <=> ] 3\r (no-eol) (esc)
429 446 linking [ <=> ] 4\r (no-eol) (esc)
430 447 linking [ <=> ] 5\r (no-eol) (esc)
431 448 linking [ <=> ] 6\r (no-eol) (esc)
432 449 linking [ <=> ] 7\r (no-eol) (esc)
433 450 linking [ <=> ] 8\r (no-eol) (esc)
434 451 \r (no-eol) (esc)
435 452 #else
436 453 $ hg clone -U . ../empty
437 454 \r (no-eol) (esc)
438 455 linking [ <=> ] 1 (no-eol)
439 456 #endif
440 457
441 458 $ cd ../empty
442 459 #if hardlink
443 460 $ hg archive --subrepos -r tip --prefix './' ../archive.tar.gz
444 461 \r (no-eol) (esc)
445 462 archiving [ ] 0/3\r (no-eol) (esc)
446 463 archiving [=============> ] 1/3\r (no-eol) (esc)
447 464 archiving [===========================> ] 2/3\r (no-eol) (esc)
448 465 archiving [==========================================>] 3/3\r (no-eol) (esc)
449 466 \r (no-eol) (esc)
450 467 \r (no-eol) (esc)
451 468 linking [ <=> ] 1\r (no-eol) (esc)
452 469 linking [ <=> ] 2\r (no-eol) (esc)
453 470 linking [ <=> ] 3\r (no-eol) (esc)
454 471 linking [ <=> ] 4\r (no-eol) (esc)
455 472 linking [ <=> ] 5\r (no-eol) (esc)
456 473 linking [ <=> ] 6\r (no-eol) (esc)
457 474 linking [ <=> ] 7\r (no-eol) (esc)
458 475 linking [ <=> ] 8\r (no-eol) (esc)
459 476 \r (no-eol) (esc)
460 477 \r (no-eol) (esc)
461 478 archiving (foo) [ ] 0/3\r (no-eol) (esc)
462 479 archiving (foo) [===========> ] 1/3\r (no-eol) (esc)
463 480 archiving (foo) [=======================> ] 2/3\r (no-eol) (esc)
464 481 archiving (foo) [====================================>] 3/3\r (no-eol) (esc)
465 482 \r (no-eol) (esc)
466 483 \r (no-eol) (esc)
467 484 linking [ <=> ] 1\r (no-eol) (esc)
468 485 linking [ <=> ] 2\r (no-eol) (esc)
469 486 linking [ <=> ] 3\r (no-eol) (esc)
470 487 linking [ <=> ] 4\r (no-eol) (esc)
471 488 linking [ <=> ] 5\r (no-eol) (esc)
472 489 linking [ <=> ] 6\r (no-eol) (esc)
473 490 \r (no-eol) (esc)
474 491 \r (no-eol) (esc)
475 492 archiving (foo/bar) [ ] 0/1\r (no-eol) (esc)
476 493 archiving (foo/bar) [================================>] 1/1\r (no-eol) (esc)
477 494 \r (no-eol) (esc)
478 495 cloning subrepo foo from $TESTTMP/repo/foo
479 496 cloning subrepo foo/bar from $TESTTMP/repo/foo/bar
480 497 #else
481 498 Note there's a slight output glitch on non-hardlink systems: the last
482 499 "linking" progress topic never gets closed, leading to slight output corruption on that platform.
483 500 $ hg archive --subrepos -r tip --prefix './' ../archive.tar.gz
484 501 \r (no-eol) (esc)
485 502 archiving [ ] 0/3\r (no-eol) (esc)
486 503 archiving [=============> ] 1/3\r (no-eol) (esc)
487 504 archiving [===========================> ] 2/3\r (no-eol) (esc)
488 505 archiving [==========================================>] 3/3\r (no-eol) (esc)
489 506 \r (no-eol) (esc)
490 507 \r (no-eol) (esc)
491 508 linking [ <=> ] 1\r (no-eol) (esc)
492 509 cloning subrepo foo/bar from $TESTTMP/repo/foo/bar
493 510 #endif
494 511
495 512 Archive + subrepos uses '/' for all component separators
496 513
497 514 $ tar -tzf ../archive.tar.gz | sort
498 515 .hg_archival.txt
499 516 .hgsub
500 517 .hgsubstate
501 518 foo/.hgsub
502 519 foo/.hgsubstate
503 520 foo/bar/z.txt
504 521 foo/y.txt
505 522 x.txt
506 523
507 524 The newly cloned subrepos contain no working copy:
508 525
509 526 $ hg -R foo summary
510 527 parent: -1:000000000000 (no revision checked out)
511 528 branch: default
512 529 commit: (clean)
513 530 update: 4 new changesets (update)
514 531
515 532 Sharing a local repo without the locally referenced subrepo (i.e. it was never
516 533 updated from null), fails the same as a clone operation.
517 534
518 535 $ hg --config progress.disable=True clone -U ../empty ../empty2
519 536
520 537 $ hg --config extensions.share= --config progress.disable=True \
521 538 > share ../empty2 ../empty_share
522 539 updating working directory
523 540 abort: repository $TESTTMP/empty2/foo not found!
524 541 [255]
525 542
526 543 $ hg --config progress.disable=True clone ../empty2 ../empty_clone
527 544 updating to branch default
528 545 abort: repository $TESTTMP/empty2/foo not found!
529 546 [255]
530 547
531 548 Disable progress extension and cleanup:
532 549
533 550 $ mv $HGRCPATH.no-progress $HGRCPATH
534 551
535 552 Test archiving when there is a directory in the way for a subrepo
536 553 created by archive:
537 554
538 555 $ hg clone -U . ../almost-empty
539 556 $ cd ../almost-empty
540 557 $ mkdir foo
541 558 $ echo f > foo/f
542 559 $ hg archive --subrepos -r tip archive
543 560 cloning subrepo foo from $TESTTMP/empty/foo
544 561 abort: destination '$TESTTMP/almost-empty/foo' is not empty (in subrepository "foo")
545 562 [255]
546 563
547 564 Clone and test outgoing:
548 565
549 566 $ cd ..
550 567 $ hg clone repo repo2
551 568 updating to branch default
552 569 cloning subrepo foo from $TESTTMP/repo/foo
553 570 cloning subrepo foo/bar from $TESTTMP/repo/foo/bar
554 571 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
555 572 $ cd repo2
556 573 $ hg outgoing -S
557 574 comparing with $TESTTMP/repo
558 575 searching for changes
559 576 no changes found
560 577 comparing with $TESTTMP/repo/foo
561 578 searching for changes
562 579 no changes found
563 580 comparing with $TESTTMP/repo/foo/bar
564 581 searching for changes
565 582 no changes found
566 583 [1]
567 584
568 585 Make nested change:
569 586
570 587 $ echo y4 >> foo/y.txt
571 588 $ hg diff --nodates -S
572 589 diff -r 65903cebad86 foo/y.txt
573 590 --- a/foo/y.txt
574 591 +++ b/foo/y.txt
575 592 @@ -1,3 +1,4 @@
576 593 y1
577 594 y2
578 595 y3
579 596 +y4
580 597 $ hg commit --subrepos -m 3-4-2
581 598 committing subrepository foo
582 599 $ hg outgoing -S
583 600 comparing with $TESTTMP/repo
584 601 searching for changes
585 602 changeset: 3:2655b8ecc4ee
586 603 tag: tip
587 604 user: test
588 605 date: Thu Jan 01 00:00:00 1970 +0000
589 606 summary: 3-4-2
590 607
591 608 comparing with $TESTTMP/repo/foo
592 609 searching for changes
593 610 changeset: 4:e96193d6cb36
594 611 tag: tip
595 612 user: test
596 613 date: Thu Jan 01 00:00:00 1970 +0000
597 614 summary: 3-4-2
598 615
599 616 comparing with $TESTTMP/repo/foo/bar
600 617 searching for changes
601 618 no changes found
602 619
603 620
604 621 Switch to original repo and setup default path:
605 622
606 623 $ cd ../repo
607 624 $ echo '[paths]' >> .hg/hgrc
608 625 $ echo 'default = ../repo2' >> .hg/hgrc
609 626
610 627 Test incoming:
611 628
612 629 $ hg incoming -S
613 630 comparing with $TESTTMP/repo2
614 631 searching for changes
615 632 changeset: 3:2655b8ecc4ee
616 633 tag: tip
617 634 user: test
618 635 date: Thu Jan 01 00:00:00 1970 +0000
619 636 summary: 3-4-2
620 637
621 638 comparing with $TESTTMP/repo2/foo
622 639 searching for changes
623 640 changeset: 4:e96193d6cb36
624 641 tag: tip
625 642 user: test
626 643 date: Thu Jan 01 00:00:00 1970 +0000
627 644 summary: 3-4-2
628 645
629 646 comparing with $TESTTMP/repo2/foo/bar
630 647 searching for changes
631 648 no changes found
632 649
633 650 $ hg incoming -S --bundle incoming.hg
634 651 abort: cannot combine --bundle and --subrepos
635 652 [255]
636 653
637 654 Test missing subrepo:
638 655
639 656 $ rm -r foo
640 657 $ hg status -S
641 658 warning: error "unknown revision '65903cebad86f1a84bd4f1134f62fa7dcb7a1c98'" in subrepository "foo"
642 659
643 660 Issue2619: IndexError: list index out of range on hg add with subrepos
644 661 The subrepo must sorts after the explicit filename.
645 662
646 663 $ cd ..
647 664 $ hg init test
648 665 $ cd test
649 666 $ hg init x
650 667 $ echo abc > abc.txt
651 668 $ hg ci -Am "abc"
652 669 adding abc.txt
653 670 $ echo "x = x" >> .hgsub
654 671 $ hg add .hgsub
655 672 $ touch a x/a
656 673 $ hg add a x/a
657 674
658 675 $ hg ci -Sm "added x"
659 676 committing subrepository x
660 677 $ echo abc > x/a
661 678 $ hg revert --rev '.^' "set:subrepo('glob:x*')"
662 679 abort: subrepository 'x' does not exist in 25ac2c9b3180!
663 680 [255]
664 681
665 682 $ cd ..
@@ -1,162 +1,187 b''
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 BUG: Remote subrepos cannot be shared, and pooled repos don't have their
88 relative subrepos in the relative location stated in .hgsub.
87 Clone pooling works for local clones with a remote subrepo reference.
88
89 BUG: subrepos should be shared out of the pool.
89 90
90 91 $ hg --config extensions.share= --config share.pool=$TESTTMP/pool \
91 92 > clone absolute_subrepo cloned_from_abs
92 93 (sharing from new pooled repository 8d6a2f1e993b34b6557de0042cfe825ae12a8dae)
93 94 requesting all changes
94 95 adding changesets
95 96 adding manifests
96 97 adding file changes
97 98 added 2 changesets with 3 changes to 3 files
98 99 new changesets 8d6a2f1e993b:c6d0e6ebd1c9
99 100 searching for changes
100 101 no changes found
101 102 updating working directory
102 sharing subrepo sub from http://localhost:$HGPORT/sub
103 abort: can only share local repositories (in subrepository "sub")
104 [255]
103 cloning subrepo sub from http://localhost:$HGPORT/sub
104 requesting all changes
105 adding changesets
106 adding manifests
107 adding file changes
108 added 1 changesets with 1 changes to 1 files
109 new changesets 863c1745b441
110 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
111
112 Vanilla sharing with a subrepo remote path reference will clone the subrepo.
113 Each share of these top level repos will end up with independent subrepo copies
114 (potentially leaving the shared parent with dangling cset references).
105 115
106 116 $ hg --config extensions.share= share absolute_subrepo shared_from_abs
107 117 updating working directory
108 sharing subrepo sub from http://localhost:$HGPORT/sub
109 abort: can only share local repositories (in subrepository "sub")
110 [255]
118 cloning subrepo sub from http://localhost:$HGPORT/sub
119 requesting all changes
120 adding changesets
121 adding manifests
122 adding file changes
123 added 1 changesets with 1 changes to 1 files
124 new changesets 863c1745b441
125 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
111 126
112 127 $ hg --config extensions.share= share -U absolute_subrepo shared_from_abs2
113 128 $ hg -R shared_from_abs2 update -r tip
114 sharing subrepo sub from http://localhost:$HGPORT/sub
115 abort: can only share local repositories (in subrepository "sub")
116 [255]
129 cloning subrepo sub from http://localhost:$HGPORT/sub
130 requesting all changes
131 adding changesets
132 adding manifests
133 adding file changes
134 added 1 changesets with 1 changes to 1 files
135 new changesets 863c1745b441
136 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
117 137
118 BUG: A repo without its subrepo available locally should be sharable if the
138 A parent repo without its subrepo available locally can be shared if the
119 139 subrepo is referenced by absolute path.
120 140
121 141 $ hg clone -U absolute_subrepo cloned_null_from_abs
122 142 $ hg --config extensions.share= share cloned_null_from_abs shared_from_null_abs
123 143 updating working directory
124 sharing subrepo sub from http://localhost:$HGPORT/sub
125 abort: can only share local repositories (in subrepository "sub")
126 [255]
144 cloning subrepo sub from http://localhost:$HGPORT/sub
145 requesting all changes
146 adding changesets
147 adding manifests
148 adding file changes
149 added 1 changesets with 1 changes to 1 files
150 new changesets 863c1745b441
151 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
127 152
128 153 $ killdaemons.py
129 154
130 155 subrepo paths with ssh urls
131 156
132 157 $ hg clone -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/cloned sshclone
133 158 requesting all changes
134 159 adding changesets
135 160 adding manifests
136 161 adding file changes
137 162 added 1 changesets with 3 changes to 3 files
138 163 new changesets fdfeeb3e979e
139 164 updating to branch default
140 165 cloning subrepo sub from ssh://user@dummy/sub
141 166 requesting all changes
142 167 adding changesets
143 168 adding manifests
144 169 adding file changes
145 170 added 1 changesets with 1 changes to 1 files
146 171 new changesets 863c1745b441
147 172 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
148 173
149 174 $ hg -R sshclone push -e "\"$PYTHON\" \"$TESTDIR/dummyssh\"" ssh://user@dummy/`pwd`/cloned
150 175 pushing to ssh://user@dummy/$TESTTMP/cloned
151 176 pushing subrepo sub to ssh://user@dummy/$TESTTMP/sub
152 177 searching for changes
153 178 no changes found
154 179 searching for changes
155 180 no changes found
156 181 [1]
157 182
158 183 $ cat dummylog
159 184 Got arguments 1:user@dummy 2:hg -R cloned serve --stdio
160 185 Got arguments 1:user@dummy 2:hg -R sub serve --stdio
161 186 Got arguments 1:user@dummy 2:hg -R $TESTTMP/cloned serve --stdio
162 187 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