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