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