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