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