##// END OF EJS Templates
git-subrepos: revert respects specified location to save .orig files...
Christian Delahousse -
r26940:91786f20 default
parent child Browse files
Show More
@@ -1,1929 +1,1929 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 from __future__ import absolute_import
9 9
10 10 import copy
11 11 import errno
12 12 import os
13 13 import posixpath
14 14 import re
15 15 import stat
16 16 import subprocess
17 17 import sys
18 18 import tarfile
19 19 import xml.dom.minidom
20 20
21 21
22 22 from .i18n import _
23 23 from . import (
24 24 cmdutil,
25 25 config,
26 26 error,
27 27 exchange,
28 28 match as matchmod,
29 29 node,
30 30 pathutil,
31 31 phases,
32 32 scmutil,
33 33 util,
34 34 )
35 35
36 36 hg = None
37 37 propertycache = util.propertycache
38 38
39 39 nullstate = ('', '', 'empty')
40 40
41 41 def _expandedabspath(path):
42 42 '''
43 43 get a path or url and if it is a path expand it and return an absolute path
44 44 '''
45 45 expandedpath = util.urllocalpath(util.expandpath(path))
46 46 u = util.url(expandedpath)
47 47 if not u.scheme:
48 48 path = util.normpath(os.path.abspath(u.path))
49 49 return path
50 50
51 51 def _getstorehashcachename(remotepath):
52 52 '''get a unique filename for the store hash cache of a remote repository'''
53 53 return util.sha1(_expandedabspath(remotepath)).hexdigest()[0:12]
54 54
55 55 class SubrepoAbort(error.Abort):
56 56 """Exception class used to avoid handling a subrepo error more than once"""
57 57 def __init__(self, *args, **kw):
58 58 error.Abort.__init__(self, *args, **kw)
59 59 self.subrepo = kw.get('subrepo')
60 60 self.cause = kw.get('cause')
61 61
62 62 def annotatesubrepoerror(func):
63 63 def decoratedmethod(self, *args, **kargs):
64 64 try:
65 65 res = func(self, *args, **kargs)
66 66 except SubrepoAbort as ex:
67 67 # This exception has already been handled
68 68 raise ex
69 69 except error.Abort as ex:
70 70 subrepo = subrelpath(self)
71 71 errormsg = str(ex) + ' ' + _('(in subrepo %s)') % subrepo
72 72 # avoid handling this exception by raising a SubrepoAbort exception
73 73 raise SubrepoAbort(errormsg, hint=ex.hint, subrepo=subrepo,
74 74 cause=sys.exc_info())
75 75 return res
76 76 return decoratedmethod
77 77
78 78 def state(ctx, ui):
79 79 """return a state dict, mapping subrepo paths configured in .hgsub
80 80 to tuple: (source from .hgsub, revision from .hgsubstate, kind
81 81 (key in types dict))
82 82 """
83 83 p = config.config()
84 84 repo = ctx.repo()
85 85 def read(f, sections=None, remap=None):
86 86 if f in ctx:
87 87 try:
88 88 data = ctx[f].data()
89 89 except IOError as err:
90 90 if err.errno != errno.ENOENT:
91 91 raise
92 92 # handle missing subrepo spec files as removed
93 93 ui.warn(_("warning: subrepo spec file \'%s\' not found\n") %
94 94 repo.pathto(f))
95 95 return
96 96 p.parse(f, data, sections, remap, read)
97 97 else:
98 98 raise error.Abort(_("subrepo spec file \'%s\' not found") %
99 99 repo.pathto(f))
100 100 if '.hgsub' in ctx:
101 101 read('.hgsub')
102 102
103 103 for path, src in ui.configitems('subpaths'):
104 104 p.set('subpaths', path, src, ui.configsource('subpaths', path))
105 105
106 106 rev = {}
107 107 if '.hgsubstate' in ctx:
108 108 try:
109 109 for i, l in enumerate(ctx['.hgsubstate'].data().splitlines()):
110 110 l = l.lstrip()
111 111 if not l:
112 112 continue
113 113 try:
114 114 revision, path = l.split(" ", 1)
115 115 except ValueError:
116 116 raise error.Abort(_("invalid subrepository revision "
117 117 "specifier in \'%s\' line %d")
118 118 % (repo.pathto('.hgsubstate'), (i + 1)))
119 119 rev[path] = revision
120 120 except IOError as err:
121 121 if err.errno != errno.ENOENT:
122 122 raise
123 123
124 124 def remap(src):
125 125 for pattern, repl in p.items('subpaths'):
126 126 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
127 127 # does a string decode.
128 128 repl = repl.encode('string-escape')
129 129 # However, we still want to allow back references to go
130 130 # through unharmed, so we turn r'\\1' into r'\1'. Again,
131 131 # extra escapes are needed because re.sub string decodes.
132 132 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
133 133 try:
134 134 src = re.sub(pattern, repl, src, 1)
135 135 except re.error as e:
136 136 raise error.Abort(_("bad subrepository pattern in %s: %s")
137 137 % (p.source('subpaths', pattern), e))
138 138 return src
139 139
140 140 state = {}
141 141 for path, src in p[''].items():
142 142 kind = 'hg'
143 143 if src.startswith('['):
144 144 if ']' not in src:
145 145 raise error.Abort(_('missing ] in subrepo source'))
146 146 kind, src = src.split(']', 1)
147 147 kind = kind[1:]
148 148 src = src.lstrip() # strip any extra whitespace after ']'
149 149
150 150 if not util.url(src).isabs():
151 151 parent = _abssource(repo, abort=False)
152 152 if parent:
153 153 parent = util.url(parent)
154 154 parent.path = posixpath.join(parent.path or '', src)
155 155 parent.path = posixpath.normpath(parent.path)
156 156 joined = str(parent)
157 157 # Remap the full joined path and use it if it changes,
158 158 # else remap the original source.
159 159 remapped = remap(joined)
160 160 if remapped == joined:
161 161 src = remap(src)
162 162 else:
163 163 src = remapped
164 164
165 165 src = remap(src)
166 166 state[util.pconvert(path)] = (src.strip(), rev.get(path, ''), kind)
167 167
168 168 return state
169 169
170 170 def writestate(repo, state):
171 171 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
172 172 lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state)
173 173 if state[s][1] != nullstate[1]]
174 174 repo.wwrite('.hgsubstate', ''.join(lines), '')
175 175
176 176 def submerge(repo, wctx, mctx, actx, overwrite):
177 177 """delegated from merge.applyupdates: merging of .hgsubstate file
178 178 in working context, merging context and ancestor context"""
179 179 if mctx == actx: # backwards?
180 180 actx = wctx.p1()
181 181 s1 = wctx.substate
182 182 s2 = mctx.substate
183 183 sa = actx.substate
184 184 sm = {}
185 185
186 186 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
187 187
188 188 def debug(s, msg, r=""):
189 189 if r:
190 190 r = "%s:%s:%s" % r
191 191 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
192 192
193 193 for s, l in sorted(s1.iteritems()):
194 194 a = sa.get(s, nullstate)
195 195 ld = l # local state with possible dirty flag for compares
196 196 if wctx.sub(s).dirty():
197 197 ld = (l[0], l[1] + "+")
198 198 if wctx == actx: # overwrite
199 199 a = ld
200 200
201 201 if s in s2:
202 202 r = s2[s]
203 203 if ld == r or r == a: # no change or local is newer
204 204 sm[s] = l
205 205 continue
206 206 elif ld == a: # other side changed
207 207 debug(s, "other changed, get", r)
208 208 wctx.sub(s).get(r, overwrite)
209 209 sm[s] = r
210 210 elif ld[0] != r[0]: # sources differ
211 211 if repo.ui.promptchoice(
212 212 _(' subrepository sources for %s differ\n'
213 213 'use (l)ocal source (%s) or (r)emote source (%s)?'
214 214 '$$ &Local $$ &Remote') % (s, l[0], r[0]), 0):
215 215 debug(s, "prompt changed, get", r)
216 216 wctx.sub(s).get(r, overwrite)
217 217 sm[s] = r
218 218 elif ld[1] == a[1]: # local side is unchanged
219 219 debug(s, "other side changed, get", r)
220 220 wctx.sub(s).get(r, overwrite)
221 221 sm[s] = r
222 222 else:
223 223 debug(s, "both sides changed")
224 224 srepo = wctx.sub(s)
225 225 option = repo.ui.promptchoice(
226 226 _(' subrepository %s diverged (local revision: %s, '
227 227 'remote revision: %s)\n'
228 228 '(M)erge, keep (l)ocal or keep (r)emote?'
229 229 '$$ &Merge $$ &Local $$ &Remote')
230 230 % (s, srepo.shortid(l[1]), srepo.shortid(r[1])), 0)
231 231 if option == 0:
232 232 wctx.sub(s).merge(r)
233 233 sm[s] = l
234 234 debug(s, "merge with", r)
235 235 elif option == 1:
236 236 sm[s] = l
237 237 debug(s, "keep local subrepo revision", l)
238 238 else:
239 239 wctx.sub(s).get(r, overwrite)
240 240 sm[s] = r
241 241 debug(s, "get remote subrepo revision", r)
242 242 elif ld == a: # remote removed, local unchanged
243 243 debug(s, "remote removed, remove")
244 244 wctx.sub(s).remove()
245 245 elif a == nullstate: # not present in remote or ancestor
246 246 debug(s, "local added, keep")
247 247 sm[s] = l
248 248 continue
249 249 else:
250 250 if repo.ui.promptchoice(
251 251 _(' local changed subrepository %s which remote removed\n'
252 252 'use (c)hanged version or (d)elete?'
253 253 '$$ &Changed $$ &Delete') % s, 0):
254 254 debug(s, "prompt remove")
255 255 wctx.sub(s).remove()
256 256
257 257 for s, r in sorted(s2.items()):
258 258 if s in s1:
259 259 continue
260 260 elif s not in sa:
261 261 debug(s, "remote added, get", r)
262 262 mctx.sub(s).get(r)
263 263 sm[s] = r
264 264 elif r != sa[s]:
265 265 if repo.ui.promptchoice(
266 266 _(' remote changed subrepository %s which local removed\n'
267 267 'use (c)hanged version or (d)elete?'
268 268 '$$ &Changed $$ &Delete') % s, 0) == 0:
269 269 debug(s, "prompt recreate", r)
270 270 mctx.sub(s).get(r)
271 271 sm[s] = r
272 272
273 273 # record merged .hgsubstate
274 274 writestate(repo, sm)
275 275 return sm
276 276
277 277 def _updateprompt(ui, sub, dirty, local, remote):
278 278 if dirty:
279 279 msg = (_(' subrepository sources for %s differ\n'
280 280 'use (l)ocal source (%s) or (r)emote source (%s)?'
281 281 '$$ &Local $$ &Remote')
282 282 % (subrelpath(sub), local, remote))
283 283 else:
284 284 msg = (_(' subrepository sources for %s differ (in checked out '
285 285 'version)\n'
286 286 'use (l)ocal source (%s) or (r)emote source (%s)?'
287 287 '$$ &Local $$ &Remote')
288 288 % (subrelpath(sub), local, remote))
289 289 return ui.promptchoice(msg, 0)
290 290
291 291 def reporelpath(repo):
292 292 """return path to this (sub)repo as seen from outermost repo"""
293 293 parent = repo
294 294 while util.safehasattr(parent, '_subparent'):
295 295 parent = parent._subparent
296 296 return repo.root[len(pathutil.normasprefix(parent.root)):]
297 297
298 298 def subrelpath(sub):
299 299 """return path to this subrepo as seen from outermost repo"""
300 300 return sub._relpath
301 301
302 302 def _abssource(repo, push=False, abort=True):
303 303 """return pull/push path of repo - either based on parent repo .hgsub info
304 304 or on the top repo config. Abort or return None if no source found."""
305 305 if util.safehasattr(repo, '_subparent'):
306 306 source = util.url(repo._subsource)
307 307 if source.isabs():
308 308 return str(source)
309 309 source.path = posixpath.normpath(source.path)
310 310 parent = _abssource(repo._subparent, push, abort=False)
311 311 if parent:
312 312 parent = util.url(util.pconvert(parent))
313 313 parent.path = posixpath.join(parent.path or '', source.path)
314 314 parent.path = posixpath.normpath(parent.path)
315 315 return str(parent)
316 316 else: # recursion reached top repo
317 317 if util.safehasattr(repo, '_subtoppath'):
318 318 return repo._subtoppath
319 319 if push and repo.ui.config('paths', 'default-push'):
320 320 return repo.ui.config('paths', 'default-push')
321 321 if repo.ui.config('paths', 'default'):
322 322 return repo.ui.config('paths', 'default')
323 323 if repo.shared():
324 324 # chop off the .hg component to get the default path form
325 325 return os.path.dirname(repo.sharedpath)
326 326 if abort:
327 327 raise error.Abort(_("default path for subrepository not found"))
328 328
329 329 def _sanitize(ui, vfs, ignore):
330 330 for dirname, dirs, names in vfs.walk():
331 331 for i, d in enumerate(dirs):
332 332 if d.lower() == ignore:
333 333 del dirs[i]
334 334 break
335 335 if vfs.basename(dirname).lower() != '.hg':
336 336 continue
337 337 for f in names:
338 338 if f.lower() == 'hgrc':
339 339 ui.warn(_("warning: removing potentially hostile 'hgrc' "
340 340 "in '%s'\n") % vfs.join(dirname))
341 341 vfs.unlink(vfs.reljoin(dirname, f))
342 342
343 343 def subrepo(ctx, path, allowwdir=False):
344 344 """return instance of the right subrepo class for subrepo in path"""
345 345 # subrepo inherently violates our import layering rules
346 346 # because it wants to make repo objects from deep inside the stack
347 347 # so we manually delay the circular imports to not break
348 348 # scripts that don't use our demand-loading
349 349 global hg
350 350 from . import hg as h
351 351 hg = h
352 352
353 353 pathutil.pathauditor(ctx.repo().root)(path)
354 354 state = ctx.substate[path]
355 355 if state[2] not in types:
356 356 raise error.Abort(_('unknown subrepo type %s') % state[2])
357 357 if allowwdir:
358 358 state = (state[0], ctx.subrev(path), state[2])
359 359 return types[state[2]](ctx, path, state[:2])
360 360
361 361 def nullsubrepo(ctx, path, pctx):
362 362 """return an empty subrepo in pctx for the extant subrepo in ctx"""
363 363 # subrepo inherently violates our import layering rules
364 364 # because it wants to make repo objects from deep inside the stack
365 365 # so we manually delay the circular imports to not break
366 366 # scripts that don't use our demand-loading
367 367 global hg
368 368 from . import hg as h
369 369 hg = h
370 370
371 371 pathutil.pathauditor(ctx.repo().root)(path)
372 372 state = ctx.substate[path]
373 373 if state[2] not in types:
374 374 raise error.Abort(_('unknown subrepo type %s') % state[2])
375 375 subrev = ''
376 376 if state[2] == 'hg':
377 377 subrev = "0" * 40
378 378 return types[state[2]](pctx, path, (state[0], subrev))
379 379
380 380 def newcommitphase(ui, ctx):
381 381 commitphase = phases.newcommitphase(ui)
382 382 substate = getattr(ctx, "substate", None)
383 383 if not substate:
384 384 return commitphase
385 385 check = ui.config('phases', 'checksubrepos', 'follow')
386 386 if check not in ('ignore', 'follow', 'abort'):
387 387 raise error.Abort(_('invalid phases.checksubrepos configuration: %s')
388 388 % (check))
389 389 if check == 'ignore':
390 390 return commitphase
391 391 maxphase = phases.public
392 392 maxsub = None
393 393 for s in sorted(substate):
394 394 sub = ctx.sub(s)
395 395 subphase = sub.phase(substate[s][1])
396 396 if maxphase < subphase:
397 397 maxphase = subphase
398 398 maxsub = s
399 399 if commitphase < maxphase:
400 400 if check == 'abort':
401 401 raise error.Abort(_("can't commit in %s phase"
402 402 " conflicting %s from subrepository %s") %
403 403 (phases.phasenames[commitphase],
404 404 phases.phasenames[maxphase], maxsub))
405 405 ui.warn(_("warning: changes are committed in"
406 406 " %s phase from subrepository %s\n") %
407 407 (phases.phasenames[maxphase], maxsub))
408 408 return maxphase
409 409 return commitphase
410 410
411 411 # subrepo classes need to implement the following abstract class:
412 412
413 413 class abstractsubrepo(object):
414 414
415 415 def __init__(self, ctx, path):
416 416 """Initialize abstractsubrepo part
417 417
418 418 ``ctx`` is the context referring this subrepository in the
419 419 parent repository.
420 420
421 421 ``path`` is the path to this subrepository as seen from
422 422 innermost repository.
423 423 """
424 424 self.ui = ctx.repo().ui
425 425 self._ctx = ctx
426 426 self._path = path
427 427
428 428 def storeclean(self, path):
429 429 """
430 430 returns true if the repository has not changed since it was last
431 431 cloned from or pushed to a given repository.
432 432 """
433 433 return False
434 434
435 435 def dirty(self, ignoreupdate=False):
436 436 """returns true if the dirstate of the subrepo is dirty or does not
437 437 match current stored state. If ignoreupdate is true, only check
438 438 whether the subrepo has uncommitted changes in its dirstate.
439 439 """
440 440 raise NotImplementedError
441 441
442 442 def dirtyreason(self, ignoreupdate=False):
443 443 """return reason string if it is ``dirty()``
444 444
445 445 Returned string should have enough information for the message
446 446 of exception.
447 447
448 448 This returns None, otherwise.
449 449 """
450 450 if self.dirty(ignoreupdate=ignoreupdate):
451 451 return _("uncommitted changes in subrepository '%s'"
452 452 ) % subrelpath(self)
453 453
454 454 def bailifchanged(self, ignoreupdate=False):
455 455 """raise Abort if subrepository is ``dirty()``
456 456 """
457 457 dirtyreason = self.dirtyreason(ignoreupdate=ignoreupdate)
458 458 if dirtyreason:
459 459 raise error.Abort(dirtyreason)
460 460
461 461 def basestate(self):
462 462 """current working directory base state, disregarding .hgsubstate
463 463 state and working directory modifications"""
464 464 raise NotImplementedError
465 465
466 466 def checknested(self, path):
467 467 """check if path is a subrepository within this repository"""
468 468 return False
469 469
470 470 def commit(self, text, user, date):
471 471 """commit the current changes to the subrepo with the given
472 472 log message. Use given user and date if possible. Return the
473 473 new state of the subrepo.
474 474 """
475 475 raise NotImplementedError
476 476
477 477 def phase(self, state):
478 478 """returns phase of specified state in the subrepository.
479 479 """
480 480 return phases.public
481 481
482 482 def remove(self):
483 483 """remove the subrepo
484 484
485 485 (should verify the dirstate is not dirty first)
486 486 """
487 487 raise NotImplementedError
488 488
489 489 def get(self, state, overwrite=False):
490 490 """run whatever commands are needed to put the subrepo into
491 491 this state
492 492 """
493 493 raise NotImplementedError
494 494
495 495 def merge(self, state):
496 496 """merge currently-saved state with the new state."""
497 497 raise NotImplementedError
498 498
499 499 def push(self, opts):
500 500 """perform whatever action is analogous to 'hg push'
501 501
502 502 This may be a no-op on some systems.
503 503 """
504 504 raise NotImplementedError
505 505
506 506 def add(self, ui, match, prefix, explicitonly, **opts):
507 507 return []
508 508
509 509 def addremove(self, matcher, prefix, opts, dry_run, similarity):
510 510 self.ui.warn("%s: %s" % (prefix, _("addremove is not supported")))
511 511 return 1
512 512
513 513 def cat(self, match, prefix, **opts):
514 514 return 1
515 515
516 516 def status(self, rev2, **opts):
517 517 return scmutil.status([], [], [], [], [], [], [])
518 518
519 519 def diff(self, ui, diffopts, node2, match, prefix, **opts):
520 520 pass
521 521
522 522 def outgoing(self, ui, dest, opts):
523 523 return 1
524 524
525 525 def incoming(self, ui, source, opts):
526 526 return 1
527 527
528 528 def files(self):
529 529 """return filename iterator"""
530 530 raise NotImplementedError
531 531
532 532 def filedata(self, name):
533 533 """return file data"""
534 534 raise NotImplementedError
535 535
536 536 def fileflags(self, name):
537 537 """return file flags"""
538 538 return ''
539 539
540 540 def getfileset(self, expr):
541 541 """Resolve the fileset expression for this repo"""
542 542 return set()
543 543
544 544 def printfiles(self, ui, m, fm, fmt, subrepos):
545 545 """handle the files command for this subrepo"""
546 546 return 1
547 547
548 548 def archive(self, archiver, prefix, match=None):
549 549 if match is not None:
550 550 files = [f for f in self.files() if match(f)]
551 551 else:
552 552 files = self.files()
553 553 total = len(files)
554 554 relpath = subrelpath(self)
555 555 self.ui.progress(_('archiving (%s)') % relpath, 0,
556 556 unit=_('files'), total=total)
557 557 for i, name in enumerate(files):
558 558 flags = self.fileflags(name)
559 559 mode = 'x' in flags and 0o755 or 0o644
560 560 symlink = 'l' in flags
561 561 archiver.addfile(prefix + self._path + '/' + name,
562 562 mode, symlink, self.filedata(name))
563 563 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
564 564 unit=_('files'), total=total)
565 565 self.ui.progress(_('archiving (%s)') % relpath, None)
566 566 return total
567 567
568 568 def walk(self, match):
569 569 '''
570 570 walk recursively through the directory tree, finding all files
571 571 matched by the match function
572 572 '''
573 573 pass
574 574
575 575 def forget(self, match, prefix):
576 576 return ([], [])
577 577
578 578 def removefiles(self, matcher, prefix, after, force, subrepos):
579 579 """remove the matched files from the subrepository and the filesystem,
580 580 possibly by force and/or after the file has been removed from the
581 581 filesystem. Return 0 on success, 1 on any warning.
582 582 """
583 583 return 1
584 584
585 585 def revert(self, substate, *pats, **opts):
586 586 self.ui.warn('%s: reverting %s subrepos is unsupported\n' \
587 587 % (substate[0], substate[2]))
588 588 return []
589 589
590 590 def shortid(self, revid):
591 591 return revid
592 592
593 593 def verify(self):
594 594 '''verify the integrity of the repository. Return 0 on success or
595 595 warning, 1 on any error.
596 596 '''
597 597 return 0
598 598
599 599 @propertycache
600 600 def wvfs(self):
601 601 """return vfs to access the working directory of this subrepository
602 602 """
603 603 return scmutil.vfs(self._ctx.repo().wvfs.join(self._path))
604 604
605 605 @propertycache
606 606 def _relpath(self):
607 607 """return path to this subrepository as seen from outermost repository
608 608 """
609 609 return self.wvfs.reljoin(reporelpath(self._ctx.repo()), self._path)
610 610
611 611 class hgsubrepo(abstractsubrepo):
612 612 def __init__(self, ctx, path, state):
613 613 super(hgsubrepo, self).__init__(ctx, path)
614 614 self._state = state
615 615 r = ctx.repo()
616 616 root = r.wjoin(path)
617 617 create = not r.wvfs.exists('%s/.hg' % path)
618 618 self._repo = hg.repository(r.baseui, root, create=create)
619 619
620 620 # Propagate the parent's --hidden option
621 621 if r is r.unfiltered():
622 622 self._repo = self._repo.unfiltered()
623 623
624 624 self.ui = self._repo.ui
625 625 for s, k in [('ui', 'commitsubrepos')]:
626 626 v = r.ui.config(s, k)
627 627 if v:
628 628 self.ui.setconfig(s, k, v, 'subrepo')
629 629 # internal config: ui._usedassubrepo
630 630 self.ui.setconfig('ui', '_usedassubrepo', 'True', 'subrepo')
631 631 self._initrepo(r, state[0], create)
632 632
633 633 def storeclean(self, path):
634 634 lock = self._repo.lock()
635 635 try:
636 636 return self._storeclean(path)
637 637 finally:
638 638 lock.release()
639 639
640 640 def _storeclean(self, path):
641 641 clean = True
642 642 itercache = self._calcstorehash(path)
643 643 for filehash in self._readstorehashcache(path):
644 644 if filehash != next(itercache, None):
645 645 clean = False
646 646 break
647 647 if clean:
648 648 # if not empty:
649 649 # the cached and current pull states have a different size
650 650 clean = next(itercache, None) is None
651 651 return clean
652 652
653 653 def _calcstorehash(self, remotepath):
654 654 '''calculate a unique "store hash"
655 655
656 656 This method is used to to detect when there are changes that may
657 657 require a push to a given remote path.'''
658 658 # sort the files that will be hashed in increasing (likely) file size
659 659 filelist = ('bookmarks', 'store/phaseroots', 'store/00changelog.i')
660 660 yield '# %s\n' % _expandedabspath(remotepath)
661 661 vfs = self._repo.vfs
662 662 for relname in filelist:
663 663 filehash = util.sha1(vfs.tryread(relname)).hexdigest()
664 664 yield '%s = %s\n' % (relname, filehash)
665 665
666 666 @propertycache
667 667 def _cachestorehashvfs(self):
668 668 return scmutil.vfs(self._repo.join('cache/storehash'))
669 669
670 670 def _readstorehashcache(self, remotepath):
671 671 '''read the store hash cache for a given remote repository'''
672 672 cachefile = _getstorehashcachename(remotepath)
673 673 return self._cachestorehashvfs.tryreadlines(cachefile, 'r')
674 674
675 675 def _cachestorehash(self, remotepath):
676 676 '''cache the current store hash
677 677
678 678 Each remote repo requires its own store hash cache, because a subrepo
679 679 store may be "clean" versus a given remote repo, but not versus another
680 680 '''
681 681 cachefile = _getstorehashcachename(remotepath)
682 682 lock = self._repo.lock()
683 683 try:
684 684 storehash = list(self._calcstorehash(remotepath))
685 685 vfs = self._cachestorehashvfs
686 686 vfs.writelines(cachefile, storehash, mode='w', notindexed=True)
687 687 finally:
688 688 lock.release()
689 689
690 690 def _getctx(self):
691 691 '''fetch the context for this subrepo revision, possibly a workingctx
692 692 '''
693 693 if self._ctx.rev() is None:
694 694 return self._repo[None] # workingctx if parent is workingctx
695 695 else:
696 696 rev = self._state[1]
697 697 return self._repo[rev]
698 698
699 699 @annotatesubrepoerror
700 700 def _initrepo(self, parentrepo, source, create):
701 701 self._repo._subparent = parentrepo
702 702 self._repo._subsource = source
703 703
704 704 if create:
705 705 lines = ['[paths]\n']
706 706
707 707 def addpathconfig(key, value):
708 708 if value:
709 709 lines.append('%s = %s\n' % (key, value))
710 710 self.ui.setconfig('paths', key, value, 'subrepo')
711 711
712 712 defpath = _abssource(self._repo, abort=False)
713 713 defpushpath = _abssource(self._repo, True, abort=False)
714 714 addpathconfig('default', defpath)
715 715 if defpath != defpushpath:
716 716 addpathconfig('default-push', defpushpath)
717 717
718 718 fp = self._repo.vfs("hgrc", "w", text=True)
719 719 try:
720 720 fp.write(''.join(lines))
721 721 finally:
722 722 fp.close()
723 723
724 724 @annotatesubrepoerror
725 725 def add(self, ui, match, prefix, explicitonly, **opts):
726 726 return cmdutil.add(ui, self._repo, match,
727 727 self.wvfs.reljoin(prefix, self._path),
728 728 explicitonly, **opts)
729 729
730 730 @annotatesubrepoerror
731 731 def addremove(self, m, prefix, opts, dry_run, similarity):
732 732 # In the same way as sub directories are processed, once in a subrepo,
733 733 # always entry any of its subrepos. Don't corrupt the options that will
734 734 # be used to process sibling subrepos however.
735 735 opts = copy.copy(opts)
736 736 opts['subrepos'] = True
737 737 return scmutil.addremove(self._repo, m,
738 738 self.wvfs.reljoin(prefix, self._path), opts,
739 739 dry_run, similarity)
740 740
741 741 @annotatesubrepoerror
742 742 def cat(self, match, prefix, **opts):
743 743 rev = self._state[1]
744 744 ctx = self._repo[rev]
745 745 return cmdutil.cat(self.ui, self._repo, ctx, match, prefix, **opts)
746 746
747 747 @annotatesubrepoerror
748 748 def status(self, rev2, **opts):
749 749 try:
750 750 rev1 = self._state[1]
751 751 ctx1 = self._repo[rev1]
752 752 ctx2 = self._repo[rev2]
753 753 return self._repo.status(ctx1, ctx2, **opts)
754 754 except error.RepoLookupError as inst:
755 755 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
756 756 % (inst, subrelpath(self)))
757 757 return scmutil.status([], [], [], [], [], [], [])
758 758
759 759 @annotatesubrepoerror
760 760 def diff(self, ui, diffopts, node2, match, prefix, **opts):
761 761 try:
762 762 node1 = node.bin(self._state[1])
763 763 # We currently expect node2 to come from substate and be
764 764 # in hex format
765 765 if node2 is not None:
766 766 node2 = node.bin(node2)
767 767 cmdutil.diffordiffstat(ui, self._repo, diffopts,
768 768 node1, node2, match,
769 769 prefix=posixpath.join(prefix, self._path),
770 770 listsubrepos=True, **opts)
771 771 except error.RepoLookupError as inst:
772 772 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
773 773 % (inst, subrelpath(self)))
774 774
775 775 @annotatesubrepoerror
776 776 def archive(self, archiver, prefix, match=None):
777 777 self._get(self._state + ('hg',))
778 778 total = abstractsubrepo.archive(self, archiver, prefix, match)
779 779 rev = self._state[1]
780 780 ctx = self._repo[rev]
781 781 for subpath in ctx.substate:
782 782 s = subrepo(ctx, subpath, True)
783 783 submatch = matchmod.narrowmatcher(subpath, match)
784 784 total += s.archive(archiver, prefix + self._path + '/', submatch)
785 785 return total
786 786
787 787 @annotatesubrepoerror
788 788 def dirty(self, ignoreupdate=False):
789 789 r = self._state[1]
790 790 if r == '' and not ignoreupdate: # no state recorded
791 791 return True
792 792 w = self._repo[None]
793 793 if r != w.p1().hex() and not ignoreupdate:
794 794 # different version checked out
795 795 return True
796 796 return w.dirty() # working directory changed
797 797
798 798 def basestate(self):
799 799 return self._repo['.'].hex()
800 800
801 801 def checknested(self, path):
802 802 return self._repo._checknested(self._repo.wjoin(path))
803 803
804 804 @annotatesubrepoerror
805 805 def commit(self, text, user, date):
806 806 # don't bother committing in the subrepo if it's only been
807 807 # updated
808 808 if not self.dirty(True):
809 809 return self._repo['.'].hex()
810 810 self.ui.debug("committing subrepo %s\n" % subrelpath(self))
811 811 n = self._repo.commit(text, user, date)
812 812 if not n:
813 813 return self._repo['.'].hex() # different version checked out
814 814 return node.hex(n)
815 815
816 816 @annotatesubrepoerror
817 817 def phase(self, state):
818 818 return self._repo[state].phase()
819 819
820 820 @annotatesubrepoerror
821 821 def remove(self):
822 822 # we can't fully delete the repository as it may contain
823 823 # local-only history
824 824 self.ui.note(_('removing subrepo %s\n') % subrelpath(self))
825 825 hg.clean(self._repo, node.nullid, False)
826 826
827 827 def _get(self, state):
828 828 source, revision, kind = state
829 829 if revision in self._repo.unfiltered():
830 830 return True
831 831 self._repo._subsource = source
832 832 srcurl = _abssource(self._repo)
833 833 other = hg.peer(self._repo, {}, srcurl)
834 834 if len(self._repo) == 0:
835 835 self.ui.status(_('cloning subrepo %s from %s\n')
836 836 % (subrelpath(self), srcurl))
837 837 parentrepo = self._repo._subparent
838 838 # use self._repo.vfs instead of self.wvfs to remove .hg only
839 839 self._repo.vfs.rmtree()
840 840 other, cloned = hg.clone(self._repo._subparent.baseui, {},
841 841 other, self._repo.root,
842 842 update=False)
843 843 self._repo = cloned.local()
844 844 self._initrepo(parentrepo, source, create=True)
845 845 self._cachestorehash(srcurl)
846 846 else:
847 847 self.ui.status(_('pulling subrepo %s from %s\n')
848 848 % (subrelpath(self), srcurl))
849 849 cleansub = self.storeclean(srcurl)
850 850 exchange.pull(self._repo, other)
851 851 if cleansub:
852 852 # keep the repo clean after pull
853 853 self._cachestorehash(srcurl)
854 854 return False
855 855
856 856 @annotatesubrepoerror
857 857 def get(self, state, overwrite=False):
858 858 inrepo = self._get(state)
859 859 source, revision, kind = state
860 860 repo = self._repo
861 861 repo.ui.debug("getting subrepo %s\n" % self._path)
862 862 if inrepo:
863 863 urepo = repo.unfiltered()
864 864 ctx = urepo[revision]
865 865 if ctx.hidden():
866 866 urepo.ui.warn(
867 867 _('revision %s in subrepo %s is hidden\n') \
868 868 % (revision[0:12], self._path))
869 869 repo = urepo
870 870 hg.updaterepo(repo, revision, overwrite)
871 871
872 872 @annotatesubrepoerror
873 873 def merge(self, state):
874 874 self._get(state)
875 875 cur = self._repo['.']
876 876 dst = self._repo[state[1]]
877 877 anc = dst.ancestor(cur)
878 878
879 879 def mergefunc():
880 880 if anc == cur and dst.branch() == cur.branch():
881 881 self.ui.debug("updating subrepo %s\n" % subrelpath(self))
882 882 hg.update(self._repo, state[1])
883 883 elif anc == dst:
884 884 self.ui.debug("skipping subrepo %s\n" % subrelpath(self))
885 885 else:
886 886 self.ui.debug("merging subrepo %s\n" % subrelpath(self))
887 887 hg.merge(self._repo, state[1], remind=False)
888 888
889 889 wctx = self._repo[None]
890 890 if self.dirty():
891 891 if anc != dst:
892 892 if _updateprompt(self.ui, self, wctx.dirty(), cur, dst):
893 893 mergefunc()
894 894 else:
895 895 mergefunc()
896 896 else:
897 897 mergefunc()
898 898
899 899 @annotatesubrepoerror
900 900 def push(self, opts):
901 901 force = opts.get('force')
902 902 newbranch = opts.get('new_branch')
903 903 ssh = opts.get('ssh')
904 904
905 905 # push subrepos depth-first for coherent ordering
906 906 c = self._repo['']
907 907 subs = c.substate # only repos that are committed
908 908 for s in sorted(subs):
909 909 if c.sub(s).push(opts) == 0:
910 910 return False
911 911
912 912 dsturl = _abssource(self._repo, True)
913 913 if not force:
914 914 if self.storeclean(dsturl):
915 915 self.ui.status(
916 916 _('no changes made to subrepo %s since last push to %s\n')
917 917 % (subrelpath(self), dsturl))
918 918 return None
919 919 self.ui.status(_('pushing subrepo %s to %s\n') %
920 920 (subrelpath(self), dsturl))
921 921 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
922 922 res = exchange.push(self._repo, other, force, newbranch=newbranch)
923 923
924 924 # the repo is now clean
925 925 self._cachestorehash(dsturl)
926 926 return res.cgresult
927 927
928 928 @annotatesubrepoerror
929 929 def outgoing(self, ui, dest, opts):
930 930 if 'rev' in opts or 'branch' in opts:
931 931 opts = copy.copy(opts)
932 932 opts.pop('rev', None)
933 933 opts.pop('branch', None)
934 934 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
935 935
936 936 @annotatesubrepoerror
937 937 def incoming(self, ui, source, opts):
938 938 if 'rev' in opts or 'branch' in opts:
939 939 opts = copy.copy(opts)
940 940 opts.pop('rev', None)
941 941 opts.pop('branch', None)
942 942 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
943 943
944 944 @annotatesubrepoerror
945 945 def files(self):
946 946 rev = self._state[1]
947 947 ctx = self._repo[rev]
948 948 return ctx.manifest().keys()
949 949
950 950 def filedata(self, name):
951 951 rev = self._state[1]
952 952 return self._repo[rev][name].data()
953 953
954 954 def fileflags(self, name):
955 955 rev = self._state[1]
956 956 ctx = self._repo[rev]
957 957 return ctx.flags(name)
958 958
959 959 @annotatesubrepoerror
960 960 def printfiles(self, ui, m, fm, fmt, subrepos):
961 961 # If the parent context is a workingctx, use the workingctx here for
962 962 # consistency.
963 963 if self._ctx.rev() is None:
964 964 ctx = self._repo[None]
965 965 else:
966 966 rev = self._state[1]
967 967 ctx = self._repo[rev]
968 968 return cmdutil.files(ui, ctx, m, fm, fmt, subrepos)
969 969
970 970 @annotatesubrepoerror
971 971 def getfileset(self, expr):
972 972 if self._ctx.rev() is None:
973 973 ctx = self._repo[None]
974 974 else:
975 975 rev = self._state[1]
976 976 ctx = self._repo[rev]
977 977
978 978 files = ctx.getfileset(expr)
979 979
980 980 for subpath in ctx.substate:
981 981 sub = ctx.sub(subpath)
982 982
983 983 try:
984 984 files.extend(subpath + '/' + f for f in sub.getfileset(expr))
985 985 except error.LookupError:
986 986 self.ui.status(_("skipping missing subrepository: %s\n")
987 987 % self.wvfs.reljoin(reporelpath(self), subpath))
988 988 return files
989 989
990 990 def walk(self, match):
991 991 ctx = self._repo[None]
992 992 return ctx.walk(match)
993 993
994 994 @annotatesubrepoerror
995 995 def forget(self, match, prefix):
996 996 return cmdutil.forget(self.ui, self._repo, match,
997 997 self.wvfs.reljoin(prefix, self._path), True)
998 998
999 999 @annotatesubrepoerror
1000 1000 def removefiles(self, matcher, prefix, after, force, subrepos):
1001 1001 return cmdutil.remove(self.ui, self._repo, matcher,
1002 1002 self.wvfs.reljoin(prefix, self._path),
1003 1003 after, force, subrepos)
1004 1004
1005 1005 @annotatesubrepoerror
1006 1006 def revert(self, substate, *pats, **opts):
1007 1007 # reverting a subrepo is a 2 step process:
1008 1008 # 1. if the no_backup is not set, revert all modified
1009 1009 # files inside the subrepo
1010 1010 # 2. update the subrepo to the revision specified in
1011 1011 # the corresponding substate dictionary
1012 1012 self.ui.status(_('reverting subrepo %s\n') % substate[0])
1013 1013 if not opts.get('no_backup'):
1014 1014 # Revert all files on the subrepo, creating backups
1015 1015 # Note that this will not recursively revert subrepos
1016 1016 # We could do it if there was a set:subrepos() predicate
1017 1017 opts = opts.copy()
1018 1018 opts['date'] = None
1019 1019 opts['rev'] = substate[1]
1020 1020
1021 1021 self.filerevert(*pats, **opts)
1022 1022
1023 1023 # Update the repo to the revision specified in the given substate
1024 1024 if not opts.get('dry_run'):
1025 1025 self.get(substate, overwrite=True)
1026 1026
1027 1027 def filerevert(self, *pats, **opts):
1028 1028 ctx = self._repo[opts['rev']]
1029 1029 parents = self._repo.dirstate.parents()
1030 1030 if opts.get('all'):
1031 1031 pats = ['set:modified()']
1032 1032 else:
1033 1033 pats = []
1034 1034 cmdutil.revert(self.ui, self._repo, ctx, parents, *pats, **opts)
1035 1035
1036 1036 def shortid(self, revid):
1037 1037 return revid[:12]
1038 1038
1039 1039 def verify(self):
1040 1040 try:
1041 1041 rev = self._state[1]
1042 1042 ctx = self._repo.unfiltered()[rev]
1043 1043 if ctx.hidden():
1044 1044 # Since hidden revisions aren't pushed/pulled, it seems worth an
1045 1045 # explicit warning.
1046 1046 ui = self._repo.ui
1047 1047 ui.warn(_("subrepo '%s' is hidden in revision %s\n") %
1048 1048 (self._relpath, node.short(self._ctx.node())))
1049 1049 return 0
1050 1050 except error.RepoLookupError:
1051 1051 # A missing subrepo revision may be a case of needing to pull it, so
1052 1052 # don't treat this as an error.
1053 1053 self._repo.ui.warn(_("subrepo '%s' not found in revision %s\n") %
1054 1054 (self._relpath, node.short(self._ctx.node())))
1055 1055 return 0
1056 1056
1057 1057 @propertycache
1058 1058 def wvfs(self):
1059 1059 """return own wvfs for efficiency and consistency
1060 1060 """
1061 1061 return self._repo.wvfs
1062 1062
1063 1063 @propertycache
1064 1064 def _relpath(self):
1065 1065 """return path to this subrepository as seen from outermost repository
1066 1066 """
1067 1067 # Keep consistent dir separators by avoiding vfs.join(self._path)
1068 1068 return reporelpath(self._repo)
1069 1069
1070 1070 class svnsubrepo(abstractsubrepo):
1071 1071 def __init__(self, ctx, path, state):
1072 1072 super(svnsubrepo, self).__init__(ctx, path)
1073 1073 self._state = state
1074 1074 self._exe = util.findexe('svn')
1075 1075 if not self._exe:
1076 1076 raise error.Abort(_("'svn' executable not found for subrepo '%s'")
1077 1077 % self._path)
1078 1078
1079 1079 def _svncommand(self, commands, filename='', failok=False):
1080 1080 cmd = [self._exe]
1081 1081 extrakw = {}
1082 1082 if not self.ui.interactive():
1083 1083 # Making stdin be a pipe should prevent svn from behaving
1084 1084 # interactively even if we can't pass --non-interactive.
1085 1085 extrakw['stdin'] = subprocess.PIPE
1086 1086 # Starting in svn 1.5 --non-interactive is a global flag
1087 1087 # instead of being per-command, but we need to support 1.4 so
1088 1088 # we have to be intelligent about what commands take
1089 1089 # --non-interactive.
1090 1090 if commands[0] in ('update', 'checkout', 'commit'):
1091 1091 cmd.append('--non-interactive')
1092 1092 cmd.extend(commands)
1093 1093 if filename is not None:
1094 1094 path = self.wvfs.reljoin(self._ctx.repo().origroot,
1095 1095 self._path, filename)
1096 1096 cmd.append(path)
1097 1097 env = dict(os.environ)
1098 1098 # Avoid localized output, preserve current locale for everything else.
1099 1099 lc_all = env.get('LC_ALL')
1100 1100 if lc_all:
1101 1101 env['LANG'] = lc_all
1102 1102 del env['LC_ALL']
1103 1103 env['LC_MESSAGES'] = 'C'
1104 1104 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
1105 1105 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
1106 1106 universal_newlines=True, env=env, **extrakw)
1107 1107 stdout, stderr = p.communicate()
1108 1108 stderr = stderr.strip()
1109 1109 if not failok:
1110 1110 if p.returncode:
1111 1111 raise error.Abort(stderr or 'exited with code %d'
1112 1112 % p.returncode)
1113 1113 if stderr:
1114 1114 self.ui.warn(stderr + '\n')
1115 1115 return stdout, stderr
1116 1116
1117 1117 @propertycache
1118 1118 def _svnversion(self):
1119 1119 output, err = self._svncommand(['--version', '--quiet'], filename=None)
1120 1120 m = re.search(r'^(\d+)\.(\d+)', output)
1121 1121 if not m:
1122 1122 raise error.Abort(_('cannot retrieve svn tool version'))
1123 1123 return (int(m.group(1)), int(m.group(2)))
1124 1124
1125 1125 def _wcrevs(self):
1126 1126 # Get the working directory revision as well as the last
1127 1127 # commit revision so we can compare the subrepo state with
1128 1128 # both. We used to store the working directory one.
1129 1129 output, err = self._svncommand(['info', '--xml'])
1130 1130 doc = xml.dom.minidom.parseString(output)
1131 1131 entries = doc.getElementsByTagName('entry')
1132 1132 lastrev, rev = '0', '0'
1133 1133 if entries:
1134 1134 rev = str(entries[0].getAttribute('revision')) or '0'
1135 1135 commits = entries[0].getElementsByTagName('commit')
1136 1136 if commits:
1137 1137 lastrev = str(commits[0].getAttribute('revision')) or '0'
1138 1138 return (lastrev, rev)
1139 1139
1140 1140 def _wcrev(self):
1141 1141 return self._wcrevs()[0]
1142 1142
1143 1143 def _wcchanged(self):
1144 1144 """Return (changes, extchanges, missing) where changes is True
1145 1145 if the working directory was changed, extchanges is
1146 1146 True if any of these changes concern an external entry and missing
1147 1147 is True if any change is a missing entry.
1148 1148 """
1149 1149 output, err = self._svncommand(['status', '--xml'])
1150 1150 externals, changes, missing = [], [], []
1151 1151 doc = xml.dom.minidom.parseString(output)
1152 1152 for e in doc.getElementsByTagName('entry'):
1153 1153 s = e.getElementsByTagName('wc-status')
1154 1154 if not s:
1155 1155 continue
1156 1156 item = s[0].getAttribute('item')
1157 1157 props = s[0].getAttribute('props')
1158 1158 path = e.getAttribute('path')
1159 1159 if item == 'external':
1160 1160 externals.append(path)
1161 1161 elif item == 'missing':
1162 1162 missing.append(path)
1163 1163 if (item not in ('', 'normal', 'unversioned', 'external')
1164 1164 or props not in ('', 'none', 'normal')):
1165 1165 changes.append(path)
1166 1166 for path in changes:
1167 1167 for ext in externals:
1168 1168 if path == ext or path.startswith(ext + os.sep):
1169 1169 return True, True, bool(missing)
1170 1170 return bool(changes), False, bool(missing)
1171 1171
1172 1172 def dirty(self, ignoreupdate=False):
1173 1173 if not self._wcchanged()[0]:
1174 1174 if self._state[1] in self._wcrevs() or ignoreupdate:
1175 1175 return False
1176 1176 return True
1177 1177
1178 1178 def basestate(self):
1179 1179 lastrev, rev = self._wcrevs()
1180 1180 if lastrev != rev:
1181 1181 # Last committed rev is not the same than rev. We would
1182 1182 # like to take lastrev but we do not know if the subrepo
1183 1183 # URL exists at lastrev. Test it and fallback to rev it
1184 1184 # is not there.
1185 1185 try:
1186 1186 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
1187 1187 return lastrev
1188 1188 except error.Abort:
1189 1189 pass
1190 1190 return rev
1191 1191
1192 1192 @annotatesubrepoerror
1193 1193 def commit(self, text, user, date):
1194 1194 # user and date are out of our hands since svn is centralized
1195 1195 changed, extchanged, missing = self._wcchanged()
1196 1196 if not changed:
1197 1197 return self.basestate()
1198 1198 if extchanged:
1199 1199 # Do not try to commit externals
1200 1200 raise error.Abort(_('cannot commit svn externals'))
1201 1201 if missing:
1202 1202 # svn can commit with missing entries but aborting like hg
1203 1203 # seems a better approach.
1204 1204 raise error.Abort(_('cannot commit missing svn entries'))
1205 1205 commitinfo, err = self._svncommand(['commit', '-m', text])
1206 1206 self.ui.status(commitinfo)
1207 1207 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
1208 1208 if not newrev:
1209 1209 if not commitinfo.strip():
1210 1210 # Sometimes, our definition of "changed" differs from
1211 1211 # svn one. For instance, svn ignores missing files
1212 1212 # when committing. If there are only missing files, no
1213 1213 # commit is made, no output and no error code.
1214 1214 raise error.Abort(_('failed to commit svn changes'))
1215 1215 raise error.Abort(commitinfo.splitlines()[-1])
1216 1216 newrev = newrev.groups()[0]
1217 1217 self.ui.status(self._svncommand(['update', '-r', newrev])[0])
1218 1218 return newrev
1219 1219
1220 1220 @annotatesubrepoerror
1221 1221 def remove(self):
1222 1222 if self.dirty():
1223 1223 self.ui.warn(_('not removing repo %s because '
1224 1224 'it has changes.\n') % self._path)
1225 1225 return
1226 1226 self.ui.note(_('removing subrepo %s\n') % self._path)
1227 1227
1228 1228 self.wvfs.rmtree(forcibly=True)
1229 1229 try:
1230 1230 pwvfs = self._ctx.repo().wvfs
1231 1231 pwvfs.removedirs(pwvfs.dirname(self._path))
1232 1232 except OSError:
1233 1233 pass
1234 1234
1235 1235 @annotatesubrepoerror
1236 1236 def get(self, state, overwrite=False):
1237 1237 if overwrite:
1238 1238 self._svncommand(['revert', '--recursive'])
1239 1239 args = ['checkout']
1240 1240 if self._svnversion >= (1, 5):
1241 1241 args.append('--force')
1242 1242 # The revision must be specified at the end of the URL to properly
1243 1243 # update to a directory which has since been deleted and recreated.
1244 1244 args.append('%s@%s' % (state[0], state[1]))
1245 1245 status, err = self._svncommand(args, failok=True)
1246 1246 _sanitize(self.ui, self.wvfs, '.svn')
1247 1247 if not re.search('Checked out revision [0-9]+.', status):
1248 1248 if ('is already a working copy for a different URL' in err
1249 1249 and (self._wcchanged()[:2] == (False, False))):
1250 1250 # obstructed but clean working copy, so just blow it away.
1251 1251 self.remove()
1252 1252 self.get(state, overwrite=False)
1253 1253 return
1254 1254 raise error.Abort((status or err).splitlines()[-1])
1255 1255 self.ui.status(status)
1256 1256
1257 1257 @annotatesubrepoerror
1258 1258 def merge(self, state):
1259 1259 old = self._state[1]
1260 1260 new = state[1]
1261 1261 wcrev = self._wcrev()
1262 1262 if new != wcrev:
1263 1263 dirty = old == wcrev or self._wcchanged()[0]
1264 1264 if _updateprompt(self.ui, self, dirty, wcrev, new):
1265 1265 self.get(state, False)
1266 1266
1267 1267 def push(self, opts):
1268 1268 # push is a no-op for SVN
1269 1269 return True
1270 1270
1271 1271 @annotatesubrepoerror
1272 1272 def files(self):
1273 1273 output = self._svncommand(['list', '--recursive', '--xml'])[0]
1274 1274 doc = xml.dom.minidom.parseString(output)
1275 1275 paths = []
1276 1276 for e in doc.getElementsByTagName('entry'):
1277 1277 kind = str(e.getAttribute('kind'))
1278 1278 if kind != 'file':
1279 1279 continue
1280 1280 name = ''.join(c.data for c
1281 1281 in e.getElementsByTagName('name')[0].childNodes
1282 1282 if c.nodeType == c.TEXT_NODE)
1283 1283 paths.append(name.encode('utf-8'))
1284 1284 return paths
1285 1285
1286 1286 def filedata(self, name):
1287 1287 return self._svncommand(['cat'], name)[0]
1288 1288
1289 1289
1290 1290 class gitsubrepo(abstractsubrepo):
1291 1291 def __init__(self, ctx, path, state):
1292 1292 super(gitsubrepo, self).__init__(ctx, path)
1293 1293 self._state = state
1294 1294 self._abspath = ctx.repo().wjoin(path)
1295 1295 self._subparent = ctx.repo()
1296 1296 self._ensuregit()
1297 1297
1298 1298 def _ensuregit(self):
1299 1299 try:
1300 1300 self._gitexecutable = 'git'
1301 1301 out, err = self._gitnodir(['--version'])
1302 1302 except OSError as e:
1303 1303 if e.errno != 2 or os.name != 'nt':
1304 1304 raise
1305 1305 self._gitexecutable = 'git.cmd'
1306 1306 out, err = self._gitnodir(['--version'])
1307 1307 versionstatus = self._checkversion(out)
1308 1308 if versionstatus == 'unknown':
1309 1309 self.ui.warn(_('cannot retrieve git version\n'))
1310 1310 elif versionstatus == 'abort':
1311 1311 raise error.Abort(_('git subrepo requires at least 1.6.0 or later'))
1312 1312 elif versionstatus == 'warning':
1313 1313 self.ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
1314 1314
1315 1315 @staticmethod
1316 1316 def _gitversion(out):
1317 1317 m = re.search(r'^git version (\d+)\.(\d+)\.(\d+)', out)
1318 1318 if m:
1319 1319 return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
1320 1320
1321 1321 m = re.search(r'^git version (\d+)\.(\d+)', out)
1322 1322 if m:
1323 1323 return (int(m.group(1)), int(m.group(2)), 0)
1324 1324
1325 1325 return -1
1326 1326
1327 1327 @staticmethod
1328 1328 def _checkversion(out):
1329 1329 '''ensure git version is new enough
1330 1330
1331 1331 >>> _checkversion = gitsubrepo._checkversion
1332 1332 >>> _checkversion('git version 1.6.0')
1333 1333 'ok'
1334 1334 >>> _checkversion('git version 1.8.5')
1335 1335 'ok'
1336 1336 >>> _checkversion('git version 1.4.0')
1337 1337 'abort'
1338 1338 >>> _checkversion('git version 1.5.0')
1339 1339 'warning'
1340 1340 >>> _checkversion('git version 1.9-rc0')
1341 1341 'ok'
1342 1342 >>> _checkversion('git version 1.9.0.265.g81cdec2')
1343 1343 'ok'
1344 1344 >>> _checkversion('git version 1.9.0.GIT')
1345 1345 'ok'
1346 1346 >>> _checkversion('git version 12345')
1347 1347 'unknown'
1348 1348 >>> _checkversion('no')
1349 1349 'unknown'
1350 1350 '''
1351 1351 version = gitsubrepo._gitversion(out)
1352 1352 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1353 1353 # despite the docstring comment. For now, error on 1.4.0, warn on
1354 1354 # 1.5.0 but attempt to continue.
1355 1355 if version == -1:
1356 1356 return 'unknown'
1357 1357 if version < (1, 5, 0):
1358 1358 return 'abort'
1359 1359 elif version < (1, 6, 0):
1360 1360 return 'warning'
1361 1361 return 'ok'
1362 1362
1363 1363 def _gitcommand(self, commands, env=None, stream=False):
1364 1364 return self._gitdir(commands, env=env, stream=stream)[0]
1365 1365
1366 1366 def _gitdir(self, commands, env=None, stream=False):
1367 1367 return self._gitnodir(commands, env=env, stream=stream,
1368 1368 cwd=self._abspath)
1369 1369
1370 1370 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1371 1371 """Calls the git command
1372 1372
1373 1373 The methods tries to call the git command. versions prior to 1.6.0
1374 1374 are not supported and very probably fail.
1375 1375 """
1376 1376 self.ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1377 1377 # unless ui.quiet is set, print git's stderr,
1378 1378 # which is mostly progress and useful info
1379 1379 errpipe = None
1380 1380 if self.ui.quiet:
1381 1381 errpipe = open(os.devnull, 'w')
1382 1382 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
1383 1383 cwd=cwd, env=env, close_fds=util.closefds,
1384 1384 stdout=subprocess.PIPE, stderr=errpipe)
1385 1385 if stream:
1386 1386 return p.stdout, None
1387 1387
1388 1388 retdata = p.stdout.read().strip()
1389 1389 # wait for the child to exit to avoid race condition.
1390 1390 p.wait()
1391 1391
1392 1392 if p.returncode != 0 and p.returncode != 1:
1393 1393 # there are certain error codes that are ok
1394 1394 command = commands[0]
1395 1395 if command in ('cat-file', 'symbolic-ref'):
1396 1396 return retdata, p.returncode
1397 1397 # for all others, abort
1398 1398 raise error.Abort('git %s error %d in %s' %
1399 1399 (command, p.returncode, self._relpath))
1400 1400
1401 1401 return retdata, p.returncode
1402 1402
1403 1403 def _gitmissing(self):
1404 1404 return not self.wvfs.exists('.git')
1405 1405
1406 1406 def _gitstate(self):
1407 1407 return self._gitcommand(['rev-parse', 'HEAD'])
1408 1408
1409 1409 def _gitcurrentbranch(self):
1410 1410 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1411 1411 if err:
1412 1412 current = None
1413 1413 return current
1414 1414
1415 1415 def _gitremote(self, remote):
1416 1416 out = self._gitcommand(['remote', 'show', '-n', remote])
1417 1417 line = out.split('\n')[1]
1418 1418 i = line.index('URL: ') + len('URL: ')
1419 1419 return line[i:]
1420 1420
1421 1421 def _githavelocally(self, revision):
1422 1422 out, code = self._gitdir(['cat-file', '-e', revision])
1423 1423 return code == 0
1424 1424
1425 1425 def _gitisancestor(self, r1, r2):
1426 1426 base = self._gitcommand(['merge-base', r1, r2])
1427 1427 return base == r1
1428 1428
1429 1429 def _gitisbare(self):
1430 1430 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1431 1431
1432 1432 def _gitupdatestat(self):
1433 1433 """This must be run before git diff-index.
1434 1434 diff-index only looks at changes to file stat;
1435 1435 this command looks at file contents and updates the stat."""
1436 1436 self._gitcommand(['update-index', '-q', '--refresh'])
1437 1437
1438 1438 def _gitbranchmap(self):
1439 1439 '''returns 2 things:
1440 1440 a map from git branch to revision
1441 1441 a map from revision to branches'''
1442 1442 branch2rev = {}
1443 1443 rev2branch = {}
1444 1444
1445 1445 out = self._gitcommand(['for-each-ref', '--format',
1446 1446 '%(objectname) %(refname)'])
1447 1447 for line in out.split('\n'):
1448 1448 revision, ref = line.split(' ')
1449 1449 if (not ref.startswith('refs/heads/') and
1450 1450 not ref.startswith('refs/remotes/')):
1451 1451 continue
1452 1452 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1453 1453 continue # ignore remote/HEAD redirects
1454 1454 branch2rev[ref] = revision
1455 1455 rev2branch.setdefault(revision, []).append(ref)
1456 1456 return branch2rev, rev2branch
1457 1457
1458 1458 def _gittracking(self, branches):
1459 1459 'return map of remote branch to local tracking branch'
1460 1460 # assumes no more than one local tracking branch for each remote
1461 1461 tracking = {}
1462 1462 for b in branches:
1463 1463 if b.startswith('refs/remotes/'):
1464 1464 continue
1465 1465 bname = b.split('/', 2)[2]
1466 1466 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1467 1467 if remote:
1468 1468 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1469 1469 tracking['refs/remotes/%s/%s' %
1470 1470 (remote, ref.split('/', 2)[2])] = b
1471 1471 return tracking
1472 1472
1473 1473 def _abssource(self, source):
1474 1474 if '://' not in source:
1475 1475 # recognize the scp syntax as an absolute source
1476 1476 colon = source.find(':')
1477 1477 if colon != -1 and '/' not in source[:colon]:
1478 1478 return source
1479 1479 self._subsource = source
1480 1480 return _abssource(self)
1481 1481
1482 1482 def _fetch(self, source, revision):
1483 1483 if self._gitmissing():
1484 1484 source = self._abssource(source)
1485 1485 self.ui.status(_('cloning subrepo %s from %s\n') %
1486 1486 (self._relpath, source))
1487 1487 self._gitnodir(['clone', source, self._abspath])
1488 1488 if self._githavelocally(revision):
1489 1489 return
1490 1490 self.ui.status(_('pulling subrepo %s from %s\n') %
1491 1491 (self._relpath, self._gitremote('origin')))
1492 1492 # try only origin: the originally cloned repo
1493 1493 self._gitcommand(['fetch'])
1494 1494 if not self._githavelocally(revision):
1495 1495 raise error.Abort(_("revision %s does not exist in subrepo %s\n") %
1496 1496 (revision, self._relpath))
1497 1497
1498 1498 @annotatesubrepoerror
1499 1499 def dirty(self, ignoreupdate=False):
1500 1500 if self._gitmissing():
1501 1501 return self._state[1] != ''
1502 1502 if self._gitisbare():
1503 1503 return True
1504 1504 if not ignoreupdate and self._state[1] != self._gitstate():
1505 1505 # different version checked out
1506 1506 return True
1507 1507 # check for staged changes or modified files; ignore untracked files
1508 1508 self._gitupdatestat()
1509 1509 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1510 1510 return code == 1
1511 1511
1512 1512 def basestate(self):
1513 1513 return self._gitstate()
1514 1514
1515 1515 @annotatesubrepoerror
1516 1516 def get(self, state, overwrite=False):
1517 1517 source, revision, kind = state
1518 1518 if not revision:
1519 1519 self.remove()
1520 1520 return
1521 1521 self._fetch(source, revision)
1522 1522 # if the repo was set to be bare, unbare it
1523 1523 if self._gitisbare():
1524 1524 self._gitcommand(['config', 'core.bare', 'false'])
1525 1525 if self._gitstate() == revision:
1526 1526 self._gitcommand(['reset', '--hard', 'HEAD'])
1527 1527 return
1528 1528 elif self._gitstate() == revision:
1529 1529 if overwrite:
1530 1530 # first reset the index to unmark new files for commit, because
1531 1531 # reset --hard will otherwise throw away files added for commit,
1532 1532 # not just unmark them.
1533 1533 self._gitcommand(['reset', 'HEAD'])
1534 1534 self._gitcommand(['reset', '--hard', 'HEAD'])
1535 1535 return
1536 1536 branch2rev, rev2branch = self._gitbranchmap()
1537 1537
1538 1538 def checkout(args):
1539 1539 cmd = ['checkout']
1540 1540 if overwrite:
1541 1541 # first reset the index to unmark new files for commit, because
1542 1542 # the -f option will otherwise throw away files added for
1543 1543 # commit, not just unmark them.
1544 1544 self._gitcommand(['reset', 'HEAD'])
1545 1545 cmd.append('-f')
1546 1546 self._gitcommand(cmd + args)
1547 1547 _sanitize(self.ui, self.wvfs, '.git')
1548 1548
1549 1549 def rawcheckout():
1550 1550 # no branch to checkout, check it out with no branch
1551 1551 self.ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1552 1552 self._relpath)
1553 1553 self.ui.warn(_('check out a git branch if you intend '
1554 1554 'to make changes\n'))
1555 1555 checkout(['-q', revision])
1556 1556
1557 1557 if revision not in rev2branch:
1558 1558 rawcheckout()
1559 1559 return
1560 1560 branches = rev2branch[revision]
1561 1561 firstlocalbranch = None
1562 1562 for b in branches:
1563 1563 if b == 'refs/heads/master':
1564 1564 # master trumps all other branches
1565 1565 checkout(['refs/heads/master'])
1566 1566 return
1567 1567 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1568 1568 firstlocalbranch = b
1569 1569 if firstlocalbranch:
1570 1570 checkout([firstlocalbranch])
1571 1571 return
1572 1572
1573 1573 tracking = self._gittracking(branch2rev.keys())
1574 1574 # choose a remote branch already tracked if possible
1575 1575 remote = branches[0]
1576 1576 if remote not in tracking:
1577 1577 for b in branches:
1578 1578 if b in tracking:
1579 1579 remote = b
1580 1580 break
1581 1581
1582 1582 if remote not in tracking:
1583 1583 # create a new local tracking branch
1584 1584 local = remote.split('/', 3)[3]
1585 1585 checkout(['-b', local, remote])
1586 1586 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1587 1587 # When updating to a tracked remote branch,
1588 1588 # if the local tracking branch is downstream of it,
1589 1589 # a normal `git pull` would have performed a "fast-forward merge"
1590 1590 # which is equivalent to updating the local branch to the remote.
1591 1591 # Since we are only looking at branching at update, we need to
1592 1592 # detect this situation and perform this action lazily.
1593 1593 if tracking[remote] != self._gitcurrentbranch():
1594 1594 checkout([tracking[remote]])
1595 1595 self._gitcommand(['merge', '--ff', remote])
1596 1596 _sanitize(self.ui, self.wvfs, '.git')
1597 1597 else:
1598 1598 # a real merge would be required, just checkout the revision
1599 1599 rawcheckout()
1600 1600
1601 1601 @annotatesubrepoerror
1602 1602 def commit(self, text, user, date):
1603 1603 if self._gitmissing():
1604 1604 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1605 1605 cmd = ['commit', '-a', '-m', text]
1606 1606 env = os.environ.copy()
1607 1607 if user:
1608 1608 cmd += ['--author', user]
1609 1609 if date:
1610 1610 # git's date parser silently ignores when seconds < 1e9
1611 1611 # convert to ISO8601
1612 1612 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1613 1613 '%Y-%m-%dT%H:%M:%S %1%2')
1614 1614 self._gitcommand(cmd, env=env)
1615 1615 # make sure commit works otherwise HEAD might not exist under certain
1616 1616 # circumstances
1617 1617 return self._gitstate()
1618 1618
1619 1619 @annotatesubrepoerror
1620 1620 def merge(self, state):
1621 1621 source, revision, kind = state
1622 1622 self._fetch(source, revision)
1623 1623 base = self._gitcommand(['merge-base', revision, self._state[1]])
1624 1624 self._gitupdatestat()
1625 1625 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1626 1626
1627 1627 def mergefunc():
1628 1628 if base == revision:
1629 1629 self.get(state) # fast forward merge
1630 1630 elif base != self._state[1]:
1631 1631 self._gitcommand(['merge', '--no-commit', revision])
1632 1632 _sanitize(self.ui, self.wvfs, '.git')
1633 1633
1634 1634 if self.dirty():
1635 1635 if self._gitstate() != revision:
1636 1636 dirty = self._gitstate() == self._state[1] or code != 0
1637 1637 if _updateprompt(self.ui, self, dirty,
1638 1638 self._state[1][:7], revision[:7]):
1639 1639 mergefunc()
1640 1640 else:
1641 1641 mergefunc()
1642 1642
1643 1643 @annotatesubrepoerror
1644 1644 def push(self, opts):
1645 1645 force = opts.get('force')
1646 1646
1647 1647 if not self._state[1]:
1648 1648 return True
1649 1649 if self._gitmissing():
1650 1650 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1651 1651 # if a branch in origin contains the revision, nothing to do
1652 1652 branch2rev, rev2branch = self._gitbranchmap()
1653 1653 if self._state[1] in rev2branch:
1654 1654 for b in rev2branch[self._state[1]]:
1655 1655 if b.startswith('refs/remotes/origin/'):
1656 1656 return True
1657 1657 for b, revision in branch2rev.iteritems():
1658 1658 if b.startswith('refs/remotes/origin/'):
1659 1659 if self._gitisancestor(self._state[1], revision):
1660 1660 return True
1661 1661 # otherwise, try to push the currently checked out branch
1662 1662 cmd = ['push']
1663 1663 if force:
1664 1664 cmd.append('--force')
1665 1665
1666 1666 current = self._gitcurrentbranch()
1667 1667 if current:
1668 1668 # determine if the current branch is even useful
1669 1669 if not self._gitisancestor(self._state[1], current):
1670 1670 self.ui.warn(_('unrelated git branch checked out '
1671 1671 'in subrepo %s\n') % self._relpath)
1672 1672 return False
1673 1673 self.ui.status(_('pushing branch %s of subrepo %s\n') %
1674 1674 (current.split('/', 2)[2], self._relpath))
1675 1675 ret = self._gitdir(cmd + ['origin', current])
1676 1676 return ret[1] == 0
1677 1677 else:
1678 1678 self.ui.warn(_('no branch checked out in subrepo %s\n'
1679 1679 'cannot push revision %s\n') %
1680 1680 (self._relpath, self._state[1]))
1681 1681 return False
1682 1682
1683 1683 @annotatesubrepoerror
1684 1684 def add(self, ui, match, prefix, explicitonly, **opts):
1685 1685 if self._gitmissing():
1686 1686 return []
1687 1687
1688 1688 (modified, added, removed,
1689 1689 deleted, unknown, ignored, clean) = self.status(None, unknown=True,
1690 1690 clean=True)
1691 1691
1692 1692 tracked = set()
1693 1693 # dirstates 'amn' warn, 'r' is added again
1694 1694 for l in (modified, added, deleted, clean):
1695 1695 tracked.update(l)
1696 1696
1697 1697 # Unknown files not of interest will be rejected by the matcher
1698 1698 files = unknown
1699 1699 files.extend(match.files())
1700 1700
1701 1701 rejected = []
1702 1702
1703 1703 files = [f for f in sorted(set(files)) if match(f)]
1704 1704 for f in files:
1705 1705 exact = match.exact(f)
1706 1706 command = ["add"]
1707 1707 if exact:
1708 1708 command.append("-f") #should be added, even if ignored
1709 1709 if ui.verbose or not exact:
1710 1710 ui.status(_('adding %s\n') % match.rel(f))
1711 1711
1712 1712 if f in tracked: # hg prints 'adding' even if already tracked
1713 1713 if exact:
1714 1714 rejected.append(f)
1715 1715 continue
1716 1716 if not opts.get('dry_run'):
1717 1717 self._gitcommand(command + [f])
1718 1718
1719 1719 for f in rejected:
1720 1720 ui.warn(_("%s already tracked!\n") % match.abs(f))
1721 1721
1722 1722 return rejected
1723 1723
1724 1724 @annotatesubrepoerror
1725 1725 def remove(self):
1726 1726 if self._gitmissing():
1727 1727 return
1728 1728 if self.dirty():
1729 1729 self.ui.warn(_('not removing repo %s because '
1730 1730 'it has changes.\n') % self._relpath)
1731 1731 return
1732 1732 # we can't fully delete the repository as it may contain
1733 1733 # local-only history
1734 1734 self.ui.note(_('removing subrepo %s\n') % self._relpath)
1735 1735 self._gitcommand(['config', 'core.bare', 'true'])
1736 1736 for f, kind in self.wvfs.readdir():
1737 1737 if f == '.git':
1738 1738 continue
1739 1739 if kind == stat.S_IFDIR:
1740 1740 self.wvfs.rmtree(f)
1741 1741 else:
1742 1742 self.wvfs.unlink(f)
1743 1743
1744 1744 def archive(self, archiver, prefix, match=None):
1745 1745 total = 0
1746 1746 source, revision = self._state
1747 1747 if not revision:
1748 1748 return total
1749 1749 self._fetch(source, revision)
1750 1750
1751 1751 # Parse git's native archive command.
1752 1752 # This should be much faster than manually traversing the trees
1753 1753 # and objects with many subprocess calls.
1754 1754 tarstream = self._gitcommand(['archive', revision], stream=True)
1755 1755 tar = tarfile.open(fileobj=tarstream, mode='r|')
1756 1756 relpath = subrelpath(self)
1757 1757 self.ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1758 1758 for i, info in enumerate(tar):
1759 1759 if info.isdir():
1760 1760 continue
1761 1761 if match and not match(info.name):
1762 1762 continue
1763 1763 if info.issym():
1764 1764 data = info.linkname
1765 1765 else:
1766 1766 data = tar.extractfile(info).read()
1767 1767 archiver.addfile(prefix + self._path + '/' + info.name,
1768 1768 info.mode, info.issym(), data)
1769 1769 total += 1
1770 1770 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
1771 1771 unit=_('files'))
1772 1772 self.ui.progress(_('archiving (%s)') % relpath, None)
1773 1773 return total
1774 1774
1775 1775
1776 1776 @annotatesubrepoerror
1777 1777 def cat(self, match, prefix, **opts):
1778 1778 rev = self._state[1]
1779 1779 if match.anypats():
1780 1780 return 1 #No support for include/exclude yet
1781 1781
1782 1782 if not match.files():
1783 1783 return 1
1784 1784
1785 1785 for f in match.files():
1786 1786 output = self._gitcommand(["show", "%s:%s" % (rev, f)])
1787 1787 fp = cmdutil.makefileobj(self._subparent, opts.get('output'),
1788 1788 self._ctx.node(),
1789 1789 pathname=self.wvfs.reljoin(prefix, f))
1790 1790 fp.write(output)
1791 1791 fp.close()
1792 1792 return 0
1793 1793
1794 1794
1795 1795 @annotatesubrepoerror
1796 1796 def status(self, rev2, **opts):
1797 1797 rev1 = self._state[1]
1798 1798 if self._gitmissing() or not rev1:
1799 1799 # if the repo is missing, return no results
1800 1800 return scmutil.status([], [], [], [], [], [], [])
1801 1801 modified, added, removed = [], [], []
1802 1802 self._gitupdatestat()
1803 1803 if rev2:
1804 1804 command = ['diff-tree', '-r', rev1, rev2]
1805 1805 else:
1806 1806 command = ['diff-index', rev1]
1807 1807 out = self._gitcommand(command)
1808 1808 for line in out.split('\n'):
1809 1809 tab = line.find('\t')
1810 1810 if tab == -1:
1811 1811 continue
1812 1812 status, f = line[tab - 1], line[tab + 1:]
1813 1813 if status == 'M':
1814 1814 modified.append(f)
1815 1815 elif status == 'A':
1816 1816 added.append(f)
1817 1817 elif status == 'D':
1818 1818 removed.append(f)
1819 1819
1820 1820 deleted, unknown, ignored, clean = [], [], [], []
1821 1821
1822 1822 command = ['status', '--porcelain', '-z']
1823 1823 if opts.get('unknown'):
1824 1824 command += ['--untracked-files=all']
1825 1825 if opts.get('ignored'):
1826 1826 command += ['--ignored']
1827 1827 out = self._gitcommand(command)
1828 1828
1829 1829 changedfiles = set()
1830 1830 changedfiles.update(modified)
1831 1831 changedfiles.update(added)
1832 1832 changedfiles.update(removed)
1833 1833 for line in out.split('\0'):
1834 1834 if not line:
1835 1835 continue
1836 1836 st = line[0:2]
1837 1837 #moves and copies show 2 files on one line
1838 1838 if line.find('\0') >= 0:
1839 1839 filename1, filename2 = line[3:].split('\0')
1840 1840 else:
1841 1841 filename1 = line[3:]
1842 1842 filename2 = None
1843 1843
1844 1844 changedfiles.add(filename1)
1845 1845 if filename2:
1846 1846 changedfiles.add(filename2)
1847 1847
1848 1848 if st == '??':
1849 1849 unknown.append(filename1)
1850 1850 elif st == '!!':
1851 1851 ignored.append(filename1)
1852 1852
1853 1853 if opts.get('clean'):
1854 1854 out = self._gitcommand(['ls-files'])
1855 1855 for f in out.split('\n'):
1856 1856 if not f in changedfiles:
1857 1857 clean.append(f)
1858 1858
1859 1859 return scmutil.status(modified, added, removed, deleted,
1860 1860 unknown, ignored, clean)
1861 1861
1862 1862 @annotatesubrepoerror
1863 1863 def diff(self, ui, diffopts, node2, match, prefix, **opts):
1864 1864 node1 = self._state[1]
1865 1865 cmd = ['diff']
1866 1866 if opts['stat']:
1867 1867 cmd.append('--stat')
1868 1868 else:
1869 1869 # for Git, this also implies '-p'
1870 1870 cmd.append('-U%d' % diffopts.context)
1871 1871
1872 1872 gitprefix = self.wvfs.reljoin(prefix, self._path)
1873 1873
1874 1874 if diffopts.noprefix:
1875 1875 cmd.extend(['--src-prefix=%s/' % gitprefix,
1876 1876 '--dst-prefix=%s/' % gitprefix])
1877 1877 else:
1878 1878 cmd.extend(['--src-prefix=a/%s/' % gitprefix,
1879 1879 '--dst-prefix=b/%s/' % gitprefix])
1880 1880
1881 1881 if diffopts.ignorews:
1882 1882 cmd.append('--ignore-all-space')
1883 1883 if diffopts.ignorewsamount:
1884 1884 cmd.append('--ignore-space-change')
1885 1885 if self._gitversion(self._gitcommand(['--version'])) >= (1, 8, 4) \
1886 1886 and diffopts.ignoreblanklines:
1887 1887 cmd.append('--ignore-blank-lines')
1888 1888
1889 1889 cmd.append(node1)
1890 1890 if node2:
1891 1891 cmd.append(node2)
1892 1892
1893 1893 output = ""
1894 1894 if match.always():
1895 1895 output += self._gitcommand(cmd) + '\n'
1896 1896 else:
1897 1897 st = self.status(node2)[:3]
1898 1898 files = [f for sublist in st for f in sublist]
1899 1899 for f in files:
1900 1900 if match(f):
1901 1901 output += self._gitcommand(cmd + ['--', f]) + '\n'
1902 1902
1903 1903 if output.strip():
1904 1904 ui.write(output)
1905 1905
1906 1906 @annotatesubrepoerror
1907 1907 def revert(self, substate, *pats, **opts):
1908 1908 self.ui.status(_('reverting subrepo %s\n') % substate[0])
1909 1909 if not opts.get('no_backup'):
1910 1910 status = self.status(None)
1911 1911 names = status.modified
1912 1912 for name in names:
1913 bakname = "%s.orig" % name
1913 bakname = cmdutil.origpath(self.ui, self._subparent, name)
1914 1914 self.ui.note(_('saving current version of %s as %s\n') %
1915 1915 (name, bakname))
1916 1916 self.wvfs.rename(name, bakname)
1917 1917
1918 1918 if not opts.get('dry_run'):
1919 1919 self.get(substate, overwrite=True)
1920 1920 return []
1921 1921
1922 1922 def shortid(self, revid):
1923 1923 return revid[:7]
1924 1924
1925 1925 types = {
1926 1926 'hg': hgsubrepo,
1927 1927 'svn': svnsubrepo,
1928 1928 'git': gitsubrepo,
1929 1929 }
@@ -1,1125 +1,1135 b''
1 1 #require git
2 2
3 3 make git commits repeatable
4 4
5 5 $ echo "[core]" >> $HOME/.gitconfig
6 6 $ echo "autocrlf = false" >> $HOME/.gitconfig
7 7 $ GIT_AUTHOR_NAME='test'; export GIT_AUTHOR_NAME
8 8 $ GIT_AUTHOR_EMAIL='test@example.org'; export GIT_AUTHOR_EMAIL
9 9 $ GIT_AUTHOR_DATE='1234567891 +0000'; export GIT_AUTHOR_DATE
10 10 $ GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"; export GIT_COMMITTER_NAME
11 11 $ GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"; export GIT_COMMITTER_EMAIL
12 12 $ GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"; export GIT_COMMITTER_DATE
13 13 $ GIT_CONFIG_NOSYSTEM=1; export GIT_CONFIG_NOSYSTEM
14 14
15 15 root hg repo
16 16
17 17 $ hg init t
18 18 $ cd t
19 19 $ echo a > a
20 20 $ hg add a
21 21 $ hg commit -m a
22 22 $ cd ..
23 23
24 24 new external git repo
25 25
26 26 $ mkdir gitroot
27 27 $ cd gitroot
28 28 $ git init -q
29 29 $ echo g > g
30 30 $ git add g
31 31 $ git commit -q -m g
32 32
33 33 add subrepo clone
34 34
35 35 $ cd ../t
36 36 $ echo 's = [git]../gitroot' > .hgsub
37 37 $ git clone -q ../gitroot s
38 38 $ hg add .hgsub
39 39 $ hg commit -m 'new git subrepo'
40 40 $ hg debugsub
41 41 path s
42 42 source ../gitroot
43 43 revision da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
44 44
45 45 record a new commit from upstream from a different branch
46 46
47 47 $ cd ../gitroot
48 48 $ git checkout -q -b testing
49 49 $ echo gg >> g
50 50 $ git commit -q -a -m gg
51 51
52 52 $ cd ../t/s
53 53 $ git pull -q >/dev/null 2>/dev/null
54 54 $ git checkout -q -b testing origin/testing >/dev/null
55 55
56 56 $ cd ..
57 57 $ hg status --subrepos
58 58 M s/g
59 59 $ hg commit -m 'update git subrepo'
60 60 $ hg debugsub
61 61 path s
62 62 source ../gitroot
63 63 revision 126f2a14290cd5ce061fdedc430170e8d39e1c5a
64 64
65 65 make $GITROOT pushable, by replacing it with a clone with nothing checked out
66 66
67 67 $ cd ..
68 68 $ git clone gitroot gitrootbare --bare -q
69 69 $ rm -rf gitroot
70 70 $ mv gitrootbare gitroot
71 71
72 72 clone root
73 73
74 74 $ cd t
75 75 $ hg clone . ../tc 2> /dev/null
76 76 updating to branch default
77 77 cloning subrepo s from $TESTTMP/gitroot
78 78 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
79 79 $ cd ../tc
80 80 $ hg debugsub
81 81 path s
82 82 source ../gitroot
83 83 revision 126f2a14290cd5ce061fdedc430170e8d39e1c5a
84 84
85 85 update to previous substate
86 86
87 87 $ hg update 1 -q
88 88 $ cat s/g
89 89 g
90 90 $ hg debugsub
91 91 path s
92 92 source ../gitroot
93 93 revision da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
94 94
95 95 clone root, make local change
96 96
97 97 $ cd ../t
98 98 $ hg clone . ../ta 2> /dev/null
99 99 updating to branch default
100 100 cloning subrepo s from $TESTTMP/gitroot
101 101 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
102 102
103 103 $ cd ../ta
104 104 $ echo ggg >> s/g
105 105 $ hg status --subrepos
106 106 M s/g
107 107 $ hg diff --subrepos
108 108 diff --git a/s/g b/s/g
109 109 index 089258f..85341ee 100644
110 110 --- a/s/g
111 111 +++ b/s/g
112 112 @@ -1,2 +1,3 @@
113 113 g
114 114 gg
115 115 +ggg
116 116 $ hg commit --subrepos -m ggg
117 117 committing subrepository s
118 118 $ hg debugsub
119 119 path s
120 120 source ../gitroot
121 121 revision 79695940086840c99328513acbe35f90fcd55e57
122 122
123 123 clone root separately, make different local change
124 124
125 125 $ cd ../t
126 126 $ hg clone . ../tb 2> /dev/null
127 127 updating to branch default
128 128 cloning subrepo s from $TESTTMP/gitroot
129 129 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
130 130
131 131 $ cd ../tb/s
132 132 $ hg status --subrepos
133 133 $ echo f > f
134 134 $ hg status --subrepos
135 135 ? s/f
136 136 $ hg add .
137 137 adding f
138 138 $ git add f
139 139 $ cd ..
140 140
141 141 $ hg status --subrepos
142 142 A s/f
143 143 $ hg commit --subrepos -m f
144 144 committing subrepository s
145 145 $ hg debugsub
146 146 path s
147 147 source ../gitroot
148 148 revision aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
149 149
150 150 user b push changes
151 151
152 152 $ hg push 2>/dev/null
153 153 pushing to $TESTTMP/t (glob)
154 154 pushing branch testing of subrepo s
155 155 searching for changes
156 156 adding changesets
157 157 adding manifests
158 158 adding file changes
159 159 added 1 changesets with 1 changes to 1 files
160 160
161 161 user a pulls, merges, commits
162 162
163 163 $ cd ../ta
164 164 $ hg pull
165 165 pulling from $TESTTMP/t (glob)
166 166 searching for changes
167 167 adding changesets
168 168 adding manifests
169 169 adding file changes
170 170 added 1 changesets with 1 changes to 1 files (+1 heads)
171 171 (run 'hg heads' to see heads, 'hg merge' to merge)
172 172 $ hg merge 2>/dev/null
173 173 subrepository s diverged (local revision: 7969594, remote revision: aa84837)
174 174 (M)erge, keep (l)ocal or keep (r)emote? m
175 175 pulling subrepo s from $TESTTMP/gitroot
176 176 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
177 177 (branch merge, don't forget to commit)
178 178 $ hg st --subrepos s
179 179 A s/f
180 180 $ cat s/f
181 181 f
182 182 $ cat s/g
183 183 g
184 184 gg
185 185 ggg
186 186 $ hg commit --subrepos -m 'merge'
187 187 committing subrepository s
188 188 $ hg status --subrepos --rev 1:5
189 189 M .hgsubstate
190 190 M s/g
191 191 A s/f
192 192 $ hg debugsub
193 193 path s
194 194 source ../gitroot
195 195 revision f47b465e1bce645dbf37232a00574aa1546ca8d3
196 196 $ hg push 2>/dev/null
197 197 pushing to $TESTTMP/t (glob)
198 198 pushing branch testing of subrepo s
199 199 searching for changes
200 200 adding changesets
201 201 adding manifests
202 202 adding file changes
203 203 added 2 changesets with 2 changes to 1 files
204 204
205 205 make upstream git changes
206 206
207 207 $ cd ..
208 208 $ git clone -q gitroot gitclone
209 209 $ cd gitclone
210 210 $ echo ff >> f
211 211 $ git commit -q -a -m ff
212 212 $ echo fff >> f
213 213 $ git commit -q -a -m fff
214 214 $ git push origin testing 2>/dev/null
215 215
216 216 make and push changes to hg without updating the subrepo
217 217
218 218 $ cd ../t
219 219 $ hg clone . ../td 2>&1 | egrep -v '^Cloning into|^done\.'
220 220 updating to branch default
221 221 cloning subrepo s from $TESTTMP/gitroot
222 222 checking out detached HEAD in subrepo s
223 223 check out a git branch if you intend to make changes
224 224 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
225 225 $ cd ../td
226 226 $ echo aa >> a
227 227 $ hg commit -m aa
228 228 $ hg push
229 229 pushing to $TESTTMP/t (glob)
230 230 searching for changes
231 231 adding changesets
232 232 adding manifests
233 233 adding file changes
234 234 added 1 changesets with 1 changes to 1 files
235 235
236 236 sync to upstream git, distribute changes
237 237
238 238 $ cd ../ta
239 239 $ hg pull -u -q
240 240 $ cd s
241 241 $ git pull -q >/dev/null 2>/dev/null
242 242 $ cd ..
243 243 $ hg commit -m 'git upstream sync'
244 244 $ hg debugsub
245 245 path s
246 246 source ../gitroot
247 247 revision 32a343883b74769118bb1d3b4b1fbf9156f4dddc
248 248 $ hg push -q
249 249
250 250 $ cd ../tb
251 251 $ hg pull -q
252 252 $ hg update 2>/dev/null
253 253 pulling subrepo s from $TESTTMP/gitroot
254 254 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
255 255 $ hg debugsub
256 256 path s
257 257 source ../gitroot
258 258 revision 32a343883b74769118bb1d3b4b1fbf9156f4dddc
259 259
260 260 create a new git branch
261 261
262 262 $ cd s
263 263 $ git checkout -b b2
264 264 Switched to a new branch 'b2'
265 265 $ echo a>a
266 266 $ git add a
267 267 $ git commit -qm 'add a'
268 268 $ cd ..
269 269 $ hg commit -m 'add branch in s'
270 270
271 271 pulling new git branch should not create tracking branch named 'origin/b2'
272 272 (issue3870)
273 273 $ cd ../td/s
274 274 $ git remote set-url origin $TESTTMP/tb/s
275 275 $ git branch --no-track oldtesting
276 276 $ cd ..
277 277 $ hg pull -q ../tb
278 278 $ hg up
279 279 From $TESTTMP/tb/s
280 280 * [new branch] b2 -> origin/b2
281 281 Previous HEAD position was f47b465... merge
282 282 Switched to a new branch 'b2'
283 283 pulling subrepo s from $TESTTMP/tb/s
284 284 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
285 285
286 286 update to a revision without the subrepo, keeping the local git repository
287 287
288 288 $ cd ../t
289 289 $ hg up 0
290 290 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
291 291 $ ls -a s
292 292 .
293 293 ..
294 294 .git
295 295
296 296 $ hg up 2
297 297 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
298 298 $ ls -a s
299 299 .
300 300 ..
301 301 .git
302 302 g
303 303
304 304 archive subrepos
305 305
306 306 $ cd ../tc
307 307 $ hg pull -q
308 308 $ hg archive --subrepos -r 5 ../archive 2>/dev/null
309 309 pulling subrepo s from $TESTTMP/gitroot
310 310 $ cd ../archive
311 311 $ cat s/f
312 312 f
313 313 $ cat s/g
314 314 g
315 315 gg
316 316 ggg
317 317
318 318 $ hg -R ../tc archive --subrepo -r 5 -X ../tc/**f ../archive_x 2>/dev/null
319 319 $ find ../archive_x | sort | grep -v pax_global_header
320 320 ../archive_x
321 321 ../archive_x/.hg_archival.txt
322 322 ../archive_x/.hgsub
323 323 ../archive_x/.hgsubstate
324 324 ../archive_x/a
325 325 ../archive_x/s
326 326 ../archive_x/s/g
327 327
328 328 $ hg -R ../tc archive -S ../archive.tgz --prefix '.' 2>/dev/null
329 329 $ tar -tzf ../archive.tgz | sort | grep -v pax_global_header
330 330 .hg_archival.txt
331 331 .hgsub
332 332 .hgsubstate
333 333 a
334 334 s/g
335 335
336 336 create nested repo
337 337
338 338 $ cd ..
339 339 $ hg init outer
340 340 $ cd outer
341 341 $ echo b>b
342 342 $ hg add b
343 343 $ hg commit -m b
344 344
345 345 $ hg clone ../t inner 2> /dev/null
346 346 updating to branch default
347 347 cloning subrepo s from $TESTTMP/gitroot
348 348 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
349 349 $ echo inner = inner > .hgsub
350 350 $ hg add .hgsub
351 351 $ hg commit -m 'nested sub'
352 352
353 353 nested commit
354 354
355 355 $ echo ffff >> inner/s/f
356 356 $ hg status --subrepos
357 357 M inner/s/f
358 358 $ hg commit --subrepos -m nested
359 359 committing subrepository inner
360 360 committing subrepository inner/s (glob)
361 361
362 362 nested archive
363 363
364 364 $ hg archive --subrepos ../narchive
365 365 $ ls ../narchive/inner/s | grep -v pax_global_header
366 366 f
367 367 g
368 368
369 369 relative source expansion
370 370
371 371 $ cd ..
372 372 $ mkdir d
373 373 $ hg clone t d/t 2> /dev/null
374 374 updating to branch default
375 375 cloning subrepo s from $TESTTMP/gitroot
376 376 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
377 377
378 378 Don't crash if the subrepo is missing
379 379
380 380 $ hg clone t missing -q
381 381 $ cd missing
382 382 $ rm -rf s
383 383 $ hg status -S
384 384 $ hg sum | grep commit
385 385 commit: 1 subrepos
386 386 $ hg push -q
387 387 abort: subrepo s is missing (in subrepo s)
388 388 [255]
389 389 $ hg commit --subrepos -qm missing
390 390 abort: subrepo s is missing (in subrepo s)
391 391 [255]
392 392
393 393 #if symlink
394 394 Don't crash if subrepo is a broken symlink
395 395 $ ln -s broken s
396 396 $ hg status -S
397 397 $ hg push -q
398 398 abort: subrepo s is missing (in subrepo s)
399 399 [255]
400 400 $ hg commit --subrepos -qm missing
401 401 abort: subrepo s is missing (in subrepo s)
402 402 [255]
403 403 $ rm s
404 404 #endif
405 405
406 406 $ hg update -C 2> /dev/null
407 407 cloning subrepo s from $TESTTMP/gitroot
408 408 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
409 409 $ hg sum | grep commit
410 410 commit: (clean)
411 411
412 412 Don't crash if the .hgsubstate entry is missing
413 413
414 414 $ hg update 1 -q
415 415 $ hg rm .hgsubstate
416 416 $ hg commit .hgsubstate -m 'no substate'
417 417 nothing changed
418 418 [1]
419 419 $ hg tag -l nosubstate
420 420 $ hg manifest
421 421 .hgsub
422 422 .hgsubstate
423 423 a
424 424
425 425 $ hg status -S
426 426 R .hgsubstate
427 427 $ hg sum | grep commit
428 428 commit: 1 removed, 1 subrepos (new branch head)
429 429
430 430 $ hg commit -m 'restore substate'
431 431 nothing changed
432 432 [1]
433 433 $ hg manifest
434 434 .hgsub
435 435 .hgsubstate
436 436 a
437 437 $ hg sum | grep commit
438 438 commit: 1 removed, 1 subrepos (new branch head)
439 439
440 440 $ hg update -qC nosubstate
441 441 $ ls s
442 442 g
443 443
444 444 issue3109: false positives in git diff-index
445 445
446 446 $ hg update -q
447 447 $ touch -t 200001010000 s/g
448 448 $ hg status --subrepos
449 449 $ touch -t 200001010000 s/g
450 450 $ hg sum | grep commit
451 451 commit: (clean)
452 452
453 453 Check hg update --clean
454 454 $ cd $TESTTMP/ta
455 455 $ echo > s/g
456 456 $ cd s
457 457 $ echo c1 > f1
458 458 $ echo c1 > f2
459 459 $ git add f1
460 460 $ cd ..
461 461 $ hg status -S
462 462 M s/g
463 463 A s/f1
464 464 ? s/f2
465 465 $ ls s
466 466 f
467 467 f1
468 468 f2
469 469 g
470 470 $ hg update --clean
471 471 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
472 472 $ hg status -S
473 473 ? s/f1
474 474 ? s/f2
475 475 $ ls s
476 476 f
477 477 f1
478 478 f2
479 479 g
480 480
481 481 Sticky subrepositories, no changes
482 482 $ cd $TESTTMP/ta
483 483 $ hg id -n
484 484 7
485 485 $ cd s
486 486 $ git rev-parse HEAD
487 487 32a343883b74769118bb1d3b4b1fbf9156f4dddc
488 488 $ cd ..
489 489 $ hg update 1 > /dev/null 2>&1
490 490 $ hg id -n
491 491 1
492 492 $ cd s
493 493 $ git rev-parse HEAD
494 494 da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
495 495 $ cd ..
496 496
497 497 Sticky subrepositories, file changes
498 498 $ touch s/f1
499 499 $ cd s
500 500 $ git add f1
501 501 $ cd ..
502 502 $ hg id -n
503 503 1+
504 504 $ cd s
505 505 $ git rev-parse HEAD
506 506 da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
507 507 $ cd ..
508 508 $ hg update 4
509 509 subrepository s diverged (local revision: da5f5b1, remote revision: aa84837)
510 510 (M)erge, keep (l)ocal or keep (r)emote? m
511 511 subrepository sources for s differ
512 512 use (l)ocal source (da5f5b1) or (r)emote source (aa84837)? l
513 513 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
514 514 $ hg id -n
515 515 4+
516 516 $ cd s
517 517 $ git rev-parse HEAD
518 518 da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
519 519 $ cd ..
520 520 $ hg update --clean tip > /dev/null 2>&1
521 521
522 522 Sticky subrepository, revision updates
523 523 $ hg id -n
524 524 7
525 525 $ cd s
526 526 $ git rev-parse HEAD
527 527 32a343883b74769118bb1d3b4b1fbf9156f4dddc
528 528 $ cd ..
529 529 $ cd s
530 530 $ git checkout aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
531 531 Previous HEAD position was 32a3438... fff
532 532 HEAD is now at aa84837... f
533 533 $ cd ..
534 534 $ hg update 1
535 535 subrepository s diverged (local revision: 32a3438, remote revision: da5f5b1)
536 536 (M)erge, keep (l)ocal or keep (r)emote? m
537 537 subrepository sources for s differ (in checked out version)
538 538 use (l)ocal source (32a3438) or (r)emote source (da5f5b1)? l
539 539 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
540 540 $ hg id -n
541 541 1+
542 542 $ cd s
543 543 $ git rev-parse HEAD
544 544 aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
545 545 $ cd ..
546 546
547 547 Sticky subrepository, file changes and revision updates
548 548 $ touch s/f1
549 549 $ cd s
550 550 $ git add f1
551 551 $ git rev-parse HEAD
552 552 aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
553 553 $ cd ..
554 554 $ hg id -n
555 555 1+
556 556 $ hg update 7
557 557 subrepository s diverged (local revision: 32a3438, remote revision: 32a3438)
558 558 (M)erge, keep (l)ocal or keep (r)emote? m
559 559 subrepository sources for s differ
560 560 use (l)ocal source (32a3438) or (r)emote source (32a3438)? l
561 561 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
562 562 $ hg id -n
563 563 7+
564 564 $ cd s
565 565 $ git rev-parse HEAD
566 566 aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
567 567 $ cd ..
568 568
569 569 Sticky repository, update --clean
570 570 $ hg update --clean tip 2>/dev/null
571 571 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
572 572 $ hg id -n
573 573 7
574 574 $ cd s
575 575 $ git rev-parse HEAD
576 576 32a343883b74769118bb1d3b4b1fbf9156f4dddc
577 577 $ cd ..
578 578
579 579 Test subrepo already at intended revision:
580 580 $ cd s
581 581 $ git checkout 32a343883b74769118bb1d3b4b1fbf9156f4dddc
582 582 HEAD is now at 32a3438... fff
583 583 $ cd ..
584 584 $ hg update 1
585 585 Previous HEAD position was 32a3438... fff
586 586 HEAD is now at da5f5b1... g
587 587 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
588 588 $ hg id -n
589 589 1
590 590 $ cd s
591 591 $ git rev-parse HEAD
592 592 da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
593 593 $ cd ..
594 594
595 595 Test forgetting files, not implemented in git subrepo, used to
596 596 traceback
597 597 #if no-windows
598 598 $ hg forget 'notafile*'
599 599 notafile*: No such file or directory
600 600 [1]
601 601 #else
602 602 $ hg forget 'notafile'
603 603 notafile: * (glob)
604 604 [1]
605 605 #endif
606 606
607 607 $ cd ..
608 608
609 609 Test sanitizing ".hg/hgrc" in subrepo
610 610
611 611 $ cd t
612 612 $ hg tip -q
613 613 7:af6d2edbb0d3
614 614 $ hg update -q -C af6d2edbb0d3
615 615 $ cd s
616 616 $ git checkout -q -b sanitize-test
617 617 $ mkdir .hg
618 618 $ echo '.hg/hgrc in git repo' > .hg/hgrc
619 619 $ mkdir -p sub/.hg
620 620 $ echo 'sub/.hg/hgrc in git repo' > sub/.hg/hgrc
621 621 $ git add .hg sub
622 622 $ git commit -qm 'add .hg/hgrc to be sanitized at hg update'
623 623 $ git push -q origin sanitize-test
624 624 $ cd ..
625 625 $ grep ' s$' .hgsubstate
626 626 32a343883b74769118bb1d3b4b1fbf9156f4dddc s
627 627 $ hg commit -qm 'commit with git revision including .hg/hgrc'
628 628 $ hg parents -q
629 629 8:3473d20bddcf
630 630 $ grep ' s$' .hgsubstate
631 631 c4069473b459cf27fd4d7c2f50c4346b4e936599 s
632 632 $ cd ..
633 633
634 634 $ hg -R tc pull -q
635 635 $ hg -R tc update -q -C 3473d20bddcf 2>&1 | sort
636 636 warning: removing potentially hostile 'hgrc' in '$TESTTMP/tc/s/.hg' (glob)
637 637 warning: removing potentially hostile 'hgrc' in '$TESTTMP/tc/s/sub/.hg' (glob)
638 638 $ cd tc
639 639 $ hg parents -q
640 640 8:3473d20bddcf
641 641 $ grep ' s$' .hgsubstate
642 642 c4069473b459cf27fd4d7c2f50c4346b4e936599 s
643 643 $ test -f s/.hg/hgrc
644 644 [1]
645 645 $ test -f s/sub/.hg/hgrc
646 646 [1]
647 647 $ cd ..
648 648
649 649 additional test for "git merge --ff" route:
650 650
651 651 $ cd t
652 652 $ hg tip -q
653 653 8:3473d20bddcf
654 654 $ hg update -q -C af6d2edbb0d3
655 655 $ cd s
656 656 $ git checkout -q testing
657 657 $ mkdir .hg
658 658 $ echo '.hg/hgrc in git repo' > .hg/hgrc
659 659 $ mkdir -p sub/.hg
660 660 $ echo 'sub/.hg/hgrc in git repo' > sub/.hg/hgrc
661 661 $ git add .hg sub
662 662 $ git commit -qm 'add .hg/hgrc to be sanitized at hg update (git merge --ff)'
663 663 $ git push -q origin testing
664 664 $ cd ..
665 665 $ grep ' s$' .hgsubstate
666 666 32a343883b74769118bb1d3b4b1fbf9156f4dddc s
667 667 $ hg commit -qm 'commit with git revision including .hg/hgrc'
668 668 $ hg parents -q
669 669 9:ed23f7fe024e
670 670 $ grep ' s$' .hgsubstate
671 671 f262643c1077219fbd3858d54e78ef050ef84fbf s
672 672 $ cd ..
673 673
674 674 $ cd tc
675 675 $ hg update -q -C af6d2edbb0d3
676 676 $ test -f s/.hg/hgrc
677 677 [1]
678 678 $ test -f s/sub/.hg/hgrc
679 679 [1]
680 680 $ cd ..
681 681 $ hg -R tc pull -q
682 682 $ hg -R tc update -q -C ed23f7fe024e 2>&1 | sort
683 683 warning: removing potentially hostile 'hgrc' in '$TESTTMP/tc/s/.hg' (glob)
684 684 warning: removing potentially hostile 'hgrc' in '$TESTTMP/tc/s/sub/.hg' (glob)
685 685 $ cd tc
686 686 $ hg parents -q
687 687 9:ed23f7fe024e
688 688 $ grep ' s$' .hgsubstate
689 689 f262643c1077219fbd3858d54e78ef050ef84fbf s
690 690 $ test -f s/.hg/hgrc
691 691 [1]
692 692 $ test -f s/sub/.hg/hgrc
693 693 [1]
694 694
695 695 Test that sanitizing is omitted in meta data area:
696 696
697 697 $ mkdir s/.git/.hg
698 698 $ echo '.hg/hgrc in git metadata area' > s/.git/.hg/hgrc
699 699 $ hg update -q -C af6d2edbb0d3
700 700 checking out detached HEAD in subrepo s
701 701 check out a git branch if you intend to make changes
702 702
703 703 check differences made by most recent change
704 704 $ cd s
705 705 $ cat > foobar << EOF
706 706 > woopwoop
707 707 >
708 708 > foo
709 709 > bar
710 710 > EOF
711 711 $ git add foobar
712 712 $ cd ..
713 713
714 714 $ hg diff --subrepos
715 715 diff --git a/s/foobar b/s/foobar
716 716 new file mode 100644
717 717 index 0000000..8a5a5e2
718 718 --- /dev/null
719 719 +++ b/s/foobar
720 720 @@ -0,0 +1,4 @@
721 721 +woopwoop
722 722 +
723 723 +foo
724 724 +bar
725 725
726 726 $ hg commit --subrepos -m "Added foobar"
727 727 committing subrepository s
728 728 created new head
729 729
730 730 $ hg diff -c . --subrepos --nodates
731 731 diff -r af6d2edbb0d3 -r 255ee8cf690e .hgsubstate
732 732 --- a/.hgsubstate
733 733 +++ b/.hgsubstate
734 734 @@ -1,1 +1,1 @@
735 735 -32a343883b74769118bb1d3b4b1fbf9156f4dddc s
736 736 +fd4dbf828a5b2fcd36b2bcf21ea773820970d129 s
737 737 diff --git a/s/foobar b/s/foobar
738 738 new file mode 100644
739 739 index 0000000..8a5a5e2
740 740 --- /dev/null
741 741 +++ b/s/foobar
742 742 @@ -0,0 +1,4 @@
743 743 +woopwoop
744 744 +
745 745 +foo
746 746 +bar
747 747
748 748 check output when only diffing the subrepository
749 749 $ hg diff -c . --subrepos s
750 750 diff --git a/s/foobar b/s/foobar
751 751 new file mode 100644
752 752 index 0000000..8a5a5e2
753 753 --- /dev/null
754 754 +++ b/s/foobar
755 755 @@ -0,0 +1,4 @@
756 756 +woopwoop
757 757 +
758 758 +foo
759 759 +bar
760 760
761 761 check output when diffing something else
762 762 $ hg diff -c . --subrepos .hgsubstate --nodates
763 763 diff -r af6d2edbb0d3 -r 255ee8cf690e .hgsubstate
764 764 --- a/.hgsubstate
765 765 +++ b/.hgsubstate
766 766 @@ -1,1 +1,1 @@
767 767 -32a343883b74769118bb1d3b4b1fbf9156f4dddc s
768 768 +fd4dbf828a5b2fcd36b2bcf21ea773820970d129 s
769 769
770 770 add new changes, including whitespace
771 771 $ cd s
772 772 $ cat > foobar << EOF
773 773 > woop woop
774 774 >
775 775 > foo
776 776 > bar
777 777 > EOF
778 778 $ echo foo > barfoo
779 779 $ git add barfoo
780 780 $ cd ..
781 781
782 782 $ hg diff --subrepos --ignore-all-space
783 783 diff --git a/s/barfoo b/s/barfoo
784 784 new file mode 100644
785 785 index 0000000..257cc56
786 786 --- /dev/null
787 787 +++ b/s/barfoo
788 788 @@ -0,0 +1 @@
789 789 +foo
790 790 $ hg diff --subrepos s/foobar
791 791 diff --git a/s/foobar b/s/foobar
792 792 index 8a5a5e2..bd5812a 100644
793 793 --- a/s/foobar
794 794 +++ b/s/foobar
795 795 @@ -1,4 +1,4 @@
796 796 -woopwoop
797 797 +woop woop
798 798
799 799 foo
800 800 bar
801 801
802 802 execute a diffstat
803 803 the output contains a regex, because git 1.7.10 and 1.7.11
804 804 change the amount of whitespace
805 805 $ hg diff --subrepos --stat
806 806 \s*barfoo |\s*1 + (re)
807 807 \s*foobar |\s*2 +- (re)
808 808 2 files changed, 2 insertions\(\+\), 1 deletions?\(-\) (re)
809 809
810 810 adding an include should ignore the other elements
811 811 $ hg diff --subrepos -I s/foobar
812 812 diff --git a/s/foobar b/s/foobar
813 813 index 8a5a5e2..bd5812a 100644
814 814 --- a/s/foobar
815 815 +++ b/s/foobar
816 816 @@ -1,4 +1,4 @@
817 817 -woopwoop
818 818 +woop woop
819 819
820 820 foo
821 821 bar
822 822
823 823 adding an exclude should ignore this element
824 824 $ hg diff --subrepos -X s/foobar
825 825 diff --git a/s/barfoo b/s/barfoo
826 826 new file mode 100644
827 827 index 0000000..257cc56
828 828 --- /dev/null
829 829 +++ b/s/barfoo
830 830 @@ -0,0 +1 @@
831 831 +foo
832 832
833 833 moving a file should show a removal and an add
834 834 $ hg revert --all
835 835 reverting subrepo ../gitroot
836 836 $ cd s
837 837 $ git mv foobar woop
838 838 $ cd ..
839 839 $ hg diff --subrepos
840 840 diff --git a/s/foobar b/s/foobar
841 841 deleted file mode 100644
842 842 index 8a5a5e2..0000000
843 843 --- a/s/foobar
844 844 +++ /dev/null
845 845 @@ -1,4 +0,0 @@
846 846 -woopwoop
847 847 -
848 848 -foo
849 849 -bar
850 850 diff --git a/s/woop b/s/woop
851 851 new file mode 100644
852 852 index 0000000..8a5a5e2
853 853 --- /dev/null
854 854 +++ b/s/woop
855 855 @@ -0,0 +1,4 @@
856 856 +woopwoop
857 857 +
858 858 +foo
859 859 +bar
860 860 $ rm s/woop
861 861
862 862 revert the subrepository
863 863 $ hg revert --all
864 864 reverting subrepo ../gitroot
865 865
866 866 $ hg status --subrepos
867 867 ? s/barfoo
868 868 ? s/foobar.orig
869 869
870 870 $ mv s/foobar.orig s/foobar
871 871
872 872 $ hg revert --no-backup s
873 873 reverting subrepo ../gitroot
874 874
875 875 $ hg status --subrepos
876 876 ? s/barfoo
877 877
878 revert moves orig files to the right place
879 $ echo 'bloop' > s/foobar
880 $ hg revert --all --verbose --config 'ui.origbackuppath=.hg/origbackups'
881 reverting subrepo ../gitroot
882 creating directory: $TESTTMP/tc/.hg/origbackups
883 saving current version of foobar as $TESTTMP/tc/.hg/origbackups/foobar.orig
884 $ ls .hg/origbackups
885 foobar.orig
886 $ rm -rf .hg/origbackups
887
878 888 show file at specific revision
879 889 $ cat > s/foobar << EOF
880 890 > woop woop
881 891 > fooo bar
882 892 > EOF
883 893 $ hg commit --subrepos -m "updated foobar"
884 894 committing subrepository s
885 895 $ cat > s/foobar << EOF
886 896 > current foobar
887 897 > (should not be visible using hg cat)
888 898 > EOF
889 899
890 900 $ hg cat -r . s/foobar
891 901 woop woop
892 902 fooo bar (no-eol)
893 903 $ hg cat -r "parents(.)" s/foobar > catparents
894 904
895 905 $ mkdir -p tmp/s
896 906
897 907 $ hg cat -r "parents(.)" --output tmp/%% s/foobar
898 908 $ diff tmp/% catparents
899 909
900 910 $ hg cat -r "parents(.)" --output tmp/%s s/foobar
901 911 $ diff tmp/foobar catparents
902 912
903 913 $ hg cat -r "parents(.)" --output tmp/%d/otherfoobar s/foobar
904 914 $ diff tmp/s/otherfoobar catparents
905 915
906 916 $ hg cat -r "parents(.)" --output tmp/%p s/foobar
907 917 $ diff tmp/s/foobar catparents
908 918
909 919 $ hg cat -r "parents(.)" --output tmp/%H s/foobar
910 920 $ diff tmp/255ee8cf690ec86e99b1e80147ea93ece117cd9d catparents
911 921
912 922 $ hg cat -r "parents(.)" --output tmp/%R s/foobar
913 923 $ diff tmp/10 catparents
914 924
915 925 $ hg cat -r "parents(.)" --output tmp/%h s/foobar
916 926 $ diff tmp/255ee8cf690e catparents
917 927
918 928 $ rm tmp/10
919 929 $ hg cat -r "parents(.)" --output tmp/%r s/foobar
920 930 $ diff tmp/10 catparents
921 931
922 932 $ mkdir tmp/tc
923 933 $ hg cat -r "parents(.)" --output tmp/%b/foobar s/foobar
924 934 $ diff tmp/tc/foobar catparents
925 935
926 936 cleanup
927 937 $ rm -r tmp
928 938 $ rm catparents
929 939
930 940 add git files, using either files or patterns
931 941 $ echo "hsss! hsssssssh!" > s/snake.python
932 942 $ echo "ccc" > s/c.c
933 943 $ echo "cpp" > s/cpp.cpp
934 944
935 945 $ hg add s/snake.python s/c.c s/cpp.cpp
936 946 $ hg st --subrepos s
937 947 M s/foobar
938 948 A s/c.c
939 949 A s/cpp.cpp
940 950 A s/snake.python
941 951 ? s/barfoo
942 952 $ hg revert s
943 953 reverting subrepo ../gitroot
944 954
945 955 $ hg add --subrepos "glob:**.python"
946 956 adding s/snake.python (glob)
947 957 $ hg st --subrepos s
948 958 A s/snake.python
949 959 ? s/barfoo
950 960 ? s/c.c
951 961 ? s/cpp.cpp
952 962 ? s/foobar.orig
953 963 $ hg revert s
954 964 reverting subrepo ../gitroot
955 965
956 966 $ hg add --subrepos s
957 967 adding s/barfoo (glob)
958 968 adding s/c.c (glob)
959 969 adding s/cpp.cpp (glob)
960 970 adding s/foobar.orig (glob)
961 971 adding s/snake.python (glob)
962 972 $ hg st --subrepos s
963 973 A s/barfoo
964 974 A s/c.c
965 975 A s/cpp.cpp
966 976 A s/foobar.orig
967 977 A s/snake.python
968 978 $ hg revert s
969 979 reverting subrepo ../gitroot
970 980 make sure everything is reverted correctly
971 981 $ hg st --subrepos s
972 982 ? s/barfoo
973 983 ? s/c.c
974 984 ? s/cpp.cpp
975 985 ? s/foobar.orig
976 986 ? s/snake.python
977 987
978 988 $ hg add --subrepos --exclude "path:s/c.c"
979 989 adding s/barfoo (glob)
980 990 adding s/cpp.cpp (glob)
981 991 adding s/foobar.orig (glob)
982 992 adding s/snake.python (glob)
983 993 $ hg st --subrepos s
984 994 A s/barfoo
985 995 A s/cpp.cpp
986 996 A s/foobar.orig
987 997 A s/snake.python
988 998 ? s/c.c
989 999 $ hg revert --all -q
990 1000
991 1001 .hgignore should not have influence in subrepos
992 1002 $ cat > .hgignore << EOF
993 1003 > syntax: glob
994 1004 > *.python
995 1005 > EOF
996 1006 $ hg add .hgignore
997 1007 $ hg add --subrepos "glob:**.python" s/barfoo
998 1008 adding s/snake.python (glob)
999 1009 $ hg st --subrepos s
1000 1010 A s/barfoo
1001 1011 A s/snake.python
1002 1012 ? s/c.c
1003 1013 ? s/cpp.cpp
1004 1014 ? s/foobar.orig
1005 1015 $ hg revert --all -q
1006 1016
1007 1017 .gitignore should have influence,
1008 1018 except for explicitly added files (no patterns)
1009 1019 $ cat > s/.gitignore << EOF
1010 1020 > *.python
1011 1021 > EOF
1012 1022 $ hg add s/.gitignore
1013 1023 $ hg st --subrepos s
1014 1024 A s/.gitignore
1015 1025 ? s/barfoo
1016 1026 ? s/c.c
1017 1027 ? s/cpp.cpp
1018 1028 ? s/foobar.orig
1019 1029 $ hg st --subrepos s --all
1020 1030 A s/.gitignore
1021 1031 ? s/barfoo
1022 1032 ? s/c.c
1023 1033 ? s/cpp.cpp
1024 1034 ? s/foobar.orig
1025 1035 I s/snake.python
1026 1036 C s/f
1027 1037 C s/foobar
1028 1038 C s/g
1029 1039 $ hg add --subrepos "glob:**.python"
1030 1040 $ hg st --subrepos s
1031 1041 A s/.gitignore
1032 1042 ? s/barfoo
1033 1043 ? s/c.c
1034 1044 ? s/cpp.cpp
1035 1045 ? s/foobar.orig
1036 1046 $ hg add --subrepos s/snake.python
1037 1047 $ hg st --subrepos s
1038 1048 A s/.gitignore
1039 1049 A s/snake.python
1040 1050 ? s/barfoo
1041 1051 ? s/c.c
1042 1052 ? s/cpp.cpp
1043 1053 ? s/foobar.orig
1044 1054
1045 1055 correctly do a dry run
1046 1056 $ hg add --subrepos s --dry-run
1047 1057 adding s/barfoo (glob)
1048 1058 adding s/c.c (glob)
1049 1059 adding s/cpp.cpp (glob)
1050 1060 adding s/foobar.orig (glob)
1051 1061 $ hg st --subrepos s
1052 1062 A s/.gitignore
1053 1063 A s/snake.python
1054 1064 ? s/barfoo
1055 1065 ? s/c.c
1056 1066 ? s/cpp.cpp
1057 1067 ? s/foobar.orig
1058 1068
1059 1069 error given when adding an already tracked file
1060 1070 $ hg add s/.gitignore
1061 1071 s/.gitignore already tracked!
1062 1072 [1]
1063 1073 $ hg add s/g
1064 1074 s/g already tracked!
1065 1075 [1]
1066 1076
1067 1077 removed files can be re-added
1068 1078 removing files using 'rm' or 'git rm' has the same effect,
1069 1079 since we ignore the staging area
1070 1080 $ hg ci --subrepos -m 'snake'
1071 1081 committing subrepository s
1072 1082 $ cd s
1073 1083 $ rm snake.python
1074 1084 (remove leftover .hg so Mercurial doesn't look for a root here)
1075 1085 $ rm -rf .hg
1076 1086 $ hg status --subrepos --all .
1077 1087 R snake.python
1078 1088 ? barfoo
1079 1089 ? c.c
1080 1090 ? cpp.cpp
1081 1091 ? foobar.orig
1082 1092 C .gitignore
1083 1093 C f
1084 1094 C foobar
1085 1095 C g
1086 1096 $ git rm snake.python
1087 1097 rm 'snake.python'
1088 1098 $ hg status --subrepos --all .
1089 1099 R snake.python
1090 1100 ? barfoo
1091 1101 ? c.c
1092 1102 ? cpp.cpp
1093 1103 ? foobar.orig
1094 1104 C .gitignore
1095 1105 C f
1096 1106 C foobar
1097 1107 C g
1098 1108 $ touch snake.python
1099 1109 $ cd ..
1100 1110 $ hg add s/snake.python
1101 1111 $ hg status -S
1102 1112 M s/snake.python
1103 1113 ? .hgignore
1104 1114 ? s/barfoo
1105 1115 ? s/c.c
1106 1116 ? s/cpp.cpp
1107 1117 ? s/foobar.orig
1108 1118 $ hg revert --all -q
1109 1119
1110 1120 make sure we show changed files, rather than changed subtrees
1111 1121 $ mkdir s/foo
1112 1122 $ touch s/foo/bwuh
1113 1123 $ hg add s/foo/bwuh
1114 1124 $ hg commit -S -m "add bwuh"
1115 1125 committing subrepository s
1116 1126 $ hg status -S --change .
1117 1127 M .hgsubstate
1118 1128 A s/foo/bwuh
1119 1129 ? s/barfoo
1120 1130 ? s/c.c
1121 1131 ? s/cpp.cpp
1122 1132 ? s/foobar.orig
1123 1133 ? s/snake.python.orig
1124 1134
1125 1135 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now