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