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