##// END OF EJS Templates
subrepo: don't crash when git .hgsubstate is empty (issue2716)
Eric Eisner -
r14469:2fdea636 stable
parent child Browse files
Show More
@@ -1,1047 +1,1054 b''
1 1 # subrepo.py - sub-repository handling for Mercurial
2 2 #
3 3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 import errno, os, re, xml.dom.minidom, shutil, urlparse, posixpath
9 9 import stat, subprocess, tarfile
10 10 from i18n import _
11 11 import config, util, node, error, cmdutil
12 12 hg = None
13 13 propertycache = util.propertycache
14 14
15 15 nullstate = ('', '', 'empty')
16 16
17 17 def state(ctx, ui):
18 18 """return a state dict, mapping subrepo paths configured in .hgsub
19 19 to tuple: (source from .hgsub, revision from .hgsubstate, kind
20 20 (key in types dict))
21 21 """
22 22 p = config.config()
23 23 def read(f, sections=None, remap=None):
24 24 if f in ctx:
25 25 try:
26 26 data = ctx[f].data()
27 27 except IOError, err:
28 28 if err.errno != errno.ENOENT:
29 29 raise
30 30 # handle missing subrepo spec files as removed
31 31 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
32 32 return
33 33 p.parse(f, data, sections, remap, read)
34 34 else:
35 35 raise util.Abort(_("subrepo spec file %s not found") % f)
36 36
37 37 if '.hgsub' in ctx:
38 38 read('.hgsub')
39 39
40 40 for path, src in ui.configitems('subpaths'):
41 41 p.set('subpaths', path, src, ui.configsource('subpaths', path))
42 42
43 43 rev = {}
44 44 if '.hgsubstate' in ctx:
45 45 try:
46 46 for l in ctx['.hgsubstate'].data().splitlines():
47 47 revision, path = l.split(" ", 1)
48 48 rev[path] = revision
49 49 except IOError, err:
50 50 if err.errno != errno.ENOENT:
51 51 raise
52 52
53 53 state = {}
54 54 for path, src in p[''].items():
55 55 kind = 'hg'
56 56 if src.startswith('['):
57 57 if ']' not in src:
58 58 raise util.Abort(_('missing ] in subrepo source'))
59 59 kind, src = src.split(']', 1)
60 60 kind = kind[1:]
61 61
62 62 for pattern, repl in p.items('subpaths'):
63 63 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
64 64 # does a string decode.
65 65 repl = repl.encode('string-escape')
66 66 # However, we still want to allow back references to go
67 67 # through unharmed, so we turn r'\\1' into r'\1'. Again,
68 68 # extra escapes are needed because re.sub string decodes.
69 69 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
70 70 try:
71 71 src = re.sub(pattern, repl, src, 1)
72 72 except re.error, e:
73 73 raise util.Abort(_("bad subrepository pattern in %s: %s")
74 74 % (p.source('subpaths', pattern), e))
75 75
76 76 state[path] = (src.strip(), rev.get(path, ''), kind)
77 77
78 78 return state
79 79
80 80 def writestate(repo, state):
81 81 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
82 82 repo.wwrite('.hgsubstate',
83 83 ''.join(['%s %s\n' % (state[s][1], s)
84 84 for s in sorted(state)]), '')
85 85
86 86 def submerge(repo, wctx, mctx, actx, overwrite):
87 87 """delegated from merge.applyupdates: merging of .hgsubstate file
88 88 in working context, merging context and ancestor context"""
89 89 if mctx == actx: # backwards?
90 90 actx = wctx.p1()
91 91 s1 = wctx.substate
92 92 s2 = mctx.substate
93 93 sa = actx.substate
94 94 sm = {}
95 95
96 96 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
97 97
98 98 def debug(s, msg, r=""):
99 99 if r:
100 100 r = "%s:%s:%s" % r
101 101 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
102 102
103 103 for s, l in s1.items():
104 104 a = sa.get(s, nullstate)
105 105 ld = l # local state with possible dirty flag for compares
106 106 if wctx.sub(s).dirty():
107 107 ld = (l[0], l[1] + "+")
108 108 if wctx == actx: # overwrite
109 109 a = ld
110 110
111 111 if s in s2:
112 112 r = s2[s]
113 113 if ld == r or r == a: # no change or local is newer
114 114 sm[s] = l
115 115 continue
116 116 elif ld == a: # other side changed
117 117 debug(s, "other changed, get", r)
118 118 wctx.sub(s).get(r, overwrite)
119 119 sm[s] = r
120 120 elif ld[0] != r[0]: # sources differ
121 121 if repo.ui.promptchoice(
122 122 _(' subrepository sources for %s differ\n'
123 123 'use (l)ocal source (%s) or (r)emote source (%s)?')
124 124 % (s, l[0], r[0]),
125 125 (_('&Local'), _('&Remote')), 0):
126 126 debug(s, "prompt changed, get", r)
127 127 wctx.sub(s).get(r, overwrite)
128 128 sm[s] = r
129 129 elif ld[1] == a[1]: # local side is unchanged
130 130 debug(s, "other side changed, get", r)
131 131 wctx.sub(s).get(r, overwrite)
132 132 sm[s] = r
133 133 else:
134 134 debug(s, "both sides changed, merge with", r)
135 135 wctx.sub(s).merge(r)
136 136 sm[s] = l
137 137 elif ld == a: # remote removed, local unchanged
138 138 debug(s, "remote removed, remove")
139 139 wctx.sub(s).remove()
140 140 else:
141 141 if repo.ui.promptchoice(
142 142 _(' local changed subrepository %s which remote removed\n'
143 143 'use (c)hanged version or (d)elete?') % s,
144 144 (_('&Changed'), _('&Delete')), 0):
145 145 debug(s, "prompt remove")
146 146 wctx.sub(s).remove()
147 147
148 148 for s, r in s2.items():
149 149 if s in s1:
150 150 continue
151 151 elif s not in sa:
152 152 debug(s, "remote added, get", r)
153 153 mctx.sub(s).get(r)
154 154 sm[s] = r
155 155 elif r != sa[s]:
156 156 if repo.ui.promptchoice(
157 157 _(' remote changed subrepository %s which local removed\n'
158 158 'use (c)hanged version or (d)elete?') % s,
159 159 (_('&Changed'), _('&Delete')), 0) == 0:
160 160 debug(s, "prompt recreate", r)
161 161 wctx.sub(s).get(r)
162 162 sm[s] = r
163 163
164 164 # record merged .hgsubstate
165 165 writestate(repo, sm)
166 166
167 167 def _updateprompt(ui, sub, dirty, local, remote):
168 168 if dirty:
169 169 msg = (_(' subrepository sources for %s differ\n'
170 170 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
171 171 % (subrelpath(sub), local, remote))
172 172 else:
173 173 msg = (_(' subrepository sources for %s differ (in checked out version)\n'
174 174 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
175 175 % (subrelpath(sub), local, remote))
176 176 return ui.promptchoice(msg, (_('&Local'), _('&Remote')), 0)
177 177
178 178 def reporelpath(repo):
179 179 """return path to this (sub)repo as seen from outermost repo"""
180 180 parent = repo
181 181 while hasattr(parent, '_subparent'):
182 182 parent = parent._subparent
183 183 return repo.root[len(parent.root)+1:]
184 184
185 185 def subrelpath(sub):
186 186 """return path to this subrepo as seen from outermost repo"""
187 187 if hasattr(sub, '_relpath'):
188 188 return sub._relpath
189 189 if not hasattr(sub, '_repo'):
190 190 return sub._path
191 191 return reporelpath(sub._repo)
192 192
193 193 def _abssource(repo, push=False, abort=True):
194 194 """return pull/push path of repo - either based on parent repo .hgsub info
195 195 or on the top repo config. Abort or return None if no source found."""
196 196 if hasattr(repo, '_subparent'):
197 197 source = repo._subsource
198 198 if source.startswith('/') or '://' in source:
199 199 return source
200 200 parent = _abssource(repo._subparent, push, abort=False)
201 201 if parent:
202 202 if '://' in parent:
203 203 if parent[-1] == '/':
204 204 parent = parent[:-1]
205 205 r = urlparse.urlparse(parent + '/' + source)
206 206 if parent.startswith('ssh://') and not r[1]:
207 207 # Python before 2.6 parses ssh:// URLs wrong
208 208 host, path = r[2][2:].split('/', 1)
209 209 r2 = '//%s/%s' % (host, posixpath.normpath(path))
210 210 else:
211 211 r2 = posixpath.normpath(r[2])
212 212 r = urlparse.urlunparse((r[0], r[1], r2,
213 213 r[3], r[4], r[5]))
214 214 return r
215 215 else: # plain file system path
216 216 return posixpath.normpath(os.path.join(parent, repo._subsource))
217 217 else: # recursion reached top repo
218 218 if hasattr(repo, '_subtoppath'):
219 219 return repo._subtoppath
220 220 if push and repo.ui.config('paths', 'default-push'):
221 221 return repo.ui.config('paths', 'default-push')
222 222 if repo.ui.config('paths', 'default'):
223 223 return repo.ui.config('paths', 'default')
224 224 if abort:
225 225 raise util.Abort(_("default path for subrepository %s not found") %
226 226 reporelpath(repo))
227 227
228 228 def itersubrepos(ctx1, ctx2):
229 229 """find subrepos in ctx1 or ctx2"""
230 230 # Create a (subpath, ctx) mapping where we prefer subpaths from
231 231 # ctx1. The subpaths from ctx2 are important when the .hgsub file
232 232 # has been modified (in ctx2) but not yet committed (in ctx1).
233 233 subpaths = dict.fromkeys(ctx2.substate, ctx2)
234 234 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
235 235 for subpath, ctx in sorted(subpaths.iteritems()):
236 236 yield subpath, ctx.sub(subpath)
237 237
238 238 def subrepo(ctx, path):
239 239 """return instance of the right subrepo class for subrepo in path"""
240 240 # subrepo inherently violates our import layering rules
241 241 # because it wants to make repo objects from deep inside the stack
242 242 # so we manually delay the circular imports to not break
243 243 # scripts that don't use our demand-loading
244 244 global hg
245 245 import hg as h
246 246 hg = h
247 247
248 248 util.path_auditor(ctx._repo.root)(path)
249 249 state = ctx.substate.get(path, nullstate)
250 250 if state[2] not in types:
251 251 raise util.Abort(_('unknown subrepo type %s') % state[2])
252 252 return types[state[2]](ctx, path, state[:2])
253 253
254 254 # subrepo classes need to implement the following abstract class:
255 255
256 256 class abstractsubrepo(object):
257 257
258 258 def dirty(self, ignoreupdate=False):
259 259 """returns true if the dirstate of the subrepo is dirty or does not
260 260 match current stored state. If ignoreupdate is true, only check
261 261 whether the subrepo has uncommitted changes in its dirstate.
262 262 """
263 263 raise NotImplementedError
264 264
265 265 def checknested(self, path):
266 266 """check if path is a subrepository within this repository"""
267 267 return False
268 268
269 269 def commit(self, text, user, date):
270 270 """commit the current changes to the subrepo with the given
271 271 log message. Use given user and date if possible. Return the
272 272 new state of the subrepo.
273 273 """
274 274 raise NotImplementedError
275 275
276 276 def remove(self):
277 277 """remove the subrepo
278 278
279 279 (should verify the dirstate is not dirty first)
280 280 """
281 281 raise NotImplementedError
282 282
283 283 def get(self, state, overwrite=False):
284 284 """run whatever commands are needed to put the subrepo into
285 285 this state
286 286 """
287 287 raise NotImplementedError
288 288
289 289 def merge(self, state):
290 290 """merge currently-saved state with the new state."""
291 291 raise NotImplementedError
292 292
293 293 def push(self, force):
294 294 """perform whatever action is analogous to 'hg push'
295 295
296 296 This may be a no-op on some systems.
297 297 """
298 298 raise NotImplementedError
299 299
300 300 def add(self, ui, match, dryrun, prefix):
301 301 return []
302 302
303 303 def status(self, rev2, **opts):
304 304 return [], [], [], [], [], [], []
305 305
306 306 def diff(self, diffopts, node2, match, prefix, **opts):
307 307 pass
308 308
309 309 def outgoing(self, ui, dest, opts):
310 310 return 1
311 311
312 312 def incoming(self, ui, source, opts):
313 313 return 1
314 314
315 315 def files(self):
316 316 """return filename iterator"""
317 317 raise NotImplementedError
318 318
319 319 def filedata(self, name):
320 320 """return file data"""
321 321 raise NotImplementedError
322 322
323 323 def fileflags(self, name):
324 324 """return file flags"""
325 325 return ''
326 326
327 327 def archive(self, ui, archiver, prefix):
328 328 files = self.files()
329 329 total = len(files)
330 330 relpath = subrelpath(self)
331 331 ui.progress(_('archiving (%s)') % relpath, 0,
332 332 unit=_('files'), total=total)
333 333 for i, name in enumerate(files):
334 334 flags = self.fileflags(name)
335 335 mode = 'x' in flags and 0755 or 0644
336 336 symlink = 'l' in flags
337 337 archiver.addfile(os.path.join(prefix, self._path, name),
338 338 mode, symlink, self.filedata(name))
339 339 ui.progress(_('archiving (%s)') % relpath, i + 1,
340 340 unit=_('files'), total=total)
341 341 ui.progress(_('archiving (%s)') % relpath, None)
342 342
343 343
344 344 class hgsubrepo(abstractsubrepo):
345 345 def __init__(self, ctx, path, state):
346 346 self._path = path
347 347 self._state = state
348 348 r = ctx._repo
349 349 root = r.wjoin(path)
350 350 create = False
351 351 if not os.path.exists(os.path.join(root, '.hg')):
352 352 create = True
353 353 util.makedirs(root)
354 354 self._repo = hg.repository(r.ui, root, create=create)
355 355 self._repo._subparent = r
356 356 self._repo._subsource = state[0]
357 357
358 358 if create:
359 359 fp = self._repo.opener("hgrc", "w", text=True)
360 360 fp.write('[paths]\n')
361 361
362 362 def addpathconfig(key, value):
363 363 if value:
364 364 fp.write('%s = %s\n' % (key, value))
365 365 self._repo.ui.setconfig('paths', key, value)
366 366
367 367 defpath = _abssource(self._repo, abort=False)
368 368 defpushpath = _abssource(self._repo, True, abort=False)
369 369 addpathconfig('default', defpath)
370 370 if defpath != defpushpath:
371 371 addpathconfig('default-push', defpushpath)
372 372 fp.close()
373 373
374 374 def add(self, ui, match, dryrun, prefix):
375 375 return cmdutil.add(ui, self._repo, match, dryrun, True,
376 376 os.path.join(prefix, self._path))
377 377
378 378 def status(self, rev2, **opts):
379 379 try:
380 380 rev1 = self._state[1]
381 381 ctx1 = self._repo[rev1]
382 382 ctx2 = self._repo[rev2]
383 383 return self._repo.status(ctx1, ctx2, **opts)
384 384 except error.RepoLookupError, inst:
385 385 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
386 386 % (inst, subrelpath(self)))
387 387 return [], [], [], [], [], [], []
388 388
389 389 def diff(self, diffopts, node2, match, prefix, **opts):
390 390 try:
391 391 node1 = node.bin(self._state[1])
392 392 # We currently expect node2 to come from substate and be
393 393 # in hex format
394 394 if node2 is not None:
395 395 node2 = node.bin(node2)
396 396 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
397 397 node1, node2, match,
398 398 prefix=os.path.join(prefix, self._path),
399 399 listsubrepos=True, **opts)
400 400 except error.RepoLookupError, inst:
401 401 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
402 402 % (inst, subrelpath(self)))
403 403
404 404 def archive(self, ui, archiver, prefix):
405 405 abstractsubrepo.archive(self, ui, archiver, prefix)
406 406
407 407 rev = self._state[1]
408 408 ctx = self._repo[rev]
409 409 for subpath in ctx.substate:
410 410 s = subrepo(ctx, subpath)
411 411 s.archive(ui, archiver, os.path.join(prefix, self._path))
412 412
413 413 def dirty(self, ignoreupdate=False):
414 414 r = self._state[1]
415 415 if r == '' and not ignoreupdate: # no state recorded
416 416 return True
417 417 w = self._repo[None]
418 418 if w.p1() != self._repo[r] and not ignoreupdate:
419 419 # different version checked out
420 420 return True
421 421 return w.dirty() # working directory changed
422 422
423 423 def checknested(self, path):
424 424 return self._repo._checknested(self._repo.wjoin(path))
425 425
426 426 def commit(self, text, user, date):
427 427 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
428 428 n = self._repo.commit(text, user, date)
429 429 if not n:
430 430 return self._repo['.'].hex() # different version checked out
431 431 return node.hex(n)
432 432
433 433 def remove(self):
434 434 # we can't fully delete the repository as it may contain
435 435 # local-only history
436 436 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
437 437 hg.clean(self._repo, node.nullid, False)
438 438
439 439 def _get(self, state):
440 440 source, revision, kind = state
441 441 try:
442 442 self._repo.lookup(revision)
443 443 except error.RepoError:
444 444 self._repo._subsource = source
445 445 srcurl = _abssource(self._repo)
446 446 self._repo.ui.status(_('pulling subrepo %s from %s\n')
447 447 % (subrelpath(self), srcurl))
448 448 other = hg.repository(self._repo.ui, srcurl)
449 449 self._repo.pull(other)
450 450
451 451 def get(self, state, overwrite=False):
452 452 self._get(state)
453 453 source, revision, kind = state
454 454 self._repo.ui.debug("getting subrepo %s\n" % self._path)
455 455 hg.clean(self._repo, revision, False)
456 456
457 457 def merge(self, state):
458 458 self._get(state)
459 459 cur = self._repo['.']
460 460 dst = self._repo[state[1]]
461 461 anc = dst.ancestor(cur)
462 462
463 463 def mergefunc():
464 464 if anc == cur:
465 465 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
466 466 hg.update(self._repo, state[1])
467 467 elif anc == dst:
468 468 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
469 469 else:
470 470 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
471 471 hg.merge(self._repo, state[1], remind=False)
472 472
473 473 wctx = self._repo[None]
474 474 if self.dirty():
475 475 if anc != dst:
476 476 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
477 477 mergefunc()
478 478 else:
479 479 mergefunc()
480 480 else:
481 481 mergefunc()
482 482
483 483 def push(self, force):
484 484 # push subrepos depth-first for coherent ordering
485 485 c = self._repo['']
486 486 subs = c.substate # only repos that are committed
487 487 for s in sorted(subs):
488 488 if not c.sub(s).push(force):
489 489 return False
490 490
491 491 dsturl = _abssource(self._repo, True)
492 492 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
493 493 (subrelpath(self), dsturl))
494 494 other = hg.repository(self._repo.ui, dsturl)
495 495 return self._repo.push(other, force)
496 496
497 497 def outgoing(self, ui, dest, opts):
498 498 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
499 499
500 500 def incoming(self, ui, source, opts):
501 501 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
502 502
503 503 def files(self):
504 504 rev = self._state[1]
505 505 ctx = self._repo[rev]
506 506 return ctx.manifest()
507 507
508 508 def filedata(self, name):
509 509 rev = self._state[1]
510 510 return self._repo[rev][name].data()
511 511
512 512 def fileflags(self, name):
513 513 rev = self._state[1]
514 514 ctx = self._repo[rev]
515 515 return ctx.flags(name)
516 516
517 517
518 518 class svnsubrepo(abstractsubrepo):
519 519 def __init__(self, ctx, path, state):
520 520 self._path = path
521 521 self._state = state
522 522 self._ctx = ctx
523 523 self._ui = ctx._repo.ui
524 524
525 525 def _svncommand(self, commands, filename=''):
526 526 cmd = ['svn']
527 527 # Starting in svn 1.5 --non-interactive is a global flag
528 528 # instead of being per-command, but we need to support 1.4 so
529 529 # we have to be intelligent about what commands take
530 530 # --non-interactive.
531 531 if (not self._ui.interactive() and
532 532 commands[0] in ('update', 'checkout', 'commit')):
533 533 cmd.append('--non-interactive')
534 534 cmd.extend(commands)
535 535 if filename is not None:
536 536 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
537 537 cmd.append(path)
538 538 env = dict(os.environ)
539 539 # Avoid localized output, preserve current locale for everything else.
540 540 env['LC_MESSAGES'] = 'C'
541 541 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
542 542 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
543 543 universal_newlines=True, env=env)
544 544 stdout, stderr = p.communicate()
545 545 stderr = stderr.strip()
546 546 if stderr:
547 547 raise util.Abort(stderr)
548 548 return stdout
549 549
550 550 @propertycache
551 551 def _svnversion(self):
552 552 output = self._svncommand(['--version'], filename=None)
553 553 m = re.search(r'^svn,\s+version\s+(\d+)\.(\d+)', output)
554 554 if not m:
555 555 raise util.Abort(_('cannot retrieve svn tool version'))
556 556 return (int(m.group(1)), int(m.group(2)))
557 557
558 558 def _wcrevs(self):
559 559 # Get the working directory revision as well as the last
560 560 # commit revision so we can compare the subrepo state with
561 561 # both. We used to store the working directory one.
562 562 output = self._svncommand(['info', '--xml'])
563 563 doc = xml.dom.minidom.parseString(output)
564 564 entries = doc.getElementsByTagName('entry')
565 565 lastrev, rev = '0', '0'
566 566 if entries:
567 567 rev = str(entries[0].getAttribute('revision')) or '0'
568 568 commits = entries[0].getElementsByTagName('commit')
569 569 if commits:
570 570 lastrev = str(commits[0].getAttribute('revision')) or '0'
571 571 return (lastrev, rev)
572 572
573 573 def _wcrev(self):
574 574 return self._wcrevs()[0]
575 575
576 576 def _wcchanged(self):
577 577 """Return (changes, extchanges) where changes is True
578 578 if the working directory was changed, and extchanges is
579 579 True if any of these changes concern an external entry.
580 580 """
581 581 output = self._svncommand(['status', '--xml'])
582 582 externals, changes = [], []
583 583 doc = xml.dom.minidom.parseString(output)
584 584 for e in doc.getElementsByTagName('entry'):
585 585 s = e.getElementsByTagName('wc-status')
586 586 if not s:
587 587 continue
588 588 item = s[0].getAttribute('item')
589 589 props = s[0].getAttribute('props')
590 590 path = e.getAttribute('path')
591 591 if item == 'external':
592 592 externals.append(path)
593 593 if (item not in ('', 'normal', 'unversioned', 'external')
594 594 or props not in ('', 'none')):
595 595 changes.append(path)
596 596 for path in changes:
597 597 for ext in externals:
598 598 if path == ext or path.startswith(ext + os.sep):
599 599 return True, True
600 600 return bool(changes), False
601 601
602 602 def dirty(self, ignoreupdate=False):
603 603 if not self._wcchanged()[0]:
604 604 if self._state[1] in self._wcrevs() or ignoreupdate:
605 605 return False
606 606 return True
607 607
608 608 def commit(self, text, user, date):
609 609 # user and date are out of our hands since svn is centralized
610 610 changed, extchanged = self._wcchanged()
611 611 if not changed:
612 612 return self._wcrev()
613 613 if extchanged:
614 614 # Do not try to commit externals
615 615 raise util.Abort(_('cannot commit svn externals'))
616 616 commitinfo = self._svncommand(['commit', '-m', text])
617 617 self._ui.status(commitinfo)
618 618 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
619 619 if not newrev:
620 620 raise util.Abort(commitinfo.splitlines()[-1])
621 621 newrev = newrev.groups()[0]
622 622 self._ui.status(self._svncommand(['update', '-r', newrev]))
623 623 return newrev
624 624
625 625 def remove(self):
626 626 if self.dirty():
627 627 self._ui.warn(_('not removing repo %s because '
628 628 'it has changes.\n' % self._path))
629 629 return
630 630 self._ui.note(_('removing subrepo %s\n') % self._path)
631 631
632 632 def onerror(function, path, excinfo):
633 633 if function is not os.remove:
634 634 raise
635 635 # read-only files cannot be unlinked under Windows
636 636 s = os.stat(path)
637 637 if (s.st_mode & stat.S_IWRITE) != 0:
638 638 raise
639 639 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
640 640 os.remove(path)
641 641
642 642 path = self._ctx._repo.wjoin(self._path)
643 643 shutil.rmtree(path, onerror=onerror)
644 644 try:
645 645 os.removedirs(os.path.dirname(path))
646 646 except OSError:
647 647 pass
648 648
649 649 def get(self, state, overwrite=False):
650 650 if overwrite:
651 651 self._svncommand(['revert', '--recursive'])
652 652 args = ['checkout']
653 653 if self._svnversion >= (1, 5):
654 654 args.append('--force')
655 655 args.extend([state[0], '--revision', state[1]])
656 656 status = self._svncommand(args)
657 657 if not re.search('Checked out revision [0-9]+.', status):
658 658 raise util.Abort(status.splitlines()[-1])
659 659 self._ui.status(status)
660 660
661 661 def merge(self, state):
662 662 old = self._state[1]
663 663 new = state[1]
664 664 if new != self._wcrev():
665 665 dirty = old == self._wcrev() or self._wcchanged()[0]
666 666 if _updateprompt(self._ui, self, dirty, self._wcrev(), new):
667 667 self.get(state, False)
668 668
669 669 def push(self, force):
670 670 # push is a no-op for SVN
671 671 return True
672 672
673 673 def files(self):
674 674 output = self._svncommand(['list'])
675 675 # This works because svn forbids \n in filenames.
676 676 return output.splitlines()
677 677
678 678 def filedata(self, name):
679 679 return self._svncommand(['cat'], name)
680 680
681 681
682 682 class gitsubrepo(abstractsubrepo):
683 683 def __init__(self, ctx, path, state):
684 684 # TODO add git version check.
685 685 self._state = state
686 686 self._ctx = ctx
687 687 self._path = path
688 688 self._relpath = os.path.join(reporelpath(ctx._repo), path)
689 689 self._abspath = ctx._repo.wjoin(path)
690 690 self._subparent = ctx._repo
691 691 self._ui = ctx._repo.ui
692 692
693 693 def _gitcommand(self, commands, env=None, stream=False):
694 694 return self._gitdir(commands, env=env, stream=stream)[0]
695 695
696 696 def _gitdir(self, commands, env=None, stream=False):
697 697 return self._gitnodir(commands, env=env, stream=stream,
698 698 cwd=self._abspath)
699 699
700 700 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
701 701 """Calls the git command
702 702
703 703 The methods tries to call the git command. versions previor to 1.6.0
704 704 are not supported and very probably fail.
705 705 """
706 706 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
707 707 # unless ui.quiet is set, print git's stderr,
708 708 # which is mostly progress and useful info
709 709 errpipe = None
710 710 if self._ui.quiet:
711 711 errpipe = open(os.devnull, 'w')
712 712 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
713 713 close_fds=util.closefds,
714 714 stdout=subprocess.PIPE, stderr=errpipe)
715 715 if stream:
716 716 return p.stdout, None
717 717
718 718 retdata = p.stdout.read().strip()
719 719 # wait for the child to exit to avoid race condition.
720 720 p.wait()
721 721
722 722 if p.returncode != 0 and p.returncode != 1:
723 723 # there are certain error codes that are ok
724 724 command = commands[0]
725 725 if command in ('cat-file', 'symbolic-ref'):
726 726 return retdata, p.returncode
727 727 # for all others, abort
728 728 raise util.Abort('git %s error %d in %s' %
729 729 (command, p.returncode, self._relpath))
730 730
731 731 return retdata, p.returncode
732 732
733 733 def _gitmissing(self):
734 734 return not os.path.exists(os.path.join(self._abspath, '.git'))
735 735
736 736 def _gitstate(self):
737 737 return self._gitcommand(['rev-parse', 'HEAD'])
738 738
739 739 def _gitcurrentbranch(self):
740 740 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
741 741 if err:
742 742 current = None
743 743 return current
744 744
745 745 def _githavelocally(self, revision):
746 746 out, code = self._gitdir(['cat-file', '-e', revision])
747 747 return code == 0
748 748
749 749 def _gitisancestor(self, r1, r2):
750 750 base = self._gitcommand(['merge-base', r1, r2])
751 751 return base == r1
752 752
753 753 def _gitbranchmap(self):
754 754 '''returns 2 things:
755 755 a map from git branch to revision
756 756 a map from revision to branches'''
757 757 branch2rev = {}
758 758 rev2branch = {}
759 759
760 760 out = self._gitcommand(['for-each-ref', '--format',
761 761 '%(objectname) %(refname)'])
762 762 for line in out.split('\n'):
763 763 revision, ref = line.split(' ')
764 764 if (not ref.startswith('refs/heads/') and
765 765 not ref.startswith('refs/remotes/')):
766 766 continue
767 767 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
768 768 continue # ignore remote/HEAD redirects
769 769 branch2rev[ref] = revision
770 770 rev2branch.setdefault(revision, []).append(ref)
771 771 return branch2rev, rev2branch
772 772
773 773 def _gittracking(self, branches):
774 774 'return map of remote branch to local tracking branch'
775 775 # assumes no more than one local tracking branch for each remote
776 776 tracking = {}
777 777 for b in branches:
778 778 if b.startswith('refs/remotes/'):
779 779 continue
780 780 remote = self._gitcommand(['config', 'branch.%s.remote' % b])
781 781 if remote:
782 782 ref = self._gitcommand(['config', 'branch.%s.merge' % b])
783 783 tracking['refs/remotes/%s/%s' %
784 784 (remote, ref.split('/', 2)[2])] = b
785 785 return tracking
786 786
787 787 def _abssource(self, source):
788 788 if '://' not in source:
789 789 # recognize the scp syntax as an absolute source
790 790 colon = source.find(':')
791 791 if colon != -1 and '/' not in source[:colon]:
792 792 return source
793 793 self._subsource = source
794 794 return _abssource(self)
795 795
796 796 def _fetch(self, source, revision):
797 797 if self._gitmissing():
798 798 self._ui.status(_('cloning subrepo %s\n') % self._relpath)
799 799 self._gitnodir(['clone', self._abssource(source), self._abspath])
800 800 if self._githavelocally(revision):
801 801 return
802 802 self._ui.status(_('pulling subrepo %s\n') % self._relpath)
803 803 # try only origin: the originally cloned repo
804 804 self._gitcommand(['fetch'])
805 805 if not self._githavelocally(revision):
806 806 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
807 807 (revision, self._relpath))
808 808
809 809 def dirty(self, ignoreupdate=False):
810 810 if self._gitmissing():
811 return True
811 return self._state[1] != ''
812 812 if not ignoreupdate and self._state[1] != self._gitstate():
813 813 # different version checked out
814 814 return True
815 815 # check for staged changes or modified files; ignore untracked files
816 816 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
817 817 return code == 1
818 818
819 819 def get(self, state, overwrite=False):
820 820 source, revision, kind = state
821 if not revision:
822 self.remove()
823 return
821 824 self._fetch(source, revision)
822 825 # if the repo was set to be bare, unbare it
823 826 if self._gitcommand(['config', '--bool', 'core.bare']) == 'true':
824 827 self._gitcommand(['config', 'core.bare', 'false'])
825 828 if self._gitstate() == revision:
826 829 self._gitcommand(['reset', '--hard', 'HEAD'])
827 830 return
828 831 elif self._gitstate() == revision:
829 832 if overwrite:
830 833 # first reset the index to unmark new files for commit, because
831 834 # reset --hard will otherwise throw away files added for commit,
832 835 # not just unmark them.
833 836 self._gitcommand(['reset', 'HEAD'])
834 837 self._gitcommand(['reset', '--hard', 'HEAD'])
835 838 return
836 839 branch2rev, rev2branch = self._gitbranchmap()
837 840
838 841 def checkout(args):
839 842 cmd = ['checkout']
840 843 if overwrite:
841 844 # first reset the index to unmark new files for commit, because
842 845 # the -f option will otherwise throw away files added for
843 846 # commit, not just unmark them.
844 847 self._gitcommand(['reset', 'HEAD'])
845 848 cmd.append('-f')
846 849 self._gitcommand(cmd + args)
847 850
848 851 def rawcheckout():
849 852 # no branch to checkout, check it out with no branch
850 853 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
851 854 self._relpath)
852 855 self._ui.warn(_('check out a git branch if you intend '
853 856 'to make changes\n'))
854 857 checkout(['-q', revision])
855 858
856 859 if revision not in rev2branch:
857 860 rawcheckout()
858 861 return
859 862 branches = rev2branch[revision]
860 863 firstlocalbranch = None
861 864 for b in branches:
862 865 if b == 'refs/heads/master':
863 866 # master trumps all other branches
864 867 checkout(['refs/heads/master'])
865 868 return
866 869 if not firstlocalbranch and not b.startswith('refs/remotes/'):
867 870 firstlocalbranch = b
868 871 if firstlocalbranch:
869 872 checkout([firstlocalbranch])
870 873 return
871 874
872 875 tracking = self._gittracking(branch2rev.keys())
873 876 # choose a remote branch already tracked if possible
874 877 remote = branches[0]
875 878 if remote not in tracking:
876 879 for b in branches:
877 880 if b in tracking:
878 881 remote = b
879 882 break
880 883
881 884 if remote not in tracking:
882 885 # create a new local tracking branch
883 886 local = remote.split('/', 2)[2]
884 887 checkout(['-b', local, remote])
885 888 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
886 889 # When updating to a tracked remote branch,
887 890 # if the local tracking branch is downstream of it,
888 891 # a normal `git pull` would have performed a "fast-forward merge"
889 892 # which is equivalent to updating the local branch to the remote.
890 893 # Since we are only looking at branching at update, we need to
891 894 # detect this situation and perform this action lazily.
892 895 if tracking[remote] != self._gitcurrentbranch():
893 896 checkout([tracking[remote]])
894 897 self._gitcommand(['merge', '--ff', remote])
895 898 else:
896 899 # a real merge would be required, just checkout the revision
897 900 rawcheckout()
898 901
899 902 def commit(self, text, user, date):
900 903 if self._gitmissing():
901 904 raise util.Abort(_("subrepo %s is missing") % self._relpath)
902 905 cmd = ['commit', '-a', '-m', text]
903 906 env = os.environ.copy()
904 907 if user:
905 908 cmd += ['--author', user]
906 909 if date:
907 910 # git's date parser silently ignores when seconds < 1e9
908 911 # convert to ISO8601
909 912 env['GIT_AUTHOR_DATE'] = util.datestr(date,
910 913 '%Y-%m-%dT%H:%M:%S %1%2')
911 914 self._gitcommand(cmd, env=env)
912 915 # make sure commit works otherwise HEAD might not exist under certain
913 916 # circumstances
914 917 return self._gitstate()
915 918
916 919 def merge(self, state):
917 920 source, revision, kind = state
918 921 self._fetch(source, revision)
919 922 base = self._gitcommand(['merge-base', revision, self._state[1]])
920 923 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
921 924
922 925 def mergefunc():
923 926 if base == revision:
924 927 self.get(state) # fast forward merge
925 928 elif base != self._state[1]:
926 929 self._gitcommand(['merge', '--no-commit', revision])
927 930
928 931 if self.dirty():
929 932 if self._gitstate() != revision:
930 933 dirty = self._gitstate() == self._state[1] or code != 0
931 934 if _updateprompt(self._ui, self, dirty,
932 935 self._state[1][:7], revision[:7]):
933 936 mergefunc()
934 937 else:
935 938 mergefunc()
936 939
937 940 def push(self, force):
941 if not self._state[1]:
942 return True
938 943 if self._gitmissing():
939 944 raise util.Abort(_("subrepo %s is missing") % self._relpath)
940 945 # if a branch in origin contains the revision, nothing to do
941 946 branch2rev, rev2branch = self._gitbranchmap()
942 947 if self._state[1] in rev2branch:
943 948 for b in rev2branch[self._state[1]]:
944 949 if b.startswith('refs/remotes/origin/'):
945 950 return True
946 951 for b, revision in branch2rev.iteritems():
947 952 if b.startswith('refs/remotes/origin/'):
948 953 if self._gitisancestor(self._state[1], revision):
949 954 return True
950 955 # otherwise, try to push the currently checked out branch
951 956 cmd = ['push']
952 957 if force:
953 958 cmd.append('--force')
954 959
955 960 current = self._gitcurrentbranch()
956 961 if current:
957 962 # determine if the current branch is even useful
958 963 if not self._gitisancestor(self._state[1], current):
959 964 self._ui.warn(_('unrelated git branch checked out '
960 965 'in subrepo %s\n') % self._relpath)
961 966 return False
962 967 self._ui.status(_('pushing branch %s of subrepo %s\n') %
963 968 (current.split('/', 2)[2], self._relpath))
964 969 self._gitcommand(cmd + ['origin', current])
965 970 return True
966 971 else:
967 972 self._ui.warn(_('no branch checked out in subrepo %s\n'
968 973 'cannot push revision %s') %
969 974 (self._relpath, self._state[1]))
970 975 return False
971 976
972 977 def remove(self):
973 978 if self._gitmissing():
974 979 return
975 980 if self.dirty():
976 981 self._ui.warn(_('not removing repo %s because '
977 982 'it has changes.\n') % self._relpath)
978 983 return
979 984 # we can't fully delete the repository as it may contain
980 985 # local-only history
981 986 self._ui.note(_('removing subrepo %s\n') % self._relpath)
982 987 self._gitcommand(['config', 'core.bare', 'true'])
983 988 for f in os.listdir(self._abspath):
984 989 if f == '.git':
985 990 continue
986 991 path = os.path.join(self._abspath, f)
987 992 if os.path.isdir(path) and not os.path.islink(path):
988 993 shutil.rmtree(path)
989 994 else:
990 995 os.remove(path)
991 996
992 997 def archive(self, ui, archiver, prefix):
993 998 source, revision = self._state
999 if not revision:
1000 return
994 1001 self._fetch(source, revision)
995 1002
996 1003 # Parse git's native archive command.
997 1004 # This should be much faster than manually traversing the trees
998 1005 # and objects with many subprocess calls.
999 1006 tarstream = self._gitcommand(['archive', revision], stream=True)
1000 1007 tar = tarfile.open(fileobj=tarstream, mode='r|')
1001 1008 relpath = subrelpath(self)
1002 1009 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1003 1010 for i, info in enumerate(tar):
1004 1011 if info.isdir():
1005 1012 continue
1006 1013 if info.issym():
1007 1014 data = info.linkname
1008 1015 else:
1009 1016 data = tar.extractfile(info).read()
1010 1017 archiver.addfile(os.path.join(prefix, self._path, info.name),
1011 1018 info.mode, info.issym(), data)
1012 1019 ui.progress(_('archiving (%s)') % relpath, i + 1,
1013 1020 unit=_('files'))
1014 1021 ui.progress(_('archiving (%s)') % relpath, None)
1015 1022
1016 1023
1017 1024 def status(self, rev2, **opts):
1018 if self._gitmissing():
1025 rev1 = self._state[1]
1026 if self._gitmissing() or not rev1:
1019 1027 # if the repo is missing, return no results
1020 1028 return [], [], [], [], [], [], []
1021 rev1 = self._state[1]
1022 1029 modified, added, removed = [], [], []
1023 1030 if rev2:
1024 1031 command = ['diff-tree', rev1, rev2]
1025 1032 else:
1026 1033 command = ['diff-index', rev1]
1027 1034 out = self._gitcommand(command)
1028 1035 for line in out.split('\n'):
1029 1036 tab = line.find('\t')
1030 1037 if tab == -1:
1031 1038 continue
1032 1039 status, f = line[tab - 1], line[tab + 1:]
1033 1040 if status == 'M':
1034 1041 modified.append(f)
1035 1042 elif status == 'A':
1036 1043 added.append(f)
1037 1044 elif status == 'D':
1038 1045 removed.append(f)
1039 1046
1040 1047 deleted = unknown = ignored = clean = []
1041 1048 return modified, added, removed, deleted, unknown, ignored, clean
1042 1049
1043 1050 types = {
1044 1051 'hg': hgsubrepo,
1045 1052 'svn': svnsubrepo,
1046 1053 'git': gitsubrepo,
1047 1054 }
@@ -1,474 +1,501 b''
1 1 $ "$TESTDIR/hghave" git || exit 80
2 2
3 3 make git commits repeatable
4 4
5 5 $ GIT_AUTHOR_NAME='test'; export GIT_AUTHOR_NAME
6 6 $ GIT_AUTHOR_EMAIL='test@example.org'; export GIT_AUTHOR_EMAIL
7 7 $ GIT_AUTHOR_DATE='1234567891 +0000'; export GIT_AUTHOR_DATE
8 8 $ GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"; export GIT_COMMITTER_NAME
9 9 $ GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"; export GIT_COMMITTER_EMAIL
10 10 $ GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"; export GIT_COMMITTER_DATE
11 11
12 12 root hg repo
13 13
14 14 $ hg init t
15 15 $ cd t
16 16 $ echo a > a
17 17 $ hg add a
18 18 $ hg commit -m a
19 19 $ cd ..
20 20
21 21 new external git repo
22 22
23 23 $ mkdir gitroot
24 24 $ cd gitroot
25 25 $ git init -q
26 26 $ echo g > g
27 27 $ git add g
28 28 $ git commit -q -m g
29 29
30 30 add subrepo clone
31 31
32 32 $ cd ../t
33 33 $ echo 's = [git]../gitroot' > .hgsub
34 34 $ git clone -q ../gitroot s
35 35 $ hg add .hgsub
36 36 $ hg commit -m 'new git subrepo'
37 37 committing subrepository s
38 38 $ hg debugsub
39 39 path s
40 40 source ../gitroot
41 41 revision da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
42 42
43 43 record a new commit from upstream from a different branch
44 44
45 45 $ cd ../gitroot
46 46 $ git checkout -q -b testing
47 47 $ echo gg >> g
48 48 $ git commit -q -a -m gg
49 49
50 50 $ cd ../t/s
51 51 $ git pull -q >/dev/null 2>/dev/null
52 52 $ git checkout -q -b testing origin/testing >/dev/null
53 53
54 54 $ cd ..
55 55 $ hg status --subrepos
56 56 M s/g
57 57 $ hg commit -m 'update git subrepo'
58 58 committing subrepository s
59 59 $ hg debugsub
60 60 path s
61 61 source ../gitroot
62 62 revision 126f2a14290cd5ce061fdedc430170e8d39e1c5a
63 63
64 64 make $GITROOT pushable, by replacing it with a clone with nothing checked out
65 65
66 66 $ cd ..
67 67 $ git clone gitroot gitrootbare --bare -q
68 68 $ rm -rf gitroot
69 69 $ mv gitrootbare gitroot
70 70
71 71 clone root
72 72
73 73 $ cd t
74 74 $ hg clone . ../tc
75 75 updating to branch default
76 76 cloning subrepo s
77 77 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
78 78 $ cd ../tc
79 79 $ hg debugsub
80 80 path s
81 81 source ../gitroot
82 82 revision 126f2a14290cd5ce061fdedc430170e8d39e1c5a
83 83
84 84 update to previous substate
85 85
86 86 $ hg update 1 -q
87 87 $ cat s/g
88 88 g
89 89 $ hg debugsub
90 90 path s
91 91 source ../gitroot
92 92 revision da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
93 93
94 94 clone root, make local change
95 95
96 96 $ cd ../t
97 97 $ hg clone . ../ta
98 98 updating to branch default
99 99 cloning subrepo s
100 100 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
101 101
102 102 $ cd ../ta
103 103 $ echo ggg >> s/g
104 104 $ hg status --subrepos
105 105 M s/g
106 106 $ hg commit -m ggg
107 107 committing subrepository s
108 108 $ hg debugsub
109 109 path s
110 110 source ../gitroot
111 111 revision 79695940086840c99328513acbe35f90fcd55e57
112 112
113 113 clone root separately, make different local change
114 114
115 115 $ cd ../t
116 116 $ hg clone . ../tb
117 117 updating to branch default
118 118 cloning subrepo s
119 119 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
120 120
121 121 $ cd ../tb/s
122 122 $ echo f > f
123 123 $ git add f
124 124 $ cd ..
125 125
126 126 $ hg status --subrepos
127 127 A s/f
128 128 $ hg commit -m f
129 129 committing subrepository s
130 130 $ hg debugsub
131 131 path s
132 132 source ../gitroot
133 133 revision aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
134 134
135 135 user b push changes
136 136
137 137 $ hg push 2>/dev/null
138 138 pushing to $TESTTMP/t
139 139 pushing branch testing of subrepo s
140 140 searching for changes
141 141 adding changesets
142 142 adding manifests
143 143 adding file changes
144 144 added 1 changesets with 1 changes to 1 files
145 145
146 146 user a pulls, merges, commits
147 147
148 148 $ cd ../ta
149 149 $ hg pull
150 150 pulling from $TESTTMP/t
151 151 searching for changes
152 152 adding changesets
153 153 adding manifests
154 154 adding file changes
155 155 added 1 changesets with 1 changes to 1 files (+1 heads)
156 156 (run 'hg heads' to see heads, 'hg merge' to merge)
157 157 $ hg merge 2>/dev/null
158 158 pulling subrepo s
159 159 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
160 160 (branch merge, don't forget to commit)
161 161 $ cat s/f
162 162 f
163 163 $ cat s/g
164 164 g
165 165 gg
166 166 ggg
167 167 $ hg commit -m 'merge'
168 168 committing subrepository s
169 169 $ hg status --subrepos --rev 1:5
170 170 M .hgsubstate
171 171 M s/g
172 172 A s/f
173 173 $ hg debugsub
174 174 path s
175 175 source ../gitroot
176 176 revision f47b465e1bce645dbf37232a00574aa1546ca8d3
177 177 $ hg push 2>/dev/null
178 178 pushing to $TESTTMP/t
179 179 pushing branch testing of subrepo s
180 180 searching for changes
181 181 adding changesets
182 182 adding manifests
183 183 adding file changes
184 184 added 2 changesets with 2 changes to 1 files
185 185
186 186 make upstream git changes
187 187
188 188 $ cd ..
189 189 $ git clone -q gitroot gitclone
190 190 $ cd gitclone
191 191 $ echo ff >> f
192 192 $ git commit -q -a -m ff
193 193 $ echo fff >> f
194 194 $ git commit -q -a -m fff
195 195 $ git push origin testing 2>/dev/null
196 196
197 197 make and push changes to hg without updating the subrepo
198 198
199 199 $ cd ../t
200 200 $ hg clone . ../td
201 201 updating to branch default
202 202 cloning subrepo s
203 203 checking out detached HEAD in subrepo s
204 204 check out a git branch if you intend to make changes
205 205 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
206 206 $ cd ../td
207 207 $ echo aa >> a
208 208 $ hg commit -m aa
209 209 $ hg push
210 210 pushing to $TESTTMP/t
211 211 searching for changes
212 212 adding changesets
213 213 adding manifests
214 214 adding file changes
215 215 added 1 changesets with 1 changes to 1 files
216 216
217 217 sync to upstream git, distribute changes
218 218
219 219 $ cd ../ta
220 220 $ hg pull -u -q
221 221 $ cd s
222 222 $ git pull -q >/dev/null 2>/dev/null
223 223 $ cd ..
224 224 $ hg commit -m 'git upstream sync'
225 225 committing subrepository s
226 226 $ hg debugsub
227 227 path s
228 228 source ../gitroot
229 229 revision 32a343883b74769118bb1d3b4b1fbf9156f4dddc
230 230 $ hg push -q
231 231
232 232 $ cd ../tb
233 233 $ hg pull -q
234 234 $ hg update 2>/dev/null
235 235 pulling subrepo s
236 236 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
237 237 $ hg debugsub
238 238 path s
239 239 source ../gitroot
240 240 revision 32a343883b74769118bb1d3b4b1fbf9156f4dddc
241 241
242 242 update to a revision without the subrepo, keeping the local git repository
243 243
244 244 $ cd ../t
245 245 $ hg up 0
246 246 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
247 247 $ ls -a s
248 248 .
249 249 ..
250 250 .git
251 251
252 252 $ hg up 2
253 253 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
254 254 $ ls -a s
255 255 .
256 256 ..
257 257 .git
258 258 g
259 259
260 260 archive subrepos
261 261
262 262 $ cd ../tc
263 263 $ hg pull -q
264 264 $ hg archive --subrepos -r 5 ../archive 2>/dev/null
265 265 pulling subrepo s
266 266 $ cd ../archive
267 267 $ cat s/f
268 268 f
269 269 $ cat s/g
270 270 g
271 271 gg
272 272 ggg
273 273
274 274 create nested repo
275 275
276 276 $ cd ..
277 277 $ hg init outer
278 278 $ cd outer
279 279 $ echo b>b
280 280 $ hg add b
281 281 $ hg commit -m b
282 282
283 283 $ hg clone ../t inner
284 284 updating to branch default
285 285 cloning subrepo s
286 286 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
287 287 $ echo inner = inner > .hgsub
288 288 $ hg add .hgsub
289 289 $ hg commit -m 'nested sub'
290 290 committing subrepository inner
291 291
292 292 nested commit
293 293
294 294 $ echo ffff >> inner/s/f
295 295 $ hg status --subrepos
296 296 M inner/s/f
297 297 $ hg commit -m nested
298 298 committing subrepository inner
299 299 committing subrepository inner/s
300 300
301 301 nested archive
302 302
303 303 $ hg archive --subrepos ../narchive
304 304 $ ls ../narchive/inner/s | grep -v pax_global_header
305 305 f
306 306 g
307 307
308 308 relative source expansion
309 309
310 310 $ cd ..
311 311 $ mkdir d
312 312 $ hg clone t d/t
313 313 updating to branch default
314 314 cloning subrepo s
315 315 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
316 316
317 317 Don't crash if the subrepo is missing
318 318
319 319 $ hg clone t missing -q
320 320 $ cd missing
321 321 $ rm -rf s
322 322 $ hg status -S
323 323 $ hg sum | grep commit
324 324 commit: 1 subrepos
325 325 $ hg push -q
326 326 abort: subrepo s is missing
327 327 [255]
328 328 $ hg commit -qm missing
329 329 abort: subrepo s is missing
330 330 [255]
331 331 $ hg update -C
332 332 cloning subrepo s
333 333 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
334 334 $ hg sum | grep commit
335 335 commit: (clean)
336 336
337 Don't crash if the .hgsubstate entry is missing
338
339 $ hg update 1 -q
340 $ hg rm .hgsubstate
341 $ hg commit .hgsubstate -m 'no substate'
342 created new head
343 $ hg tag -l nosubstate
344 $ hg manifest
345 .hgsub
346 a
347
348 $ hg status -S
349 $ hg sum | grep commit
350 commit: 1 subrepos
351
352 $ hg commit -m 'restore substate'
353 committing subrepository s
354 $ hg manifest
355 .hgsub
356 .hgsubstate
357 a
358 $ hg sum | grep commit
359 commit: (clean)
360
361 $ hg update -qC nosubstate
362 $ ls s
363
337 364 Check hg update --clean
338 365 $ cd $TESTTMP/ta
339 366 $ echo > s/g
340 367 $ cd s
341 368 $ echo c1 > f1
342 369 $ echo c1 > f2
343 370 $ git add f1
344 371 $ cd ..
345 372 $ hg status -S
346 373 M s/g
347 374 A s/f1
348 375 $ ls s
349 376 f
350 377 f1
351 378 f2
352 379 g
353 380 $ hg update --clean
354 381 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
355 382 $ hg status -S
356 383 $ ls s
357 384 f
358 385 f1
359 386 f2
360 387 g
361 388
362 389 Sticky subrepositories, no changes
363 390 $ cd $TESTTMP/ta
364 391 $ hg id -n
365 392 7
366 393 $ cd s
367 394 $ git rev-parse HEAD
368 395 32a343883b74769118bb1d3b4b1fbf9156f4dddc
369 396 $ cd ..
370 397 $ hg update 1 > /dev/null 2>&1
371 398 $ hg id -n
372 399 1
373 400 $ cd s
374 401 $ git rev-parse HEAD
375 402 da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
376 403 $ cd ..
377 404
378 405 Sticky subrepositorys, file changes
379 406 $ touch s/f1
380 407 $ cd s
381 408 $ git add f1
382 409 $ cd ..
383 410 $ hg id -n
384 411 1
385 412 $ cd s
386 413 $ git rev-parse HEAD
387 414 da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
388 415 $ cd ..
389 416 $ hg update 4
390 417 subrepository sources for s differ
391 418 use (l)ocal source (da5f5b1) or (r)emote source (aa84837)?
392 419 l
393 420 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
394 421 $ hg id -n
395 422 4+
396 423 $ cd s
397 424 $ git rev-parse HEAD
398 425 da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
399 426 $ cd ..
400 427 $ hg update --clean tip > /dev/null 2>&1
401 428
402 429 Sticky subrepository, revision updates
403 430 $ hg id -n
404 431 7
405 432 $ cd s
406 433 $ git rev-parse HEAD
407 434 32a343883b74769118bb1d3b4b1fbf9156f4dddc
408 435 $ cd ..
409 436 $ cd s
410 437 $ git checkout aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
411 438 Previous HEAD position was 32a3438... fff
412 439 HEAD is now at aa84837... f
413 440 $ cd ..
414 441 $ hg update 1
415 442 subrepository sources for s differ (in checked out version)
416 443 use (l)ocal source (32a3438) or (r)emote source (da5f5b1)?
417 444 l
418 445 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
419 446 $ hg id -n
420 447 1+
421 448 $ cd s
422 449 $ git rev-parse HEAD
423 450 aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
424 451 $ cd ..
425 452
426 453 Sticky subrepository, file changes and revision updates
427 454 $ touch s/f1
428 455 $ cd s
429 456 $ git add f1
430 457 $ git rev-parse HEAD
431 458 aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
432 459 $ cd ..
433 460 $ hg id -n
434 461 1+
435 462 $ hg update 7
436 463 subrepository sources for s differ
437 464 use (l)ocal source (32a3438) or (r)emote source (32a3438)?
438 465 l
439 466 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
440 467 $ hg id -n
441 468 7
442 469 $ cd s
443 470 $ git rev-parse HEAD
444 471 aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
445 472 $ cd ..
446 473
447 474 Sticky repository, update --clean
448 475 $ hg update --clean tip
449 476 Previous HEAD position was aa84837... f
450 477 HEAD is now at 32a3438... fff
451 478 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
452 479 $ hg id -n
453 480 7
454 481 $ cd s
455 482 $ git rev-parse HEAD
456 483 32a343883b74769118bb1d3b4b1fbf9156f4dddc
457 484 $ cd ..
458 485
459 486 Test subrepo already at intended revision:
460 487 $ cd s
461 488 $ git checkout 32a343883b74769118bb1d3b4b1fbf9156f4dddc
462 489 HEAD is now at 32a3438... fff
463 490 $ cd ..
464 491 $ hg update 1
465 492 Previous HEAD position was 32a3438... fff
466 493 HEAD is now at da5f5b1... g
467 494 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
468 495 $ hg id -n
469 496 1
470 497 $ cd s
471 498 $ git rev-parse HEAD
472 499 da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
473 500 $ cd ..
474 501
General Comments 0
You need to be logged in to leave comments. Login now