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