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