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