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