##// END OF EJS Templates
subrepo: show the source that git pulls
Eric Eisner -
r13569:3ab3b892 default
parent child Browse files
Show More
@@ -1,1016 +1,1023 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', state[0], '--revision', state[1]])
629 629 if not re.search('Checked out revision [0-9]+.', status):
630 630 raise util.Abort(status.splitlines()[-1])
631 631 self._ui.status(status)
632 632
633 633 def merge(self, state):
634 634 old = self._state[1]
635 635 new = state[1]
636 636 if new != self._wcrev():
637 637 dirty = old == self._wcrev() or self._wcchanged()[0]
638 638 if _updateprompt(self._ui, self, dirty, self._wcrev(), new):
639 639 self.get(state, False)
640 640
641 641 def push(self, force):
642 642 # push is a no-op for SVN
643 643 return True
644 644
645 645 def files(self):
646 646 output = self._svncommand(['list'])
647 647 # This works because svn forbids \n in filenames.
648 648 return output.splitlines()
649 649
650 650 def filedata(self, name):
651 651 return self._svncommand(['cat'], name)
652 652
653 653
654 654 class gitsubrepo(abstractsubrepo):
655 655 def __init__(self, ctx, path, state):
656 656 # TODO add git version check.
657 657 self._state = state
658 658 self._ctx = ctx
659 659 self._path = path
660 660 self._relpath = os.path.join(reporelpath(ctx._repo), path)
661 661 self._abspath = ctx._repo.wjoin(path)
662 662 self._subparent = ctx._repo
663 663 self._ui = ctx._repo.ui
664 664
665 665 def _gitcommand(self, commands, env=None, stream=False):
666 666 return self._gitdir(commands, env=env, stream=stream)[0]
667 667
668 668 def _gitdir(self, commands, env=None, stream=False):
669 669 return self._gitnodir(commands, env=env, stream=stream,
670 670 cwd=self._abspath)
671 671
672 672 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
673 673 """Calls the git command
674 674
675 675 The methods tries to call the git command. versions previor to 1.6.0
676 676 are not supported and very probably fail.
677 677 """
678 678 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
679 679 # unless ui.quiet is set, print git's stderr,
680 680 # which is mostly progress and useful info
681 681 errpipe = None
682 682 if self._ui.quiet:
683 683 errpipe = open(os.devnull, 'w')
684 684 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
685 685 close_fds=util.closefds,
686 686 stdout=subprocess.PIPE, stderr=errpipe)
687 687 if stream:
688 688 return p.stdout, None
689 689
690 690 retdata = p.stdout.read().strip()
691 691 # wait for the child to exit to avoid race condition.
692 692 p.wait()
693 693
694 694 if p.returncode != 0 and p.returncode != 1:
695 695 # there are certain error codes that are ok
696 696 command = commands[0]
697 697 if command in ('cat-file', 'symbolic-ref'):
698 698 return retdata, p.returncode
699 699 # for all others, abort
700 700 raise util.Abort('git %s error %d in %s' %
701 701 (command, p.returncode, self._relpath))
702 702
703 703 return retdata, p.returncode
704 704
705 705 def _gitmissing(self):
706 706 return not os.path.exists(os.path.join(self._abspath, '.git'))
707 707
708 708 def _gitstate(self):
709 709 return self._gitcommand(['rev-parse', 'HEAD'])
710 710
711 711 def _gitcurrentbranch(self):
712 712 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
713 713 if err:
714 714 current = None
715 715 return current
716 716
717 def _gitremote(self, remote):
718 out = self._gitcommand(['remote', 'show', '-n', remote])
719 line = out.split('\n')[1]
720 i = line.index('URL: ') + len('URL: ')
721 return line[i:]
722
717 723 def _githavelocally(self, revision):
718 724 out, code = self._gitdir(['cat-file', '-e', revision])
719 725 return code == 0
720 726
721 727 def _gitisancestor(self, r1, r2):
722 728 base = self._gitcommand(['merge-base', r1, r2])
723 729 return base == r1
724 730
725 731 def _gitbranchmap(self):
726 732 '''returns 2 things:
727 733 a map from git branch to revision
728 734 a map from revision to branches'''
729 735 branch2rev = {}
730 736 rev2branch = {}
731 737
732 738 out = self._gitcommand(['for-each-ref', '--format',
733 739 '%(objectname) %(refname)'])
734 740 for line in out.split('\n'):
735 741 revision, ref = line.split(' ')
736 742 if (not ref.startswith('refs/heads/') and
737 743 not ref.startswith('refs/remotes/')):
738 744 continue
739 745 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
740 746 continue # ignore remote/HEAD redirects
741 747 branch2rev[ref] = revision
742 748 rev2branch.setdefault(revision, []).append(ref)
743 749 return branch2rev, rev2branch
744 750
745 751 def _gittracking(self, branches):
746 752 'return map of remote branch to local tracking branch'
747 753 # assumes no more than one local tracking branch for each remote
748 754 tracking = {}
749 755 for b in branches:
750 756 if b.startswith('refs/remotes/'):
751 757 continue
752 758 remote = self._gitcommand(['config', 'branch.%s.remote' % b])
753 759 if remote:
754 760 ref = self._gitcommand(['config', 'branch.%s.merge' % b])
755 761 tracking['refs/remotes/%s/%s' %
756 762 (remote, ref.split('/', 2)[2])] = b
757 763 return tracking
758 764
759 765 def _abssource(self, source):
760 766 self._subsource = source
761 767 return _abssource(self)
762 768
763 769 def _fetch(self, source, revision):
764 770 if self._gitmissing():
765 771 source = self._abssource(source)
766 772 self._ui.status(_('cloning subrepo %s from %s\n') %
767 773 (self._relpath, source))
768 774 self._gitnodir(['clone', source, self._abspath])
769 775 if self._githavelocally(revision):
770 776 return
771 self._ui.status(_('pulling subrepo %s\n') % self._relpath)
777 self._ui.status(_('pulling subrepo %s from %s\n') %
778 (self._relpath, self._gitremote('origin')))
772 779 # try only origin: the originally cloned repo
773 780 self._gitcommand(['fetch'])
774 781 if not self._githavelocally(revision):
775 782 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
776 783 (revision, self._relpath))
777 784
778 785 def dirty(self, ignoreupdate=False):
779 786 if self._gitmissing():
780 787 return True
781 788 if not ignoreupdate and self._state[1] != self._gitstate():
782 789 # different version checked out
783 790 return True
784 791 # check for staged changes or modified files; ignore untracked files
785 792 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
786 793 return code == 1
787 794
788 795 def get(self, state, overwrite=False):
789 796 source, revision, kind = state
790 797 self._fetch(source, revision)
791 798 # if the repo was set to be bare, unbare it
792 799 if self._gitcommand(['config', '--bool', 'core.bare']) == 'true':
793 800 self._gitcommand(['config', 'core.bare', 'false'])
794 801 if self._gitstate() == revision:
795 802 self._gitcommand(['reset', '--hard', 'HEAD'])
796 803 return
797 804 elif self._gitstate() == revision:
798 805 if overwrite:
799 806 # first reset the index to unmark new files for commit, because
800 807 # reset --hard will otherwise throw away files added for commit,
801 808 # not just unmark them.
802 809 self._gitcommand(['reset', 'HEAD'])
803 810 self._gitcommand(['reset', '--hard', 'HEAD'])
804 811 return
805 812 branch2rev, rev2branch = self._gitbranchmap()
806 813
807 814 def checkout(args):
808 815 cmd = ['checkout']
809 816 if overwrite:
810 817 # first reset the index to unmark new files for commit, because
811 818 # the -f option will otherwise throw away files added for
812 819 # commit, not just unmark them.
813 820 self._gitcommand(['reset', 'HEAD'])
814 821 cmd.append('-f')
815 822 self._gitcommand(cmd + args)
816 823
817 824 def rawcheckout():
818 825 # no branch to checkout, check it out with no branch
819 826 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
820 827 self._relpath)
821 828 self._ui.warn(_('check out a git branch if you intend '
822 829 'to make changes\n'))
823 830 checkout(['-q', revision])
824 831
825 832 if revision not in rev2branch:
826 833 rawcheckout()
827 834 return
828 835 branches = rev2branch[revision]
829 836 firstlocalbranch = None
830 837 for b in branches:
831 838 if b == 'refs/heads/master':
832 839 # master trumps all other branches
833 840 checkout(['refs/heads/master'])
834 841 return
835 842 if not firstlocalbranch and not b.startswith('refs/remotes/'):
836 843 firstlocalbranch = b
837 844 if firstlocalbranch:
838 845 checkout([firstlocalbranch])
839 846 return
840 847
841 848 tracking = self._gittracking(branch2rev.keys())
842 849 # choose a remote branch already tracked if possible
843 850 remote = branches[0]
844 851 if remote not in tracking:
845 852 for b in branches:
846 853 if b in tracking:
847 854 remote = b
848 855 break
849 856
850 857 if remote not in tracking:
851 858 # create a new local tracking branch
852 859 local = remote.split('/', 2)[2]
853 860 checkout(['-b', local, remote])
854 861 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
855 862 # When updating to a tracked remote branch,
856 863 # if the local tracking branch is downstream of it,
857 864 # a normal `git pull` would have performed a "fast-forward merge"
858 865 # which is equivalent to updating the local branch to the remote.
859 866 # Since we are only looking at branching at update, we need to
860 867 # detect this situation and perform this action lazily.
861 868 if tracking[remote] != self._gitcurrentbranch():
862 869 checkout([tracking[remote]])
863 870 self._gitcommand(['merge', '--ff', remote])
864 871 else:
865 872 # a real merge would be required, just checkout the revision
866 873 rawcheckout()
867 874
868 875 def commit(self, text, user, date):
869 876 if self._gitmissing():
870 877 raise util.Abort(_("subrepo %s is missing") % self._relpath)
871 878 cmd = ['commit', '-a', '-m', text]
872 879 env = os.environ.copy()
873 880 if user:
874 881 cmd += ['--author', user]
875 882 if date:
876 883 # git's date parser silently ignores when seconds < 1e9
877 884 # convert to ISO8601
878 885 env['GIT_AUTHOR_DATE'] = util.datestr(date,
879 886 '%Y-%m-%dT%H:%M:%S %1%2')
880 887 self._gitcommand(cmd, env=env)
881 888 # make sure commit works otherwise HEAD might not exist under certain
882 889 # circumstances
883 890 return self._gitstate()
884 891
885 892 def merge(self, state):
886 893 source, revision, kind = state
887 894 self._fetch(source, revision)
888 895 base = self._gitcommand(['merge-base', revision, self._state[1]])
889 896 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
890 897
891 898 def mergefunc():
892 899 if base == revision:
893 900 self.get(state) # fast forward merge
894 901 elif base != self._state[1]:
895 902 self._gitcommand(['merge', '--no-commit', revision])
896 903
897 904 if self.dirty():
898 905 if self._gitstate() != revision:
899 906 dirty = self._gitstate() == self._state[1] or code != 0
900 907 if _updateprompt(self._ui, self, dirty,
901 908 self._state[1][:7], revision[:7]):
902 909 mergefunc()
903 910 else:
904 911 mergefunc()
905 912
906 913 def push(self, force):
907 914 if self._gitmissing():
908 915 raise util.Abort(_("subrepo %s is missing") % self._relpath)
909 916 # if a branch in origin contains the revision, nothing to do
910 917 branch2rev, rev2branch = self._gitbranchmap()
911 918 if self._state[1] in rev2branch:
912 919 for b in rev2branch[self._state[1]]:
913 920 if b.startswith('refs/remotes/origin/'):
914 921 return True
915 922 for b, revision in branch2rev.iteritems():
916 923 if b.startswith('refs/remotes/origin/'):
917 924 if self._gitisancestor(self._state[1], revision):
918 925 return True
919 926 # otherwise, try to push the currently checked out branch
920 927 cmd = ['push']
921 928 if force:
922 929 cmd.append('--force')
923 930
924 931 current = self._gitcurrentbranch()
925 932 if current:
926 933 # determine if the current branch is even useful
927 934 if not self._gitisancestor(self._state[1], current):
928 935 self._ui.warn(_('unrelated git branch checked out '
929 936 'in subrepo %s\n') % self._relpath)
930 937 return False
931 938 self._ui.status(_('pushing branch %s of subrepo %s\n') %
932 939 (current.split('/', 2)[2], self._relpath))
933 940 self._gitcommand(cmd + ['origin', current])
934 941 return True
935 942 else:
936 943 self._ui.warn(_('no branch checked out in subrepo %s\n'
937 944 'cannot push revision %s') %
938 945 (self._relpath, self._state[1]))
939 946 return False
940 947
941 948 def remove(self):
942 949 if self._gitmissing():
943 950 return
944 951 if self.dirty():
945 952 self._ui.warn(_('not removing repo %s because '
946 953 'it has changes.\n') % self._relpath)
947 954 return
948 955 # we can't fully delete the repository as it may contain
949 956 # local-only history
950 957 self._ui.note(_('removing subrepo %s\n') % self._relpath)
951 958 self._gitcommand(['config', 'core.bare', 'true'])
952 959 for f in os.listdir(self._abspath):
953 960 if f == '.git':
954 961 continue
955 962 path = os.path.join(self._abspath, f)
956 963 if os.path.isdir(path) and not os.path.islink(path):
957 964 shutil.rmtree(path)
958 965 else:
959 966 os.remove(path)
960 967
961 968 def archive(self, ui, archiver, prefix):
962 969 source, revision = self._state
963 970 self._fetch(source, revision)
964 971
965 972 # Parse git's native archive command.
966 973 # This should be much faster than manually traversing the trees
967 974 # and objects with many subprocess calls.
968 975 tarstream = self._gitcommand(['archive', revision], stream=True)
969 976 tar = tarfile.open(fileobj=tarstream, mode='r|')
970 977 relpath = subrelpath(self)
971 978 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
972 979 for i, info in enumerate(tar):
973 980 if info.isdir():
974 981 continue
975 982 if info.issym():
976 983 data = info.linkname
977 984 else:
978 985 data = tar.extractfile(info).read()
979 986 archiver.addfile(os.path.join(prefix, self._path, info.name),
980 987 info.mode, info.issym(), data)
981 988 ui.progress(_('archiving (%s)') % relpath, i + 1,
982 989 unit=_('files'))
983 990 ui.progress(_('archiving (%s)') % relpath, None)
984 991
985 992
986 993 def status(self, rev2, **opts):
987 994 if self._gitmissing():
988 995 # if the repo is missing, return no results
989 996 return [], [], [], [], [], [], []
990 997 rev1 = self._state[1]
991 998 modified, added, removed = [], [], []
992 999 if rev2:
993 1000 command = ['diff-tree', rev1, rev2]
994 1001 else:
995 1002 command = ['diff-index', rev1]
996 1003 out = self._gitcommand(command)
997 1004 for line in out.split('\n'):
998 1005 tab = line.find('\t')
999 1006 if tab == -1:
1000 1007 continue
1001 1008 status, f = line[tab - 1], line[tab + 1:]
1002 1009 if status == 'M':
1003 1010 modified.append(f)
1004 1011 elif status == 'A':
1005 1012 added.append(f)
1006 1013 elif status == 'D':
1007 1014 removed.append(f)
1008 1015
1009 1016 deleted = unknown = ignored = clean = []
1010 1017 return modified, added, removed, deleted, unknown, ignored, clean
1011 1018
1012 1019 types = {
1013 1020 'hg': hgsubrepo,
1014 1021 'svn': svnsubrepo,
1015 1022 'git': gitsubrepo,
1016 1023 }
@@ -1,474 +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 from $TESTTMP/gitroot
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 from $TESTTMP/gitroot
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 from $TESTTMP/gitroot
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 pulling subrepo s
158 pulling subrepo s from $TESTTMP/gitroot
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 from $TESTTMP/gitroot
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 pulling subrepo s
235 pulling subrepo s from $TESTTMP/gitroot
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 pulling subrepo s
265 pulling subrepo s from $TESTTMP/gitroot
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 from $TESTTMP/gitroot
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 from $TESTTMP/gitroot
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 from $TESTTMP/gitroot
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 337 Check hg update --clean
338 338 $ cd $TESTTMP/ta
339 339 $ echo > s/g
340 340 $ cd s
341 341 $ echo c1 > f1
342 342 $ echo c1 > f2
343 343 $ git add f1
344 344 $ cd ..
345 345 $ hg status -S
346 346 M s/g
347 347 A s/f1
348 348 $ ls s
349 349 f
350 350 f1
351 351 f2
352 352 g
353 353 $ hg update --clean
354 354 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
355 355 $ hg status -S
356 356 $ ls s
357 357 f
358 358 f1
359 359 f2
360 360 g
361 361
362 362 Sticky subrepositories, no changes
363 363 $ cd $TESTTMP/ta
364 364 $ hg id -n
365 365 7
366 366 $ cd s
367 367 $ git rev-parse HEAD
368 368 32a343883b74769118bb1d3b4b1fbf9156f4dddc
369 369 $ cd ..
370 370 $ hg update 1 > /dev/null 2>&1
371 371 $ hg id -n
372 372 1
373 373 $ cd s
374 374 $ git rev-parse HEAD
375 375 da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
376 376 $ cd ..
377 377
378 378 Sticky subrepositorys, file changes
379 379 $ touch s/f1
380 380 $ cd s
381 381 $ git add f1
382 382 $ cd ..
383 383 $ hg id -n
384 384 1
385 385 $ cd s
386 386 $ git rev-parse HEAD
387 387 da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
388 388 $ cd ..
389 389 $ hg update 4
390 390 subrepository sources for s differ
391 391 use (l)ocal source (da5f5b1) or (r)emote source (aa84837)?
392 392 l
393 393 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
394 394 $ hg id -n
395 395 4+
396 396 $ cd s
397 397 $ git rev-parse HEAD
398 398 da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
399 399 $ cd ..
400 400 $ hg update --clean tip > /dev/null 2>&1
401 401
402 402 Sticky subrepository, revision updates
403 403 $ hg id -n
404 404 7
405 405 $ cd s
406 406 $ git rev-parse HEAD
407 407 32a343883b74769118bb1d3b4b1fbf9156f4dddc
408 408 $ cd ..
409 409 $ cd s
410 410 $ git checkout aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
411 411 Previous HEAD position was 32a3438... fff
412 412 HEAD is now at aa84837... f
413 413 $ cd ..
414 414 $ hg update 1
415 415 subrepository sources for s differ (in checked out version)
416 416 use (l)ocal source (32a3438) or (r)emote source (da5f5b1)?
417 417 l
418 418 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
419 419 $ hg id -n
420 420 1+
421 421 $ cd s
422 422 $ git rev-parse HEAD
423 423 aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
424 424 $ cd ..
425 425
426 426 Sticky subrepository, file changes and revision updates
427 427 $ touch s/f1
428 428 $ cd s
429 429 $ git add f1
430 430 $ git rev-parse HEAD
431 431 aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
432 432 $ cd ..
433 433 $ hg id -n
434 434 1+
435 435 $ hg update 7
436 436 subrepository sources for s differ
437 437 use (l)ocal source (32a3438) or (r)emote source (32a3438)?
438 438 l
439 439 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
440 440 $ hg id -n
441 441 7
442 442 $ cd s
443 443 $ git rev-parse HEAD
444 444 aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
445 445 $ cd ..
446 446
447 447 Sticky repository, update --clean
448 448 $ hg update --clean tip
449 449 Previous HEAD position was aa84837... f
450 450 HEAD is now at 32a3438... fff
451 451 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
452 452 $ hg id -n
453 453 7
454 454 $ cd s
455 455 $ git rev-parse HEAD
456 456 32a343883b74769118bb1d3b4b1fbf9156f4dddc
457 457 $ cd ..
458 458
459 459 Test subrepo already at intended revision:
460 460 $ cd s
461 461 $ git checkout 32a343883b74769118bb1d3b4b1fbf9156f4dddc
462 462 HEAD is now at 32a3438... fff
463 463 $ cd ..
464 464 $ hg update 1
465 465 Previous HEAD position was 32a3438... fff
466 466 HEAD is now at da5f5b1... g
467 467 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
468 468 $ hg id -n
469 469 1
470 470 $ cd s
471 471 $ git rev-parse HEAD
472 472 da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
473 473 $ cd ..
474 474
General Comments 0
You need to be logged in to leave comments. Login now