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