##// END OF EJS Templates
py3: use bytes instead of pycompat.bytestr...
Pulkit Goyal -
r35631:991f0be9 default
parent child Browse files
Show More
@@ -1,2128 +1,2128 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 return pycompat.bytestr(source)
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 return pycompat.bytestr(parent)
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 fp = self._repo.vfs("hgrc", "wb", text=True)
845 845 try:
846 846 fp.write(''.join(lines))
847 847 finally:
848 848 fp.close()
849 849
850 850 @annotatesubrepoerror
851 851 def add(self, ui, match, prefix, explicitonly, **opts):
852 852 return cmdutil.add(ui, self._repo, match,
853 853 self.wvfs.reljoin(prefix, self._path),
854 854 explicitonly, **opts)
855 855
856 856 @annotatesubrepoerror
857 857 def addremove(self, m, prefix, opts, dry_run, similarity):
858 858 # In the same way as sub directories are processed, once in a subrepo,
859 859 # always entry any of its subrepos. Don't corrupt the options that will
860 860 # be used to process sibling subrepos however.
861 861 opts = copy.copy(opts)
862 862 opts['subrepos'] = True
863 863 return scmutil.addremove(self._repo, m,
864 864 self.wvfs.reljoin(prefix, self._path), opts,
865 865 dry_run, similarity)
866 866
867 867 @annotatesubrepoerror
868 868 def cat(self, match, fm, fntemplate, prefix, **opts):
869 869 rev = self._state[1]
870 870 ctx = self._repo[rev]
871 871 return cmdutil.cat(self.ui, self._repo, ctx, match, fm, fntemplate,
872 872 prefix, **opts)
873 873
874 874 @annotatesubrepoerror
875 875 def status(self, rev2, **opts):
876 876 try:
877 877 rev1 = self._state[1]
878 878 ctx1 = self._repo[rev1]
879 879 ctx2 = self._repo[rev2]
880 880 return self._repo.status(ctx1, ctx2, **opts)
881 881 except error.RepoLookupError as inst:
882 882 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
883 883 % (inst, subrelpath(self)))
884 884 return scmutil.status([], [], [], [], [], [], [])
885 885
886 886 @annotatesubrepoerror
887 887 def diff(self, ui, diffopts, node2, match, prefix, **opts):
888 888 try:
889 889 node1 = node.bin(self._state[1])
890 890 # We currently expect node2 to come from substate and be
891 891 # in hex format
892 892 if node2 is not None:
893 893 node2 = node.bin(node2)
894 894 cmdutil.diffordiffstat(ui, self._repo, diffopts,
895 895 node1, node2, match,
896 896 prefix=posixpath.join(prefix, self._path),
897 897 listsubrepos=True, **opts)
898 898 except error.RepoLookupError as inst:
899 899 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
900 900 % (inst, subrelpath(self)))
901 901
902 902 @annotatesubrepoerror
903 903 def archive(self, archiver, prefix, match=None, decode=True):
904 904 self._get(self._state + ('hg',))
905 905 total = abstractsubrepo.archive(self, archiver, prefix, match)
906 906 rev = self._state[1]
907 907 ctx = self._repo[rev]
908 908 for subpath in ctx.substate:
909 909 s = subrepo(ctx, subpath, True)
910 910 submatch = matchmod.subdirmatcher(subpath, match)
911 911 total += s.archive(archiver, prefix + self._path + '/', submatch,
912 912 decode)
913 913 return total
914 914
915 915 @annotatesubrepoerror
916 916 def dirty(self, ignoreupdate=False, missing=False):
917 917 r = self._state[1]
918 918 if r == '' and not ignoreupdate: # no state recorded
919 919 return True
920 920 w = self._repo[None]
921 921 if r != w.p1().hex() and not ignoreupdate:
922 922 # different version checked out
923 923 return True
924 924 return w.dirty(missing=missing) # working directory changed
925 925
926 926 def basestate(self):
927 927 return self._repo['.'].hex()
928 928
929 929 def checknested(self, path):
930 930 return self._repo._checknested(self._repo.wjoin(path))
931 931
932 932 @annotatesubrepoerror
933 933 def commit(self, text, user, date):
934 934 # don't bother committing in the subrepo if it's only been
935 935 # updated
936 936 if not self.dirty(True):
937 937 return self._repo['.'].hex()
938 938 self.ui.debug("committing subrepo %s\n" % subrelpath(self))
939 939 n = self._repo.commit(text, user, date)
940 940 if not n:
941 941 return self._repo['.'].hex() # different version checked out
942 942 return node.hex(n)
943 943
944 944 @annotatesubrepoerror
945 945 def phase(self, state):
946 946 return self._repo[state].phase()
947 947
948 948 @annotatesubrepoerror
949 949 def remove(self):
950 950 # we can't fully delete the repository as it may contain
951 951 # local-only history
952 952 self.ui.note(_('removing subrepo %s\n') % subrelpath(self))
953 953 hg.clean(self._repo, node.nullid, False)
954 954
955 955 def _get(self, state):
956 956 source, revision, kind = state
957 957 parentrepo = self._repo._subparent
958 958
959 959 if revision in self._repo.unfiltered():
960 960 # Allow shared subrepos tracked at null to setup the sharedpath
961 961 if len(self._repo) != 0 or not parentrepo.shared():
962 962 return True
963 963 self._repo._subsource = source
964 964 srcurl = _abssource(self._repo)
965 965 other = hg.peer(self._repo, {}, srcurl)
966 966 if len(self._repo) == 0:
967 967 # use self._repo.vfs instead of self.wvfs to remove .hg only
968 968 self._repo.vfs.rmtree()
969 969 if parentrepo.shared():
970 970 self.ui.status(_('sharing subrepo %s from %s\n')
971 971 % (subrelpath(self), srcurl))
972 972 shared = hg.share(self._repo._subparent.baseui,
973 973 other, self._repo.root,
974 974 update=False, bookmarks=False)
975 975 self._repo = shared.local()
976 976 else:
977 977 self.ui.status(_('cloning subrepo %s from %s\n')
978 978 % (subrelpath(self), srcurl))
979 979 other, cloned = hg.clone(self._repo._subparent.baseui, {},
980 980 other, self._repo.root,
981 981 update=False)
982 982 self._repo = cloned.local()
983 983 self._initrepo(parentrepo, source, create=True)
984 984 self._cachestorehash(srcurl)
985 985 else:
986 986 self.ui.status(_('pulling subrepo %s from %s\n')
987 987 % (subrelpath(self), srcurl))
988 988 cleansub = self.storeclean(srcurl)
989 989 exchange.pull(self._repo, other)
990 990 if cleansub:
991 991 # keep the repo clean after pull
992 992 self._cachestorehash(srcurl)
993 993 return False
994 994
995 995 @annotatesubrepoerror
996 996 def get(self, state, overwrite=False):
997 997 inrepo = self._get(state)
998 998 source, revision, kind = state
999 999 repo = self._repo
1000 1000 repo.ui.debug("getting subrepo %s\n" % self._path)
1001 1001 if inrepo:
1002 1002 urepo = repo.unfiltered()
1003 1003 ctx = urepo[revision]
1004 1004 if ctx.hidden():
1005 1005 urepo.ui.warn(
1006 1006 _('revision %s in subrepository "%s" is hidden\n') \
1007 1007 % (revision[0:12], self._path))
1008 1008 repo = urepo
1009 1009 hg.updaterepo(repo, revision, overwrite)
1010 1010
1011 1011 @annotatesubrepoerror
1012 1012 def merge(self, state):
1013 1013 self._get(state)
1014 1014 cur = self._repo['.']
1015 1015 dst = self._repo[state[1]]
1016 1016 anc = dst.ancestor(cur)
1017 1017
1018 1018 def mergefunc():
1019 1019 if anc == cur and dst.branch() == cur.branch():
1020 1020 self.ui.debug('updating subrepository "%s"\n'
1021 1021 % subrelpath(self))
1022 1022 hg.update(self._repo, state[1])
1023 1023 elif anc == dst:
1024 1024 self.ui.debug('skipping subrepository "%s"\n'
1025 1025 % subrelpath(self))
1026 1026 else:
1027 1027 self.ui.debug('merging subrepository "%s"\n' % subrelpath(self))
1028 1028 hg.merge(self._repo, state[1], remind=False)
1029 1029
1030 1030 wctx = self._repo[None]
1031 1031 if self.dirty():
1032 1032 if anc != dst:
1033 1033 if _updateprompt(self.ui, self, wctx.dirty(), cur, dst):
1034 1034 mergefunc()
1035 1035 else:
1036 1036 mergefunc()
1037 1037 else:
1038 1038 mergefunc()
1039 1039
1040 1040 @annotatesubrepoerror
1041 1041 def push(self, opts):
1042 1042 force = opts.get('force')
1043 1043 newbranch = opts.get('new_branch')
1044 1044 ssh = opts.get('ssh')
1045 1045
1046 1046 # push subrepos depth-first for coherent ordering
1047 1047 c = self._repo['']
1048 1048 subs = c.substate # only repos that are committed
1049 1049 for s in sorted(subs):
1050 1050 if c.sub(s).push(opts) == 0:
1051 1051 return False
1052 1052
1053 1053 dsturl = _abssource(self._repo, True)
1054 1054 if not force:
1055 1055 if self.storeclean(dsturl):
1056 1056 self.ui.status(
1057 1057 _('no changes made to subrepo %s since last push to %s\n')
1058 1058 % (subrelpath(self), dsturl))
1059 1059 return None
1060 1060 self.ui.status(_('pushing subrepo %s to %s\n') %
1061 1061 (subrelpath(self), dsturl))
1062 1062 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
1063 1063 res = exchange.push(self._repo, other, force, newbranch=newbranch)
1064 1064
1065 1065 # the repo is now clean
1066 1066 self._cachestorehash(dsturl)
1067 1067 return res.cgresult
1068 1068
1069 1069 @annotatesubrepoerror
1070 1070 def outgoing(self, ui, dest, opts):
1071 1071 if 'rev' in opts or 'branch' in opts:
1072 1072 opts = copy.copy(opts)
1073 1073 opts.pop('rev', None)
1074 1074 opts.pop('branch', None)
1075 1075 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
1076 1076
1077 1077 @annotatesubrepoerror
1078 1078 def incoming(self, ui, source, opts):
1079 1079 if 'rev' in opts or 'branch' in opts:
1080 1080 opts = copy.copy(opts)
1081 1081 opts.pop('rev', None)
1082 1082 opts.pop('branch', None)
1083 1083 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
1084 1084
1085 1085 @annotatesubrepoerror
1086 1086 def files(self):
1087 1087 rev = self._state[1]
1088 1088 ctx = self._repo[rev]
1089 1089 return ctx.manifest().keys()
1090 1090
1091 1091 def filedata(self, name, decode):
1092 1092 rev = self._state[1]
1093 1093 data = self._repo[rev][name].data()
1094 1094 if decode:
1095 1095 data = self._repo.wwritedata(name, data)
1096 1096 return data
1097 1097
1098 1098 def fileflags(self, name):
1099 1099 rev = self._state[1]
1100 1100 ctx = self._repo[rev]
1101 1101 return ctx.flags(name)
1102 1102
1103 1103 @annotatesubrepoerror
1104 1104 def printfiles(self, ui, m, fm, fmt, subrepos):
1105 1105 # If the parent context is a workingctx, use the workingctx here for
1106 1106 # consistency.
1107 1107 if self._ctx.rev() is None:
1108 1108 ctx = self._repo[None]
1109 1109 else:
1110 1110 rev = self._state[1]
1111 1111 ctx = self._repo[rev]
1112 1112 return cmdutil.files(ui, ctx, m, fm, fmt, subrepos)
1113 1113
1114 1114 @annotatesubrepoerror
1115 1115 def getfileset(self, expr):
1116 1116 if self._ctx.rev() is None:
1117 1117 ctx = self._repo[None]
1118 1118 else:
1119 1119 rev = self._state[1]
1120 1120 ctx = self._repo[rev]
1121 1121
1122 1122 files = ctx.getfileset(expr)
1123 1123
1124 1124 for subpath in ctx.substate:
1125 1125 sub = ctx.sub(subpath)
1126 1126
1127 1127 try:
1128 1128 files.extend(subpath + '/' + f for f in sub.getfileset(expr))
1129 1129 except error.LookupError:
1130 1130 self.ui.status(_("skipping missing subrepository: %s\n")
1131 1131 % self.wvfs.reljoin(reporelpath(self), subpath))
1132 1132 return files
1133 1133
1134 1134 def walk(self, match):
1135 1135 ctx = self._repo[None]
1136 1136 return ctx.walk(match)
1137 1137
1138 1138 @annotatesubrepoerror
1139 1139 def forget(self, match, prefix):
1140 1140 return cmdutil.forget(self.ui, self._repo, match,
1141 1141 self.wvfs.reljoin(prefix, self._path), True)
1142 1142
1143 1143 @annotatesubrepoerror
1144 1144 def removefiles(self, matcher, prefix, after, force, subrepos, warnings):
1145 1145 return cmdutil.remove(self.ui, self._repo, matcher,
1146 1146 self.wvfs.reljoin(prefix, self._path),
1147 1147 after, force, subrepos)
1148 1148
1149 1149 @annotatesubrepoerror
1150 1150 def revert(self, substate, *pats, **opts):
1151 1151 # reverting a subrepo is a 2 step process:
1152 1152 # 1. if the no_backup is not set, revert all modified
1153 1153 # files inside the subrepo
1154 1154 # 2. update the subrepo to the revision specified in
1155 1155 # the corresponding substate dictionary
1156 1156 self.ui.status(_('reverting subrepo %s\n') % substate[0])
1157 1157 if not opts.get(r'no_backup'):
1158 1158 # Revert all files on the subrepo, creating backups
1159 1159 # Note that this will not recursively revert subrepos
1160 1160 # We could do it if there was a set:subrepos() predicate
1161 1161 opts = opts.copy()
1162 1162 opts[r'date'] = None
1163 1163 opts[r'rev'] = substate[1]
1164 1164
1165 1165 self.filerevert(*pats, **opts)
1166 1166
1167 1167 # Update the repo to the revision specified in the given substate
1168 1168 if not opts.get(r'dry_run'):
1169 1169 self.get(substate, overwrite=True)
1170 1170
1171 1171 def filerevert(self, *pats, **opts):
1172 1172 ctx = self._repo[opts[r'rev']]
1173 1173 parents = self._repo.dirstate.parents()
1174 1174 if opts.get(r'all'):
1175 1175 pats = ['set:modified()']
1176 1176 else:
1177 1177 pats = []
1178 1178 cmdutil.revert(self.ui, self._repo, ctx, parents, *pats, **opts)
1179 1179
1180 1180 def shortid(self, revid):
1181 1181 return revid[:12]
1182 1182
1183 1183 @annotatesubrepoerror
1184 1184 def unshare(self):
1185 1185 # subrepo inherently violates our import layering rules
1186 1186 # because it wants to make repo objects from deep inside the stack
1187 1187 # so we manually delay the circular imports to not break
1188 1188 # scripts that don't use our demand-loading
1189 1189 global hg
1190 1190 from . import hg as h
1191 1191 hg = h
1192 1192
1193 1193 # Nothing prevents a user from sharing in a repo, and then making that a
1194 1194 # subrepo. Alternately, the previous unshare attempt may have failed
1195 1195 # part way through. So recurse whether or not this layer is shared.
1196 1196 if self._repo.shared():
1197 1197 self.ui.status(_("unsharing subrepo '%s'\n") % self._relpath)
1198 1198
1199 1199 hg.unshare(self.ui, self._repo)
1200 1200
1201 1201 def verify(self):
1202 1202 try:
1203 1203 rev = self._state[1]
1204 1204 ctx = self._repo.unfiltered()[rev]
1205 1205 if ctx.hidden():
1206 1206 # Since hidden revisions aren't pushed/pulled, it seems worth an
1207 1207 # explicit warning.
1208 1208 ui = self._repo.ui
1209 1209 ui.warn(_("subrepo '%s' is hidden in revision %s\n") %
1210 1210 (self._relpath, node.short(self._ctx.node())))
1211 1211 return 0
1212 1212 except error.RepoLookupError:
1213 1213 # A missing subrepo revision may be a case of needing to pull it, so
1214 1214 # don't treat this as an error.
1215 1215 self._repo.ui.warn(_("subrepo '%s' not found in revision %s\n") %
1216 1216 (self._relpath, node.short(self._ctx.node())))
1217 1217 return 0
1218 1218
1219 1219 @propertycache
1220 1220 def wvfs(self):
1221 1221 """return own wvfs for efficiency and consistency
1222 1222 """
1223 1223 return self._repo.wvfs
1224 1224
1225 1225 @propertycache
1226 1226 def _relpath(self):
1227 1227 """return path to this subrepository as seen from outermost repository
1228 1228 """
1229 1229 # Keep consistent dir separators by avoiding vfs.join(self._path)
1230 1230 return reporelpath(self._repo)
1231 1231
1232 1232 class svnsubrepo(abstractsubrepo):
1233 1233 def __init__(self, ctx, path, state, allowcreate):
1234 1234 super(svnsubrepo, self).__init__(ctx, path)
1235 1235 self._state = state
1236 1236 self._exe = util.findexe('svn')
1237 1237 if not self._exe:
1238 1238 raise error.Abort(_("'svn' executable not found for subrepo '%s'")
1239 1239 % self._path)
1240 1240
1241 1241 def _svncommand(self, commands, filename='', failok=False):
1242 1242 cmd = [self._exe]
1243 1243 extrakw = {}
1244 1244 if not self.ui.interactive():
1245 1245 # Making stdin be a pipe should prevent svn from behaving
1246 1246 # interactively even if we can't pass --non-interactive.
1247 1247 extrakw[r'stdin'] = subprocess.PIPE
1248 1248 # Starting in svn 1.5 --non-interactive is a global flag
1249 1249 # instead of being per-command, but we need to support 1.4 so
1250 1250 # we have to be intelligent about what commands take
1251 1251 # --non-interactive.
1252 1252 if commands[0] in ('update', 'checkout', 'commit'):
1253 1253 cmd.append('--non-interactive')
1254 1254 cmd.extend(commands)
1255 1255 if filename is not None:
1256 1256 path = self.wvfs.reljoin(self._ctx.repo().origroot,
1257 1257 self._path, filename)
1258 1258 cmd.append(path)
1259 1259 env = dict(encoding.environ)
1260 1260 # Avoid localized output, preserve current locale for everything else.
1261 1261 lc_all = env.get('LC_ALL')
1262 1262 if lc_all:
1263 1263 env['LANG'] = lc_all
1264 1264 del env['LC_ALL']
1265 1265 env['LC_MESSAGES'] = 'C'
1266 1266 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
1267 1267 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
1268 1268 universal_newlines=True, env=env, **extrakw)
1269 1269 stdout, stderr = p.communicate()
1270 1270 stderr = stderr.strip()
1271 1271 if not failok:
1272 1272 if p.returncode:
1273 1273 raise error.Abort(stderr or 'exited with code %d'
1274 1274 % p.returncode)
1275 1275 if stderr:
1276 1276 self.ui.warn(stderr + '\n')
1277 1277 return stdout, stderr
1278 1278
1279 1279 @propertycache
1280 1280 def _svnversion(self):
1281 1281 output, err = self._svncommand(['--version', '--quiet'], filename=None)
1282 1282 m = re.search(br'^(\d+)\.(\d+)', output)
1283 1283 if not m:
1284 1284 raise error.Abort(_('cannot retrieve svn tool version'))
1285 1285 return (int(m.group(1)), int(m.group(2)))
1286 1286
1287 1287 def _wcrevs(self):
1288 1288 # Get the working directory revision as well as the last
1289 1289 # commit revision so we can compare the subrepo state with
1290 1290 # both. We used to store the working directory one.
1291 1291 output, err = self._svncommand(['info', '--xml'])
1292 1292 doc = xml.dom.minidom.parseString(output)
1293 1293 entries = doc.getElementsByTagName('entry')
1294 1294 lastrev, rev = '0', '0'
1295 1295 if entries:
1296 1296 rev = str(entries[0].getAttribute('revision')) or '0'
1297 1297 commits = entries[0].getElementsByTagName('commit')
1298 1298 if commits:
1299 1299 lastrev = str(commits[0].getAttribute('revision')) or '0'
1300 1300 return (lastrev, rev)
1301 1301
1302 1302 def _wcrev(self):
1303 1303 return self._wcrevs()[0]
1304 1304
1305 1305 def _wcchanged(self):
1306 1306 """Return (changes, extchanges, missing) where changes is True
1307 1307 if the working directory was changed, extchanges is
1308 1308 True if any of these changes concern an external entry and missing
1309 1309 is True if any change is a missing entry.
1310 1310 """
1311 1311 output, err = self._svncommand(['status', '--xml'])
1312 1312 externals, changes, missing = [], [], []
1313 1313 doc = xml.dom.minidom.parseString(output)
1314 1314 for e in doc.getElementsByTagName('entry'):
1315 1315 s = e.getElementsByTagName('wc-status')
1316 1316 if not s:
1317 1317 continue
1318 1318 item = s[0].getAttribute('item')
1319 1319 props = s[0].getAttribute('props')
1320 1320 path = e.getAttribute('path')
1321 1321 if item == 'external':
1322 1322 externals.append(path)
1323 1323 elif item == 'missing':
1324 1324 missing.append(path)
1325 1325 if (item not in ('', 'normal', 'unversioned', 'external')
1326 1326 or props not in ('', 'none', 'normal')):
1327 1327 changes.append(path)
1328 1328 for path in changes:
1329 1329 for ext in externals:
1330 1330 if path == ext or path.startswith(ext + pycompat.ossep):
1331 1331 return True, True, bool(missing)
1332 1332 return bool(changes), False, bool(missing)
1333 1333
1334 1334 def dirty(self, ignoreupdate=False, missing=False):
1335 1335 wcchanged = self._wcchanged()
1336 1336 changed = wcchanged[0] or (missing and wcchanged[2])
1337 1337 if not changed:
1338 1338 if self._state[1] in self._wcrevs() or ignoreupdate:
1339 1339 return False
1340 1340 return True
1341 1341
1342 1342 def basestate(self):
1343 1343 lastrev, rev = self._wcrevs()
1344 1344 if lastrev != rev:
1345 1345 # Last committed rev is not the same than rev. We would
1346 1346 # like to take lastrev but we do not know if the subrepo
1347 1347 # URL exists at lastrev. Test it and fallback to rev it
1348 1348 # is not there.
1349 1349 try:
1350 1350 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
1351 1351 return lastrev
1352 1352 except error.Abort:
1353 1353 pass
1354 1354 return rev
1355 1355
1356 1356 @annotatesubrepoerror
1357 1357 def commit(self, text, user, date):
1358 1358 # user and date are out of our hands since svn is centralized
1359 1359 changed, extchanged, missing = self._wcchanged()
1360 1360 if not changed:
1361 1361 return self.basestate()
1362 1362 if extchanged:
1363 1363 # Do not try to commit externals
1364 1364 raise error.Abort(_('cannot commit svn externals'))
1365 1365 if missing:
1366 1366 # svn can commit with missing entries but aborting like hg
1367 1367 # seems a better approach.
1368 1368 raise error.Abort(_('cannot commit missing svn entries'))
1369 1369 commitinfo, err = self._svncommand(['commit', '-m', text])
1370 1370 self.ui.status(commitinfo)
1371 1371 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
1372 1372 if not newrev:
1373 1373 if not commitinfo.strip():
1374 1374 # Sometimes, our definition of "changed" differs from
1375 1375 # svn one. For instance, svn ignores missing files
1376 1376 # when committing. If there are only missing files, no
1377 1377 # commit is made, no output and no error code.
1378 1378 raise error.Abort(_('failed to commit svn changes'))
1379 1379 raise error.Abort(commitinfo.splitlines()[-1])
1380 1380 newrev = newrev.groups()[0]
1381 1381 self.ui.status(self._svncommand(['update', '-r', newrev])[0])
1382 1382 return newrev
1383 1383
1384 1384 @annotatesubrepoerror
1385 1385 def remove(self):
1386 1386 if self.dirty():
1387 1387 self.ui.warn(_('not removing repo %s because '
1388 1388 'it has changes.\n') % self._path)
1389 1389 return
1390 1390 self.ui.note(_('removing subrepo %s\n') % self._path)
1391 1391
1392 1392 self.wvfs.rmtree(forcibly=True)
1393 1393 try:
1394 1394 pwvfs = self._ctx.repo().wvfs
1395 1395 pwvfs.removedirs(pwvfs.dirname(self._path))
1396 1396 except OSError:
1397 1397 pass
1398 1398
1399 1399 @annotatesubrepoerror
1400 1400 def get(self, state, overwrite=False):
1401 1401 if overwrite:
1402 1402 self._svncommand(['revert', '--recursive'])
1403 1403 args = ['checkout']
1404 1404 if self._svnversion >= (1, 5):
1405 1405 args.append('--force')
1406 1406 # The revision must be specified at the end of the URL to properly
1407 1407 # update to a directory which has since been deleted and recreated.
1408 1408 args.append('%s@%s' % (state[0], state[1]))
1409 1409
1410 1410 # SEC: check that the ssh url is safe
1411 1411 util.checksafessh(state[0])
1412 1412
1413 1413 status, err = self._svncommand(args, failok=True)
1414 1414 _sanitize(self.ui, self.wvfs, '.svn')
1415 1415 if not re.search('Checked out revision [0-9]+.', status):
1416 1416 if ('is already a working copy for a different URL' in err
1417 1417 and (self._wcchanged()[:2] == (False, False))):
1418 1418 # obstructed but clean working copy, so just blow it away.
1419 1419 self.remove()
1420 1420 self.get(state, overwrite=False)
1421 1421 return
1422 1422 raise error.Abort((status or err).splitlines()[-1])
1423 1423 self.ui.status(status)
1424 1424
1425 1425 @annotatesubrepoerror
1426 1426 def merge(self, state):
1427 1427 old = self._state[1]
1428 1428 new = state[1]
1429 1429 wcrev = self._wcrev()
1430 1430 if new != wcrev:
1431 1431 dirty = old == wcrev or self._wcchanged()[0]
1432 1432 if _updateprompt(self.ui, self, dirty, wcrev, new):
1433 1433 self.get(state, False)
1434 1434
1435 1435 def push(self, opts):
1436 1436 # push is a no-op for SVN
1437 1437 return True
1438 1438
1439 1439 @annotatesubrepoerror
1440 1440 def files(self):
1441 1441 output = self._svncommand(['list', '--recursive', '--xml'])[0]
1442 1442 doc = xml.dom.minidom.parseString(output)
1443 1443 paths = []
1444 1444 for e in doc.getElementsByTagName('entry'):
1445 1445 kind = str(e.getAttribute('kind'))
1446 1446 if kind != 'file':
1447 1447 continue
1448 1448 name = ''.join(c.data for c
1449 1449 in e.getElementsByTagName('name')[0].childNodes
1450 1450 if c.nodeType == c.TEXT_NODE)
1451 1451 paths.append(name.encode('utf-8'))
1452 1452 return paths
1453 1453
1454 1454 def filedata(self, name, decode):
1455 1455 return self._svncommand(['cat'], name)[0]
1456 1456
1457 1457
1458 1458 class gitsubrepo(abstractsubrepo):
1459 1459 def __init__(self, ctx, path, state, allowcreate):
1460 1460 super(gitsubrepo, self).__init__(ctx, path)
1461 1461 self._state = state
1462 1462 self._abspath = ctx.repo().wjoin(path)
1463 1463 self._subparent = ctx.repo()
1464 1464 self._ensuregit()
1465 1465
1466 1466 def _ensuregit(self):
1467 1467 try:
1468 1468 self._gitexecutable = 'git'
1469 1469 out, err = self._gitnodir(['--version'])
1470 1470 except OSError as e:
1471 1471 genericerror = _("error executing git for subrepo '%s': %s")
1472 1472 notfoundhint = _("check git is installed and in your PATH")
1473 1473 if e.errno != errno.ENOENT:
1474 1474 raise error.Abort(genericerror % (
1475 1475 self._path, encoding.strtolocal(e.strerror)))
1476 1476 elif pycompat.iswindows:
1477 1477 try:
1478 1478 self._gitexecutable = 'git.cmd'
1479 1479 out, err = self._gitnodir(['--version'])
1480 1480 except OSError as e2:
1481 1481 if e2.errno == errno.ENOENT:
1482 1482 raise error.Abort(_("couldn't find 'git' or 'git.cmd'"
1483 1483 " for subrepo '%s'") % self._path,
1484 1484 hint=notfoundhint)
1485 1485 else:
1486 1486 raise error.Abort(genericerror % (self._path,
1487 1487 encoding.strtolocal(e2.strerror)))
1488 1488 else:
1489 1489 raise error.Abort(_("couldn't find git for subrepo '%s'")
1490 1490 % self._path, hint=notfoundhint)
1491 1491 versionstatus = self._checkversion(out)
1492 1492 if versionstatus == 'unknown':
1493 1493 self.ui.warn(_('cannot retrieve git version\n'))
1494 1494 elif versionstatus == 'abort':
1495 1495 raise error.Abort(_('git subrepo requires at least 1.6.0 or later'))
1496 1496 elif versionstatus == 'warning':
1497 1497 self.ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
1498 1498
1499 1499 @staticmethod
1500 1500 def _gitversion(out):
1501 1501 m = re.search(br'^git version (\d+)\.(\d+)\.(\d+)', out)
1502 1502 if m:
1503 1503 return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
1504 1504
1505 1505 m = re.search(br'^git version (\d+)\.(\d+)', out)
1506 1506 if m:
1507 1507 return (int(m.group(1)), int(m.group(2)), 0)
1508 1508
1509 1509 return -1
1510 1510
1511 1511 @staticmethod
1512 1512 def _checkversion(out):
1513 1513 '''ensure git version is new enough
1514 1514
1515 1515 >>> _checkversion = gitsubrepo._checkversion
1516 1516 >>> _checkversion(b'git version 1.6.0')
1517 1517 'ok'
1518 1518 >>> _checkversion(b'git version 1.8.5')
1519 1519 'ok'
1520 1520 >>> _checkversion(b'git version 1.4.0')
1521 1521 'abort'
1522 1522 >>> _checkversion(b'git version 1.5.0')
1523 1523 'warning'
1524 1524 >>> _checkversion(b'git version 1.9-rc0')
1525 1525 'ok'
1526 1526 >>> _checkversion(b'git version 1.9.0.265.g81cdec2')
1527 1527 'ok'
1528 1528 >>> _checkversion(b'git version 1.9.0.GIT')
1529 1529 'ok'
1530 1530 >>> _checkversion(b'git version 12345')
1531 1531 'unknown'
1532 1532 >>> _checkversion(b'no')
1533 1533 'unknown'
1534 1534 '''
1535 1535 version = gitsubrepo._gitversion(out)
1536 1536 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1537 1537 # despite the docstring comment. For now, error on 1.4.0, warn on
1538 1538 # 1.5.0 but attempt to continue.
1539 1539 if version == -1:
1540 1540 return 'unknown'
1541 1541 if version < (1, 5, 0):
1542 1542 return 'abort'
1543 1543 elif version < (1, 6, 0):
1544 1544 return 'warning'
1545 1545 return 'ok'
1546 1546
1547 1547 def _gitcommand(self, commands, env=None, stream=False):
1548 1548 return self._gitdir(commands, env=env, stream=stream)[0]
1549 1549
1550 1550 def _gitdir(self, commands, env=None, stream=False):
1551 1551 return self._gitnodir(commands, env=env, stream=stream,
1552 1552 cwd=self._abspath)
1553 1553
1554 1554 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1555 1555 """Calls the git command
1556 1556
1557 1557 The methods tries to call the git command. versions prior to 1.6.0
1558 1558 are not supported and very probably fail.
1559 1559 """
1560 1560 self.ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1561 1561 if env is None:
1562 1562 env = encoding.environ.copy()
1563 1563 # disable localization for Git output (issue5176)
1564 1564 env['LC_ALL'] = 'C'
1565 1565 # fix for Git CVE-2015-7545
1566 1566 if 'GIT_ALLOW_PROTOCOL' not in env:
1567 1567 env['GIT_ALLOW_PROTOCOL'] = 'file:git:http:https:ssh'
1568 1568 # unless ui.quiet is set, print git's stderr,
1569 1569 # which is mostly progress and useful info
1570 1570 errpipe = None
1571 1571 if self.ui.quiet:
1572 1572 errpipe = open(os.devnull, 'w')
1573 1573 if self.ui._colormode and len(commands) and commands[0] == "diff":
1574 1574 # insert the argument in the front,
1575 1575 # the end of git diff arguments is used for paths
1576 1576 commands.insert(1, '--color')
1577 1577 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
1578 1578 cwd=cwd, env=env, close_fds=util.closefds,
1579 1579 stdout=subprocess.PIPE, stderr=errpipe)
1580 1580 if stream:
1581 1581 return p.stdout, None
1582 1582
1583 1583 retdata = p.stdout.read().strip()
1584 1584 # wait for the child to exit to avoid race condition.
1585 1585 p.wait()
1586 1586
1587 1587 if p.returncode != 0 and p.returncode != 1:
1588 1588 # there are certain error codes that are ok
1589 1589 command = commands[0]
1590 1590 if command in ('cat-file', 'symbolic-ref'):
1591 1591 return retdata, p.returncode
1592 1592 # for all others, abort
1593 1593 raise error.Abort(_('git %s error %d in %s') %
1594 1594 (command, p.returncode, self._relpath))
1595 1595
1596 1596 return retdata, p.returncode
1597 1597
1598 1598 def _gitmissing(self):
1599 1599 return not self.wvfs.exists('.git')
1600 1600
1601 1601 def _gitstate(self):
1602 1602 return self._gitcommand(['rev-parse', 'HEAD'])
1603 1603
1604 1604 def _gitcurrentbranch(self):
1605 1605 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1606 1606 if err:
1607 1607 current = None
1608 1608 return current
1609 1609
1610 1610 def _gitremote(self, remote):
1611 1611 out = self._gitcommand(['remote', 'show', '-n', remote])
1612 1612 line = out.split('\n')[1]
1613 1613 i = line.index('URL: ') + len('URL: ')
1614 1614 return line[i:]
1615 1615
1616 1616 def _githavelocally(self, revision):
1617 1617 out, code = self._gitdir(['cat-file', '-e', revision])
1618 1618 return code == 0
1619 1619
1620 1620 def _gitisancestor(self, r1, r2):
1621 1621 base = self._gitcommand(['merge-base', r1, r2])
1622 1622 return base == r1
1623 1623
1624 1624 def _gitisbare(self):
1625 1625 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1626 1626
1627 1627 def _gitupdatestat(self):
1628 1628 """This must be run before git diff-index.
1629 1629 diff-index only looks at changes to file stat;
1630 1630 this command looks at file contents and updates the stat."""
1631 1631 self._gitcommand(['update-index', '-q', '--refresh'])
1632 1632
1633 1633 def _gitbranchmap(self):
1634 1634 '''returns 2 things:
1635 1635 a map from git branch to revision
1636 1636 a map from revision to branches'''
1637 1637 branch2rev = {}
1638 1638 rev2branch = {}
1639 1639
1640 1640 out = self._gitcommand(['for-each-ref', '--format',
1641 1641 '%(objectname) %(refname)'])
1642 1642 for line in out.split('\n'):
1643 1643 revision, ref = line.split(' ')
1644 1644 if (not ref.startswith('refs/heads/') and
1645 1645 not ref.startswith('refs/remotes/')):
1646 1646 continue
1647 1647 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1648 1648 continue # ignore remote/HEAD redirects
1649 1649 branch2rev[ref] = revision
1650 1650 rev2branch.setdefault(revision, []).append(ref)
1651 1651 return branch2rev, rev2branch
1652 1652
1653 1653 def _gittracking(self, branches):
1654 1654 'return map of remote branch to local tracking branch'
1655 1655 # assumes no more than one local tracking branch for each remote
1656 1656 tracking = {}
1657 1657 for b in branches:
1658 1658 if b.startswith('refs/remotes/'):
1659 1659 continue
1660 1660 bname = b.split('/', 2)[2]
1661 1661 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1662 1662 if remote:
1663 1663 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1664 1664 tracking['refs/remotes/%s/%s' %
1665 1665 (remote, ref.split('/', 2)[2])] = b
1666 1666 return tracking
1667 1667
1668 1668 def _abssource(self, source):
1669 1669 if '://' not in source:
1670 1670 # recognize the scp syntax as an absolute source
1671 1671 colon = source.find(':')
1672 1672 if colon != -1 and '/' not in source[:colon]:
1673 1673 return source
1674 1674 self._subsource = source
1675 1675 return _abssource(self)
1676 1676
1677 1677 def _fetch(self, source, revision):
1678 1678 if self._gitmissing():
1679 1679 # SEC: check for safe ssh url
1680 1680 util.checksafessh(source)
1681 1681
1682 1682 source = self._abssource(source)
1683 1683 self.ui.status(_('cloning subrepo %s from %s\n') %
1684 1684 (self._relpath, source))
1685 1685 self._gitnodir(['clone', source, self._abspath])
1686 1686 if self._githavelocally(revision):
1687 1687 return
1688 1688 self.ui.status(_('pulling subrepo %s from %s\n') %
1689 1689 (self._relpath, self._gitremote('origin')))
1690 1690 # try only origin: the originally cloned repo
1691 1691 self._gitcommand(['fetch'])
1692 1692 if not self._githavelocally(revision):
1693 1693 raise error.Abort(_('revision %s does not exist in subrepository '
1694 1694 '"%s"\n') % (revision, self._relpath))
1695 1695
1696 1696 @annotatesubrepoerror
1697 1697 def dirty(self, ignoreupdate=False, missing=False):
1698 1698 if self._gitmissing():
1699 1699 return self._state[1] != ''
1700 1700 if self._gitisbare():
1701 1701 return True
1702 1702 if not ignoreupdate and self._state[1] != self._gitstate():
1703 1703 # different version checked out
1704 1704 return True
1705 1705 # check for staged changes or modified files; ignore untracked files
1706 1706 self._gitupdatestat()
1707 1707 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1708 1708 return code == 1
1709 1709
1710 1710 def basestate(self):
1711 1711 return self._gitstate()
1712 1712
1713 1713 @annotatesubrepoerror
1714 1714 def get(self, state, overwrite=False):
1715 1715 source, revision, kind = state
1716 1716 if not revision:
1717 1717 self.remove()
1718 1718 return
1719 1719 self._fetch(source, revision)
1720 1720 # if the repo was set to be bare, unbare it
1721 1721 if self._gitisbare():
1722 1722 self._gitcommand(['config', 'core.bare', 'false'])
1723 1723 if self._gitstate() == revision:
1724 1724 self._gitcommand(['reset', '--hard', 'HEAD'])
1725 1725 return
1726 1726 elif self._gitstate() == revision:
1727 1727 if overwrite:
1728 1728 # first reset the index to unmark new files for commit, because
1729 1729 # reset --hard will otherwise throw away files added for commit,
1730 1730 # not just unmark them.
1731 1731 self._gitcommand(['reset', 'HEAD'])
1732 1732 self._gitcommand(['reset', '--hard', 'HEAD'])
1733 1733 return
1734 1734 branch2rev, rev2branch = self._gitbranchmap()
1735 1735
1736 1736 def checkout(args):
1737 1737 cmd = ['checkout']
1738 1738 if overwrite:
1739 1739 # first reset the index to unmark new files for commit, because
1740 1740 # the -f option will otherwise throw away files added for
1741 1741 # commit, not just unmark them.
1742 1742 self._gitcommand(['reset', 'HEAD'])
1743 1743 cmd.append('-f')
1744 1744 self._gitcommand(cmd + args)
1745 1745 _sanitize(self.ui, self.wvfs, '.git')
1746 1746
1747 1747 def rawcheckout():
1748 1748 # no branch to checkout, check it out with no branch
1749 1749 self.ui.warn(_('checking out detached HEAD in '
1750 1750 'subrepository "%s"\n') % self._relpath)
1751 1751 self.ui.warn(_('check out a git branch if you intend '
1752 1752 'to make changes\n'))
1753 1753 checkout(['-q', revision])
1754 1754
1755 1755 if revision not in rev2branch:
1756 1756 rawcheckout()
1757 1757 return
1758 1758 branches = rev2branch[revision]
1759 1759 firstlocalbranch = None
1760 1760 for b in branches:
1761 1761 if b == 'refs/heads/master':
1762 1762 # master trumps all other branches
1763 1763 checkout(['refs/heads/master'])
1764 1764 return
1765 1765 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1766 1766 firstlocalbranch = b
1767 1767 if firstlocalbranch:
1768 1768 checkout([firstlocalbranch])
1769 1769 return
1770 1770
1771 1771 tracking = self._gittracking(branch2rev.keys())
1772 1772 # choose a remote branch already tracked if possible
1773 1773 remote = branches[0]
1774 1774 if remote not in tracking:
1775 1775 for b in branches:
1776 1776 if b in tracking:
1777 1777 remote = b
1778 1778 break
1779 1779
1780 1780 if remote not in tracking:
1781 1781 # create a new local tracking branch
1782 1782 local = remote.split('/', 3)[3]
1783 1783 checkout(['-b', local, remote])
1784 1784 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1785 1785 # When updating to a tracked remote branch,
1786 1786 # if the local tracking branch is downstream of it,
1787 1787 # a normal `git pull` would have performed a "fast-forward merge"
1788 1788 # which is equivalent to updating the local branch to the remote.
1789 1789 # Since we are only looking at branching at update, we need to
1790 1790 # detect this situation and perform this action lazily.
1791 1791 if tracking[remote] != self._gitcurrentbranch():
1792 1792 checkout([tracking[remote]])
1793 1793 self._gitcommand(['merge', '--ff', remote])
1794 1794 _sanitize(self.ui, self.wvfs, '.git')
1795 1795 else:
1796 1796 # a real merge would be required, just checkout the revision
1797 1797 rawcheckout()
1798 1798
1799 1799 @annotatesubrepoerror
1800 1800 def commit(self, text, user, date):
1801 1801 if self._gitmissing():
1802 1802 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1803 1803 cmd = ['commit', '-a', '-m', text]
1804 1804 env = encoding.environ.copy()
1805 1805 if user:
1806 1806 cmd += ['--author', user]
1807 1807 if date:
1808 1808 # git's date parser silently ignores when seconds < 1e9
1809 1809 # convert to ISO8601
1810 1810 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1811 1811 '%Y-%m-%dT%H:%M:%S %1%2')
1812 1812 self._gitcommand(cmd, env=env)
1813 1813 # make sure commit works otherwise HEAD might not exist under certain
1814 1814 # circumstances
1815 1815 return self._gitstate()
1816 1816
1817 1817 @annotatesubrepoerror
1818 1818 def merge(self, state):
1819 1819 source, revision, kind = state
1820 1820 self._fetch(source, revision)
1821 1821 base = self._gitcommand(['merge-base', revision, self._state[1]])
1822 1822 self._gitupdatestat()
1823 1823 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1824 1824
1825 1825 def mergefunc():
1826 1826 if base == revision:
1827 1827 self.get(state) # fast forward merge
1828 1828 elif base != self._state[1]:
1829 1829 self._gitcommand(['merge', '--no-commit', revision])
1830 1830 _sanitize(self.ui, self.wvfs, '.git')
1831 1831
1832 1832 if self.dirty():
1833 1833 if self._gitstate() != revision:
1834 1834 dirty = self._gitstate() == self._state[1] or code != 0
1835 1835 if _updateprompt(self.ui, self, dirty,
1836 1836 self._state[1][:7], revision[:7]):
1837 1837 mergefunc()
1838 1838 else:
1839 1839 mergefunc()
1840 1840
1841 1841 @annotatesubrepoerror
1842 1842 def push(self, opts):
1843 1843 force = opts.get('force')
1844 1844
1845 1845 if not self._state[1]:
1846 1846 return True
1847 1847 if self._gitmissing():
1848 1848 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1849 1849 # if a branch in origin contains the revision, nothing to do
1850 1850 branch2rev, rev2branch = self._gitbranchmap()
1851 1851 if self._state[1] in rev2branch:
1852 1852 for b in rev2branch[self._state[1]]:
1853 1853 if b.startswith('refs/remotes/origin/'):
1854 1854 return True
1855 1855 for b, revision in branch2rev.iteritems():
1856 1856 if b.startswith('refs/remotes/origin/'):
1857 1857 if self._gitisancestor(self._state[1], revision):
1858 1858 return True
1859 1859 # otherwise, try to push the currently checked out branch
1860 1860 cmd = ['push']
1861 1861 if force:
1862 1862 cmd.append('--force')
1863 1863
1864 1864 current = self._gitcurrentbranch()
1865 1865 if current:
1866 1866 # determine if the current branch is even useful
1867 1867 if not self._gitisancestor(self._state[1], current):
1868 1868 self.ui.warn(_('unrelated git branch checked out '
1869 1869 'in subrepository "%s"\n') % self._relpath)
1870 1870 return False
1871 1871 self.ui.status(_('pushing branch %s of subrepository "%s"\n') %
1872 1872 (current.split('/', 2)[2], self._relpath))
1873 1873 ret = self._gitdir(cmd + ['origin', current])
1874 1874 return ret[1] == 0
1875 1875 else:
1876 1876 self.ui.warn(_('no branch checked out in subrepository "%s"\n'
1877 1877 'cannot push revision %s\n') %
1878 1878 (self._relpath, self._state[1]))
1879 1879 return False
1880 1880
1881 1881 @annotatesubrepoerror
1882 1882 def add(self, ui, match, prefix, explicitonly, **opts):
1883 1883 if self._gitmissing():
1884 1884 return []
1885 1885
1886 1886 (modified, added, removed,
1887 1887 deleted, unknown, ignored, clean) = self.status(None, unknown=True,
1888 1888 clean=True)
1889 1889
1890 1890 tracked = set()
1891 1891 # dirstates 'amn' warn, 'r' is added again
1892 1892 for l in (modified, added, deleted, clean):
1893 1893 tracked.update(l)
1894 1894
1895 1895 # Unknown files not of interest will be rejected by the matcher
1896 1896 files = unknown
1897 1897 files.extend(match.files())
1898 1898
1899 1899 rejected = []
1900 1900
1901 1901 files = [f for f in sorted(set(files)) if match(f)]
1902 1902 for f in files:
1903 1903 exact = match.exact(f)
1904 1904 command = ["add"]
1905 1905 if exact:
1906 1906 command.append("-f") #should be added, even if ignored
1907 1907 if ui.verbose or not exact:
1908 1908 ui.status(_('adding %s\n') % match.rel(f))
1909 1909
1910 1910 if f in tracked: # hg prints 'adding' even if already tracked
1911 1911 if exact:
1912 1912 rejected.append(f)
1913 1913 continue
1914 1914 if not opts.get(r'dry_run'):
1915 1915 self._gitcommand(command + [f])
1916 1916
1917 1917 for f in rejected:
1918 1918 ui.warn(_("%s already tracked!\n") % match.abs(f))
1919 1919
1920 1920 return rejected
1921 1921
1922 1922 @annotatesubrepoerror
1923 1923 def remove(self):
1924 1924 if self._gitmissing():
1925 1925 return
1926 1926 if self.dirty():
1927 1927 self.ui.warn(_('not removing repo %s because '
1928 1928 'it has changes.\n') % self._relpath)
1929 1929 return
1930 1930 # we can't fully delete the repository as it may contain
1931 1931 # local-only history
1932 1932 self.ui.note(_('removing subrepo %s\n') % self._relpath)
1933 1933 self._gitcommand(['config', 'core.bare', 'true'])
1934 1934 for f, kind in self.wvfs.readdir():
1935 1935 if f == '.git':
1936 1936 continue
1937 1937 if kind == stat.S_IFDIR:
1938 1938 self.wvfs.rmtree(f)
1939 1939 else:
1940 1940 self.wvfs.unlink(f)
1941 1941
1942 1942 def archive(self, archiver, prefix, match=None, decode=True):
1943 1943 total = 0
1944 1944 source, revision = self._state
1945 1945 if not revision:
1946 1946 return total
1947 1947 self._fetch(source, revision)
1948 1948
1949 1949 # Parse git's native archive command.
1950 1950 # This should be much faster than manually traversing the trees
1951 1951 # and objects with many subprocess calls.
1952 1952 tarstream = self._gitcommand(['archive', revision], stream=True)
1953 1953 tar = tarfile.open(fileobj=tarstream, mode='r|')
1954 1954 relpath = subrelpath(self)
1955 1955 self.ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1956 1956 for i, info in enumerate(tar):
1957 1957 if info.isdir():
1958 1958 continue
1959 1959 if match and not match(info.name):
1960 1960 continue
1961 1961 if info.issym():
1962 1962 data = info.linkname
1963 1963 else:
1964 1964 data = tar.extractfile(info).read()
1965 1965 archiver.addfile(prefix + self._path + '/' + info.name,
1966 1966 info.mode, info.issym(), data)
1967 1967 total += 1
1968 1968 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
1969 1969 unit=_('files'))
1970 1970 self.ui.progress(_('archiving (%s)') % relpath, None)
1971 1971 return total
1972 1972
1973 1973
1974 1974 @annotatesubrepoerror
1975 1975 def cat(self, match, fm, fntemplate, prefix, **opts):
1976 1976 rev = self._state[1]
1977 1977 if match.anypats():
1978 1978 return 1 #No support for include/exclude yet
1979 1979
1980 1980 if not match.files():
1981 1981 return 1
1982 1982
1983 1983 # TODO: add support for non-plain formatter (see cmdutil.cat())
1984 1984 for f in match.files():
1985 1985 output = self._gitcommand(["show", "%s:%s" % (rev, f)])
1986 1986 fp = cmdutil.makefileobj(self._subparent, fntemplate,
1987 1987 self._ctx.node(),
1988 1988 pathname=self.wvfs.reljoin(prefix, f))
1989 1989 fp.write(output)
1990 1990 fp.close()
1991 1991 return 0
1992 1992
1993 1993
1994 1994 @annotatesubrepoerror
1995 1995 def status(self, rev2, **opts):
1996 1996 rev1 = self._state[1]
1997 1997 if self._gitmissing() or not rev1:
1998 1998 # if the repo is missing, return no results
1999 1999 return scmutil.status([], [], [], [], [], [], [])
2000 2000 modified, added, removed = [], [], []
2001 2001 self._gitupdatestat()
2002 2002 if rev2:
2003 2003 command = ['diff-tree', '--no-renames', '-r', rev1, rev2]
2004 2004 else:
2005 2005 command = ['diff-index', '--no-renames', rev1]
2006 2006 out = self._gitcommand(command)
2007 2007 for line in out.split('\n'):
2008 2008 tab = line.find('\t')
2009 2009 if tab == -1:
2010 2010 continue
2011 2011 status, f = line[tab - 1], line[tab + 1:]
2012 2012 if status == 'M':
2013 2013 modified.append(f)
2014 2014 elif status == 'A':
2015 2015 added.append(f)
2016 2016 elif status == 'D':
2017 2017 removed.append(f)
2018 2018
2019 2019 deleted, unknown, ignored, clean = [], [], [], []
2020 2020
2021 2021 command = ['status', '--porcelain', '-z']
2022 2022 if opts.get(r'unknown'):
2023 2023 command += ['--untracked-files=all']
2024 2024 if opts.get(r'ignored'):
2025 2025 command += ['--ignored']
2026 2026 out = self._gitcommand(command)
2027 2027
2028 2028 changedfiles = set()
2029 2029 changedfiles.update(modified)
2030 2030 changedfiles.update(added)
2031 2031 changedfiles.update(removed)
2032 2032 for line in out.split('\0'):
2033 2033 if not line:
2034 2034 continue
2035 2035 st = line[0:2]
2036 2036 #moves and copies show 2 files on one line
2037 2037 if line.find('\0') >= 0:
2038 2038 filename1, filename2 = line[3:].split('\0')
2039 2039 else:
2040 2040 filename1 = line[3:]
2041 2041 filename2 = None
2042 2042
2043 2043 changedfiles.add(filename1)
2044 2044 if filename2:
2045 2045 changedfiles.add(filename2)
2046 2046
2047 2047 if st == '??':
2048 2048 unknown.append(filename1)
2049 2049 elif st == '!!':
2050 2050 ignored.append(filename1)
2051 2051
2052 2052 if opts.get(r'clean'):
2053 2053 out = self._gitcommand(['ls-files'])
2054 2054 for f in out.split('\n'):
2055 2055 if not f in changedfiles:
2056 2056 clean.append(f)
2057 2057
2058 2058 return scmutil.status(modified, added, removed, deleted,
2059 2059 unknown, ignored, clean)
2060 2060
2061 2061 @annotatesubrepoerror
2062 2062 def diff(self, ui, diffopts, node2, match, prefix, **opts):
2063 2063 node1 = self._state[1]
2064 2064 cmd = ['diff', '--no-renames']
2065 2065 if opts[r'stat']:
2066 2066 cmd.append('--stat')
2067 2067 else:
2068 2068 # for Git, this also implies '-p'
2069 2069 cmd.append('-U%d' % diffopts.context)
2070 2070
2071 2071 gitprefix = self.wvfs.reljoin(prefix, self._path)
2072 2072
2073 2073 if diffopts.noprefix:
2074 2074 cmd.extend(['--src-prefix=%s/' % gitprefix,
2075 2075 '--dst-prefix=%s/' % gitprefix])
2076 2076 else:
2077 2077 cmd.extend(['--src-prefix=a/%s/' % gitprefix,
2078 2078 '--dst-prefix=b/%s/' % gitprefix])
2079 2079
2080 2080 if diffopts.ignorews:
2081 2081 cmd.append('--ignore-all-space')
2082 2082 if diffopts.ignorewsamount:
2083 2083 cmd.append('--ignore-space-change')
2084 2084 if self._gitversion(self._gitcommand(['--version'])) >= (1, 8, 4) \
2085 2085 and diffopts.ignoreblanklines:
2086 2086 cmd.append('--ignore-blank-lines')
2087 2087
2088 2088 cmd.append(node1)
2089 2089 if node2:
2090 2090 cmd.append(node2)
2091 2091
2092 2092 output = ""
2093 2093 if match.always():
2094 2094 output += self._gitcommand(cmd) + '\n'
2095 2095 else:
2096 2096 st = self.status(node2)[:3]
2097 2097 files = [f for sublist in st for f in sublist]
2098 2098 for f in files:
2099 2099 if match(f):
2100 2100 output += self._gitcommand(cmd + ['--', f]) + '\n'
2101 2101
2102 2102 if output.strip():
2103 2103 ui.write(output)
2104 2104
2105 2105 @annotatesubrepoerror
2106 2106 def revert(self, substate, *pats, **opts):
2107 2107 self.ui.status(_('reverting subrepo %s\n') % substate[0])
2108 2108 if not opts.get(r'no_backup'):
2109 2109 status = self.status(None)
2110 2110 names = status.modified
2111 2111 for name in names:
2112 2112 bakname = scmutil.origpath(self.ui, self._subparent, name)
2113 2113 self.ui.note(_('saving current version of %s as %s\n') %
2114 2114 (name, bakname))
2115 2115 self.wvfs.rename(name, bakname)
2116 2116
2117 2117 if not opts.get(r'dry_run'):
2118 2118 self.get(substate, overwrite=True)
2119 2119 return []
2120 2120
2121 2121 def shortid(self, revid):
2122 2122 return revid[:7]
2123 2123
2124 2124 types = {
2125 2125 'hg': hgsubrepo,
2126 2126 'svn': svnsubrepo,
2127 2127 'git': gitsubrepo,
2128 2128 }
General Comments 0
You need to be logged in to leave comments. Login now