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