##// END OF EJS Templates
subrepos: print short git changeset ids...
Kevin Bullock -
r13428:5ef29e0d stable
parent child Browse files
Show More
@@ -1,997 +1,997 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._ui = ctx._repo.ui
663 663
664 664 def _gitcommand(self, commands, env=None, stream=False):
665 665 return self._gitdir(commands, env=env, stream=stream)[0]
666 666
667 667 def _gitdir(self, commands, env=None, stream=False):
668 668 return self._gitnodir(commands, env=env, stream=stream,
669 669 cwd=self._abspath)
670 670
671 671 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
672 672 """Calls the git command
673 673
674 674 The methods tries to call the git command. versions previor to 1.6.0
675 675 are not supported and very probably fail.
676 676 """
677 677 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
678 678 # unless ui.quiet is set, print git's stderr,
679 679 # which is mostly progress and useful info
680 680 errpipe = None
681 681 if self._ui.quiet:
682 682 errpipe = open(os.devnull, 'w')
683 683 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
684 684 close_fds=util.closefds,
685 685 stdout=subprocess.PIPE, stderr=errpipe)
686 686 if stream:
687 687 return p.stdout, None
688 688
689 689 retdata = p.stdout.read().strip()
690 690 # wait for the child to exit to avoid race condition.
691 691 p.wait()
692 692
693 693 if p.returncode != 0 and p.returncode != 1:
694 694 # there are certain error codes that are ok
695 695 command = commands[0]
696 696 if command in ('cat-file', 'symbolic-ref'):
697 697 return retdata, p.returncode
698 698 # for all others, abort
699 699 raise util.Abort('git %s error %d in %s' %
700 700 (command, p.returncode, self._relpath))
701 701
702 702 return retdata, p.returncode
703 703
704 704 def _gitstate(self):
705 705 return self._gitcommand(['rev-parse', 'HEAD'])
706 706
707 707 def _gitcurrentbranch(self):
708 708 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
709 709 if err:
710 710 current = None
711 711 return current
712 712
713 713 def _githavelocally(self, revision):
714 714 out, code = self._gitdir(['cat-file', '-e', revision])
715 715 return code == 0
716 716
717 717 def _gitisancestor(self, r1, r2):
718 718 base = self._gitcommand(['merge-base', r1, r2])
719 719 return base == r1
720 720
721 721 def _gitbranchmap(self):
722 722 '''returns 2 things:
723 723 a map from git branch to revision
724 724 a map from revision to branches'''
725 725 branch2rev = {}
726 726 rev2branch = {}
727 727
728 728 out = self._gitcommand(['for-each-ref', '--format',
729 729 '%(objectname) %(refname)'])
730 730 for line in out.split('\n'):
731 731 revision, ref = line.split(' ')
732 732 if ref.startswith('refs/tags/'):
733 733 continue
734 734 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
735 735 continue # ignore remote/HEAD redirects
736 736 branch2rev[ref] = revision
737 737 rev2branch.setdefault(revision, []).append(ref)
738 738 return branch2rev, rev2branch
739 739
740 740 def _gittracking(self, branches):
741 741 'return map of remote branch to local tracking branch'
742 742 # assumes no more than one local tracking branch for each remote
743 743 tracking = {}
744 744 for b in branches:
745 745 if b.startswith('refs/remotes/'):
746 746 continue
747 747 remote = self._gitcommand(['config', 'branch.%s.remote' % b])
748 748 if remote:
749 749 ref = self._gitcommand(['config', 'branch.%s.merge' % b])
750 750 tracking['refs/remotes/%s/%s' %
751 751 (remote, ref.split('/', 2)[2])] = b
752 752 return tracking
753 753
754 754 def _fetch(self, source, revision):
755 755 if not os.path.exists(os.path.join(self._abspath, '.git')):
756 756 self._ui.status(_('cloning subrepo %s\n') % self._relpath)
757 757 self._gitnodir(['clone', source, self._abspath])
758 758 if self._githavelocally(revision):
759 759 return
760 760 self._ui.status(_('pulling subrepo %s\n') % self._relpath)
761 761 # first try from origin
762 762 self._gitcommand(['fetch'])
763 763 if self._githavelocally(revision):
764 764 return
765 765 # then try from known subrepo source
766 766 self._gitcommand(['fetch', source])
767 767 if not self._githavelocally(revision):
768 768 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
769 769 (revision, self._relpath))
770 770
771 771 def dirty(self, ignoreupdate=False):
772 772 if not ignoreupdate and self._state[1] != self._gitstate():
773 773 # different version checked out
774 774 return True
775 775 # check for staged changes or modified files; ignore untracked files
776 776 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
777 777 return code == 1
778 778
779 779 def get(self, state, overwrite=False):
780 780 source, revision, kind = state
781 781 self._fetch(source, revision)
782 782 # if the repo was set to be bare, unbare it
783 783 if self._gitcommand(['config', '--bool', 'core.bare']) == 'true':
784 784 self._gitcommand(['config', 'core.bare', 'false'])
785 785 if self._gitstate() == revision:
786 786 self._gitcommand(['reset', '--hard', 'HEAD'])
787 787 return
788 788 elif self._gitstate() == revision:
789 789 if overwrite:
790 790 # first reset the index to unmark new files for commit, because
791 791 # reset --hard will otherwise throw away files added for commit,
792 792 # not just unmark them.
793 793 self._gitcommand(['reset', 'HEAD'])
794 794 self._gitcommand(['reset', '--hard', 'HEAD'])
795 795 return
796 796 branch2rev, rev2branch = self._gitbranchmap()
797 797
798 798 def checkout(args):
799 799 cmd = ['checkout']
800 800 if overwrite:
801 801 # first reset the index to unmark new files for commit, because
802 802 # the -f option will otherwise throw away files added for
803 803 # commit, not just unmark them.
804 804 self._gitcommand(['reset', 'HEAD'])
805 805 cmd.append('-f')
806 806 self._gitcommand(cmd + args)
807 807
808 808 def rawcheckout():
809 809 # no branch to checkout, check it out with no branch
810 810 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
811 811 self._relpath)
812 812 self._ui.warn(_('check out a git branch if you intend '
813 813 'to make changes\n'))
814 814 checkout(['-q', revision])
815 815
816 816 if revision not in rev2branch:
817 817 rawcheckout()
818 818 return
819 819 branches = rev2branch[revision]
820 820 firstlocalbranch = None
821 821 for b in branches:
822 822 if b == 'refs/heads/master':
823 823 # master trumps all other branches
824 824 checkout(['refs/heads/master'])
825 825 return
826 826 if not firstlocalbranch and not b.startswith('refs/remotes/'):
827 827 firstlocalbranch = b
828 828 if firstlocalbranch:
829 829 checkout([firstlocalbranch])
830 830 return
831 831
832 832 tracking = self._gittracking(branch2rev.keys())
833 833 # choose a remote branch already tracked if possible
834 834 remote = branches[0]
835 835 if remote not in tracking:
836 836 for b in branches:
837 837 if b in tracking:
838 838 remote = b
839 839 break
840 840
841 841 if remote not in tracking:
842 842 # create a new local tracking branch
843 843 local = remote.split('/', 2)[2]
844 844 checkout(['-b', local, remote])
845 845 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
846 846 # When updating to a tracked remote branch,
847 847 # if the local tracking branch is downstream of it,
848 848 # a normal `git pull` would have performed a "fast-forward merge"
849 849 # which is equivalent to updating the local branch to the remote.
850 850 # Since we are only looking at branching at update, we need to
851 851 # detect this situation and perform this action lazily.
852 852 if tracking[remote] != self._gitcurrentbranch():
853 853 checkout([tracking[remote]])
854 854 self._gitcommand(['merge', '--ff', remote])
855 855 else:
856 856 # a real merge would be required, just checkout the revision
857 857 rawcheckout()
858 858
859 859 def commit(self, text, user, date):
860 860 cmd = ['commit', '-a', '-m', text]
861 861 env = os.environ.copy()
862 862 if user:
863 863 cmd += ['--author', user]
864 864 if date:
865 865 # git's date parser silently ignores when seconds < 1e9
866 866 # convert to ISO8601
867 867 env['GIT_AUTHOR_DATE'] = util.datestr(date,
868 868 '%Y-%m-%dT%H:%M:%S %1%2')
869 869 self._gitcommand(cmd, env=env)
870 870 # make sure commit works otherwise HEAD might not exist under certain
871 871 # circumstances
872 872 return self._gitstate()
873 873
874 874 def merge(self, state):
875 875 source, revision, kind = state
876 876 self._fetch(source, revision)
877 877 base = self._gitcommand(['merge-base', revision, self._state[1]])
878 878 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
879 879
880 880 def mergefunc():
881 881 if base == revision:
882 882 self.get(state) # fast forward merge
883 883 elif base != self._state[1]:
884 884 self._gitcommand(['merge', '--no-commit', revision])
885 885
886 886 if self.dirty():
887 887 if self._gitstate() != revision:
888 888 dirty = self._gitstate() == self._state[1] or code != 0
889 if _updateprompt(self._ui, self, dirty, self._state[1], revision):
889 if _updateprompt(self._ui, self, dirty, self._state[1][:7], revision[:7]):
890 890 mergefunc()
891 891 else:
892 892 mergefunc()
893 893
894 894 def push(self, force):
895 895 # if a branch in origin contains the revision, nothing to do
896 896 branch2rev, rev2branch = self._gitbranchmap()
897 897 if self._state[1] in rev2branch:
898 898 for b in rev2branch[self._state[1]]:
899 899 if b.startswith('refs/remotes/origin/'):
900 900 return True
901 901 for b, revision in branch2rev.iteritems():
902 902 if b.startswith('refs/remotes/origin/'):
903 903 if self._gitisancestor(self._state[1], revision):
904 904 return True
905 905 # otherwise, try to push the currently checked out branch
906 906 cmd = ['push']
907 907 if force:
908 908 cmd.append('--force')
909 909
910 910 current = self._gitcurrentbranch()
911 911 if current:
912 912 # determine if the current branch is even useful
913 913 if not self._gitisancestor(self._state[1], current):
914 914 self._ui.warn(_('unrelated git branch checked out '
915 915 'in subrepo %s\n') % self._relpath)
916 916 return False
917 917 self._ui.status(_('pushing branch %s of subrepo %s\n') %
918 918 (current.split('/', 2)[2], self._relpath))
919 919 self._gitcommand(cmd + ['origin', current])
920 920 return True
921 921 else:
922 922 self._ui.warn(_('no branch checked out in subrepo %s\n'
923 923 'cannot push revision %s') %
924 924 (self._relpath, self._state[1]))
925 925 return False
926 926
927 927 def remove(self):
928 928 if self.dirty():
929 929 self._ui.warn(_('not removing repo %s because '
930 930 'it has changes.\n') % self._relpath)
931 931 return
932 932 # we can't fully delete the repository as it may contain
933 933 # local-only history
934 934 self._ui.note(_('removing subrepo %s\n') % self._relpath)
935 935 self._gitcommand(['config', 'core.bare', 'true'])
936 936 for f in os.listdir(self._abspath):
937 937 if f == '.git':
938 938 continue
939 939 path = os.path.join(self._abspath, f)
940 940 if os.path.isdir(path) and not os.path.islink(path):
941 941 shutil.rmtree(path)
942 942 else:
943 943 os.remove(path)
944 944
945 945 def archive(self, ui, archiver, prefix):
946 946 source, revision = self._state
947 947 self._fetch(source, revision)
948 948
949 949 # Parse git's native archive command.
950 950 # This should be much faster than manually traversing the trees
951 951 # and objects with many subprocess calls.
952 952 tarstream = self._gitcommand(['archive', revision], stream=True)
953 953 tar = tarfile.open(fileobj=tarstream, mode='r|')
954 954 relpath = subrelpath(self)
955 955 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
956 956 for i, info in enumerate(tar):
957 957 if info.isdir():
958 958 continue
959 959 if info.issym():
960 960 data = info.linkname
961 961 else:
962 962 data = tar.extractfile(info).read()
963 963 archiver.addfile(os.path.join(prefix, self._path, info.name),
964 964 info.mode, info.issym(), data)
965 965 ui.progress(_('archiving (%s)') % relpath, i + 1,
966 966 unit=_('files'))
967 967 ui.progress(_('archiving (%s)') % relpath, None)
968 968
969 969
970 970 def status(self, rev2, **opts):
971 971 rev1 = self._state[1]
972 972 modified, added, removed = [], [], []
973 973 if rev2:
974 974 command = ['diff-tree', rev1, rev2]
975 975 else:
976 976 command = ['diff-index', rev1]
977 977 out = self._gitcommand(command)
978 978 for line in out.split('\n'):
979 979 tab = line.find('\t')
980 980 if tab == -1:
981 981 continue
982 982 status, f = line[tab - 1], line[tab + 1:]
983 983 if status == 'M':
984 984 modified.append(f)
985 985 elif status == 'A':
986 986 added.append(f)
987 987 elif status == 'D':
988 988 removed.append(f)
989 989
990 990 deleted = unknown = ignored = clean = []
991 991 return modified, added, removed, deleted, unknown, ignored, clean
992 992
993 993 types = {
994 994 'hg': hgsubrepo,
995 995 'svn': svnsubrepo,
996 996 'git': gitsubrepo,
997 997 }
@@ -1,445 +1,445 b''
1 1 $ "$TESTDIR/hghave" git || exit 80
2 2
3 3 make git commits repeatable
4 4
5 5 $ GIT_AUTHOR_NAME='test'; export GIT_AUTHOR_NAME
6 6 $ GIT_AUTHOR_EMAIL='test@example.org'; export GIT_AUTHOR_EMAIL
7 7 $ GIT_AUTHOR_DATE='1234567891 +0000'; export GIT_AUTHOR_DATE
8 8 $ GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"; export GIT_COMMITTER_NAME
9 9 $ GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"; export GIT_COMMITTER_EMAIL
10 10 $ GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"; export GIT_COMMITTER_DATE
11 11
12 12 root hg repo
13 13
14 14 $ hg init t
15 15 $ cd t
16 16 $ echo a > a
17 17 $ hg add a
18 18 $ hg commit -m a
19 19 $ cd ..
20 20
21 21 new external git repo
22 22
23 23 $ mkdir gitroot
24 24 $ cd gitroot
25 25 $ git init -q
26 26 $ echo g > g
27 27 $ git add g
28 28 $ git commit -q -m g
29 29
30 30 add subrepo clone
31 31
32 32 $ cd ../t
33 33 $ echo 's = [git]../gitroot' > .hgsub
34 34 $ git clone -q ../gitroot s
35 35 $ hg add .hgsub
36 36 $ hg commit -m 'new git subrepo'
37 37 committing subrepository s
38 38 $ hg debugsub
39 39 path s
40 40 source ../gitroot
41 41 revision da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
42 42
43 43 record a new commit from upstream from a different branch
44 44
45 45 $ cd ../gitroot
46 46 $ git checkout -q -b testing
47 47 $ echo gg >> g
48 48 $ git commit -q -a -m gg
49 49
50 50 $ cd ../t/s
51 51 $ git pull -q >/dev/null 2>/dev/null
52 52 $ git checkout -q -b testing origin/testing >/dev/null
53 53
54 54 $ cd ..
55 55 $ hg status --subrepos
56 56 M s/g
57 57 $ hg commit -m 'update git subrepo'
58 58 committing subrepository s
59 59 $ hg debugsub
60 60 path s
61 61 source ../gitroot
62 62 revision 126f2a14290cd5ce061fdedc430170e8d39e1c5a
63 63
64 64 make $GITROOT pushable, by replacing it with a clone with nothing checked out
65 65
66 66 $ cd ..
67 67 $ git clone gitroot gitrootbare --bare -q
68 68 $ rm -rf gitroot
69 69 $ mv gitrootbare gitroot
70 70
71 71 clone root
72 72
73 73 $ cd t
74 74 $ hg clone . ../tc
75 75 updating to branch default
76 76 cloning subrepo s
77 77 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
78 78 $ cd ../tc
79 79 $ hg debugsub
80 80 path s
81 81 source ../gitroot
82 82 revision 126f2a14290cd5ce061fdedc430170e8d39e1c5a
83 83
84 84 update to previous substate
85 85
86 86 $ hg update 1 -q
87 87 $ cat s/g
88 88 g
89 89 $ hg debugsub
90 90 path s
91 91 source ../gitroot
92 92 revision da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
93 93
94 94 clone root, make local change
95 95
96 96 $ cd ../t
97 97 $ hg clone . ../ta
98 98 updating to branch default
99 99 cloning subrepo s
100 100 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
101 101
102 102 $ cd ../ta
103 103 $ echo ggg >> s/g
104 104 $ hg status --subrepos
105 105 M s/g
106 106 $ hg commit -m ggg
107 107 committing subrepository s
108 108 $ hg debugsub
109 109 path s
110 110 source ../gitroot
111 111 revision 79695940086840c99328513acbe35f90fcd55e57
112 112
113 113 clone root separately, make different local change
114 114
115 115 $ cd ../t
116 116 $ hg clone . ../tb
117 117 updating to branch default
118 118 cloning subrepo s
119 119 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
120 120
121 121 $ cd ../tb/s
122 122 $ echo f > f
123 123 $ git add f
124 124 $ cd ..
125 125
126 126 $ hg status --subrepos
127 127 A s/f
128 128 $ hg commit -m f
129 129 committing subrepository s
130 130 $ hg debugsub
131 131 path s
132 132 source ../gitroot
133 133 revision aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
134 134
135 135 user b push changes
136 136
137 137 $ hg push 2>/dev/null
138 138 pushing to $TESTTMP/t
139 139 pushing branch testing of subrepo s
140 140 searching for changes
141 141 adding changesets
142 142 adding manifests
143 143 adding file changes
144 144 added 1 changesets with 1 changes to 1 files
145 145
146 146 user a pulls, merges, commits
147 147
148 148 $ cd ../ta
149 149 $ hg pull
150 150 pulling from $TESTTMP/t
151 151 searching for changes
152 152 adding changesets
153 153 adding manifests
154 154 adding file changes
155 155 added 1 changesets with 1 changes to 1 files (+1 heads)
156 156 (run 'hg heads' to see heads, 'hg merge' to merge)
157 157 $ hg merge 2>/dev/null
158 158 pulling subrepo s
159 159 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
160 160 (branch merge, don't forget to commit)
161 161 $ cat s/f
162 162 f
163 163 $ cat s/g
164 164 g
165 165 gg
166 166 ggg
167 167 $ hg commit -m 'merge'
168 168 committing subrepository s
169 169 $ hg status --subrepos --rev 1:5
170 170 M .hgsubstate
171 171 M s/g
172 172 A s/f
173 173 $ hg debugsub
174 174 path s
175 175 source ../gitroot
176 176 revision f47b465e1bce645dbf37232a00574aa1546ca8d3
177 177 $ hg push 2>/dev/null
178 178 pushing to $TESTTMP/t
179 179 pushing branch testing of subrepo s
180 180 searching for changes
181 181 adding changesets
182 182 adding manifests
183 183 adding file changes
184 184 added 2 changesets with 2 changes to 1 files
185 185
186 186 make upstream git changes
187 187
188 188 $ cd ..
189 189 $ git clone -q gitroot gitclone
190 190 $ cd gitclone
191 191 $ echo ff >> f
192 192 $ git commit -q -a -m ff
193 193 $ echo fff >> f
194 194 $ git commit -q -a -m fff
195 195 $ git push origin testing 2>/dev/null
196 196
197 197 make and push changes to hg without updating the subrepo
198 198
199 199 $ cd ../t
200 200 $ hg clone . ../td
201 201 updating to branch default
202 202 cloning subrepo s
203 203 checking out detached HEAD in subrepo s
204 204 check out a git branch if you intend to make changes
205 205 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
206 206 $ cd ../td
207 207 $ echo aa >> a
208 208 $ hg commit -m aa
209 209 $ hg push
210 210 pushing to $TESTTMP/t
211 211 searching for changes
212 212 adding changesets
213 213 adding manifests
214 214 adding file changes
215 215 added 1 changesets with 1 changes to 1 files
216 216
217 217 sync to upstream git, distribute changes
218 218
219 219 $ cd ../ta
220 220 $ hg pull -u -q
221 221 $ cd s
222 222 $ git pull -q >/dev/null 2>/dev/null
223 223 $ cd ..
224 224 $ hg commit -m 'git upstream sync'
225 225 committing subrepository s
226 226 $ hg debugsub
227 227 path s
228 228 source ../gitroot
229 229 revision 32a343883b74769118bb1d3b4b1fbf9156f4dddc
230 230 $ hg push -q
231 231
232 232 $ cd ../tb
233 233 $ hg pull -q
234 234 $ hg update 2>/dev/null
235 235 pulling subrepo s
236 236 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
237 237 $ hg debugsub
238 238 path s
239 239 source ../gitroot
240 240 revision 32a343883b74769118bb1d3b4b1fbf9156f4dddc
241 241
242 242 update to a revision without the subrepo, keeping the local git repository
243 243
244 244 $ cd ../t
245 245 $ hg up 0
246 246 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
247 247 $ ls -a s
248 248 .
249 249 ..
250 250 .git
251 251
252 252 $ hg up 2
253 253 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
254 254 $ ls -a s
255 255 .
256 256 ..
257 257 .git
258 258 g
259 259
260 260 archive subrepos
261 261
262 262 $ cd ../tc
263 263 $ hg pull -q
264 264 $ hg archive --subrepos -r 5 ../archive 2>/dev/null
265 265 pulling subrepo s
266 266 $ cd ../archive
267 267 $ cat s/f
268 268 f
269 269 $ cat s/g
270 270 g
271 271 gg
272 272 ggg
273 273
274 274 create nested repo
275 275
276 276 $ cd ..
277 277 $ hg init outer
278 278 $ cd outer
279 279 $ echo b>b
280 280 $ hg add b
281 281 $ hg commit -m b
282 282
283 283 $ hg clone ../t inner
284 284 updating to branch default
285 285 cloning subrepo s
286 286 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
287 287 $ echo inner = inner > .hgsub
288 288 $ hg add .hgsub
289 289 $ hg commit -m 'nested sub'
290 290 committing subrepository inner
291 291
292 292 nested commit
293 293
294 294 $ echo ffff >> inner/s/f
295 295 $ hg status --subrepos
296 296 M inner/s/f
297 297 $ hg commit -m nested
298 298 committing subrepository inner
299 299 committing subrepository inner/s
300 300
301 301 nested archive
302 302
303 303 $ hg archive --subrepos ../narchive
304 304 $ ls ../narchive/inner/s | grep -v pax_global_header
305 305 f
306 306 g
307 307
308 308 Check hg update --clean
309 309 $ cd $TESTTMP/ta
310 310 $ echo > s/g
311 311 $ cd s
312 312 $ echo c1 > f1
313 313 $ echo c1 > f2
314 314 $ git add f1
315 315 $ cd ..
316 316 $ hg status -S
317 317 M s/g
318 318 A s/f1
319 319 $ ls s
320 320 f
321 321 f1
322 322 f2
323 323 g
324 324 $ hg update --clean
325 325 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
326 326 $ hg status -S
327 327 $ ls s
328 328 f
329 329 f1
330 330 f2
331 331 g
332 332
333 333 Sticky subrepositories, no changes
334 334 $ cd $TESTTMP/ta
335 335 $ hg id -n
336 336 7
337 337 $ cd s
338 338 $ git rev-parse HEAD
339 339 32a343883b74769118bb1d3b4b1fbf9156f4dddc
340 340 $ cd ..
341 341 $ hg update 1 > /dev/null 2>&1
342 342 $ hg id -n
343 343 1
344 344 $ cd s
345 345 $ git rev-parse HEAD
346 346 da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
347 347 $ cd ..
348 348
349 349 Sticky subrepositorys, file changes
350 350 $ touch s/f1
351 351 $ cd s
352 352 $ git add f1
353 353 $ cd ..
354 354 $ hg id -n
355 355 1
356 356 $ cd s
357 357 $ git rev-parse HEAD
358 358 da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
359 359 $ cd ..
360 360 $ hg update 4
361 361 subrepository sources for s differ
362 use (l)ocal source (da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7) or (r)emote source (aa84837ccfbdfedcdcdeeedc309d73e6eb069edc)?
362 use (l)ocal source (da5f5b1) or (r)emote source (aa84837)?
363 363 l
364 364 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
365 365 $ hg id -n
366 366 4+
367 367 $ cd s
368 368 $ git rev-parse HEAD
369 369 da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
370 370 $ cd ..
371 371 $ hg update --clean tip > /dev/null 2>&1
372 372
373 373 Sticky subrepository, revision updates
374 374 $ hg id -n
375 375 7
376 376 $ cd s
377 377 $ git rev-parse HEAD
378 378 32a343883b74769118bb1d3b4b1fbf9156f4dddc
379 379 $ cd ..
380 380 $ cd s
381 381 $ git checkout aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
382 382 Previous HEAD position was 32a3438... fff
383 383 HEAD is now at aa84837... f
384 384 $ cd ..
385 385 $ hg update 1
386 386 subrepository sources for s differ (in checked out version)
387 use (l)ocal source (32a343883b74769118bb1d3b4b1fbf9156f4dddc) or (r)emote source (da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7)?
387 use (l)ocal source (32a3438) or (r)emote source (da5f5b1)?
388 388 l
389 389 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
390 390 $ hg id -n
391 391 1+
392 392 $ cd s
393 393 $ git rev-parse HEAD
394 394 aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
395 395 $ cd ..
396 396
397 397 Sticky subrepository, file changes and revision updates
398 398 $ touch s/f1
399 399 $ cd s
400 400 $ git add f1
401 401 $ git rev-parse HEAD
402 402 aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
403 403 $ cd ..
404 404 $ hg id -n
405 405 1+
406 406 $ hg update 7
407 407 subrepository sources for s differ
408 use (l)ocal source (32a343883b74769118bb1d3b4b1fbf9156f4dddc) or (r)emote source (32a343883b74769118bb1d3b4b1fbf9156f4dddc)?
408 use (l)ocal source (32a3438) or (r)emote source (32a3438)?
409 409 l
410 410 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
411 411 $ hg id -n
412 412 7
413 413 $ cd s
414 414 $ git rev-parse HEAD
415 415 aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
416 416 $ cd ..
417 417
418 418 Sticky repository, update --clean
419 419 $ hg update --clean tip
420 420 Previous HEAD position was aa84837... f
421 421 HEAD is now at 32a3438... fff
422 422 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
423 423 $ hg id -n
424 424 7
425 425 $ cd s
426 426 $ git rev-parse HEAD
427 427 32a343883b74769118bb1d3b4b1fbf9156f4dddc
428 428 $ cd ..
429 429
430 430 Test subrepo already at intended revision:
431 431 $ cd s
432 432 $ git checkout 32a343883b74769118bb1d3b4b1fbf9156f4dddc
433 433 HEAD is now at 32a3438... fff
434 434 $ cd ..
435 435 $ hg update 1
436 436 Previous HEAD position was 32a3438... fff
437 437 HEAD is now at da5f5b1... g
438 438 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
439 439 $ hg id -n
440 440 1
441 441 $ cd s
442 442 $ git rev-parse HEAD
443 443 da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
444 444 $ cd ..
445 445
General Comments 0
You need to be logged in to leave comments. Login now