##// END OF EJS Templates
subrepo: setting LC_MESSAGES only works if LC_ALL is empty or unset...
Thomas Arendsen Hein -
r17705:6929b9c7 stable
parent child Browse files
Show More
@@ -1,1273 +1,1277
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 errno, os, re, xml.dom.minidom, shutil, posixpath
9 9 import stat, subprocess, tarfile
10 10 from i18n import _
11 11 import config, scmutil, util, node, error, cmdutil, bookmarks, match as matchmod
12 12 hg = None
13 13 propertycache = util.propertycache
14 14
15 15 nullstate = ('', '', 'empty')
16 16
17 17 def state(ctx, ui):
18 18 """return a state dict, mapping subrepo paths configured in .hgsub
19 19 to tuple: (source from .hgsub, revision from .hgsubstate, kind
20 20 (key in types dict))
21 21 """
22 22 p = config.config()
23 23 def read(f, sections=None, remap=None):
24 24 if f in ctx:
25 25 try:
26 26 data = ctx[f].data()
27 27 except IOError, err:
28 28 if err.errno != errno.ENOENT:
29 29 raise
30 30 # handle missing subrepo spec files as removed
31 31 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
32 32 return
33 33 p.parse(f, data, sections, remap, read)
34 34 else:
35 35 raise util.Abort(_("subrepo spec file %s not found") % f)
36 36
37 37 if '.hgsub' in ctx:
38 38 read('.hgsub')
39 39
40 40 for path, src in ui.configitems('subpaths'):
41 41 p.set('subpaths', path, src, ui.configsource('subpaths', path))
42 42
43 43 rev = {}
44 44 if '.hgsubstate' in ctx:
45 45 try:
46 46 for i, l in enumerate(ctx['.hgsubstate'].data().splitlines()):
47 47 l = l.lstrip()
48 48 if not l:
49 49 continue
50 50 try:
51 51 revision, path = l.split(" ", 1)
52 52 except ValueError:
53 53 raise util.Abort(_("invalid subrepository revision "
54 54 "specifier in .hgsubstate line %d")
55 55 % (i + 1))
56 56 rev[path] = revision
57 57 except IOError, err:
58 58 if err.errno != errno.ENOENT:
59 59 raise
60 60
61 61 def remap(src):
62 62 for pattern, repl in p.items('subpaths'):
63 63 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
64 64 # does a string decode.
65 65 repl = repl.encode('string-escape')
66 66 # However, we still want to allow back references to go
67 67 # through unharmed, so we turn r'\\1' into r'\1'. Again,
68 68 # extra escapes are needed because re.sub string decodes.
69 69 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
70 70 try:
71 71 src = re.sub(pattern, repl, src, 1)
72 72 except re.error, e:
73 73 raise util.Abort(_("bad subrepository pattern in %s: %s")
74 74 % (p.source('subpaths', pattern), e))
75 75 return src
76 76
77 77 state = {}
78 78 for path, src in p[''].items():
79 79 kind = 'hg'
80 80 if src.startswith('['):
81 81 if ']' not in src:
82 82 raise util.Abort(_('missing ] in subrepo source'))
83 83 kind, src = src.split(']', 1)
84 84 kind = kind[1:]
85 85 src = src.lstrip() # strip any extra whitespace after ']'
86 86
87 87 if not util.url(src).isabs():
88 88 parent = _abssource(ctx._repo, abort=False)
89 89 if parent:
90 90 parent = util.url(parent)
91 91 parent.path = posixpath.join(parent.path or '', src)
92 92 parent.path = posixpath.normpath(parent.path)
93 93 joined = str(parent)
94 94 # Remap the full joined path and use it if it changes,
95 95 # else remap the original source.
96 96 remapped = remap(joined)
97 97 if remapped == joined:
98 98 src = remap(src)
99 99 else:
100 100 src = remapped
101 101
102 102 src = remap(src)
103 103 state[util.pconvert(path)] = (src.strip(), rev.get(path, ''), kind)
104 104
105 105 return state
106 106
107 107 def writestate(repo, state):
108 108 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
109 109 lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state)]
110 110 repo.wwrite('.hgsubstate', ''.join(lines), '')
111 111
112 112 def submerge(repo, wctx, mctx, actx, overwrite):
113 113 """delegated from merge.applyupdates: merging of .hgsubstate file
114 114 in working context, merging context and ancestor context"""
115 115 if mctx == actx: # backwards?
116 116 actx = wctx.p1()
117 117 s1 = wctx.substate
118 118 s2 = mctx.substate
119 119 sa = actx.substate
120 120 sm = {}
121 121
122 122 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
123 123
124 124 def debug(s, msg, r=""):
125 125 if r:
126 126 r = "%s:%s:%s" % r
127 127 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
128 128
129 129 for s, l in s1.items():
130 130 a = sa.get(s, nullstate)
131 131 ld = l # local state with possible dirty flag for compares
132 132 if wctx.sub(s).dirty():
133 133 ld = (l[0], l[1] + "+")
134 134 if wctx == actx: # overwrite
135 135 a = ld
136 136
137 137 if s in s2:
138 138 r = s2[s]
139 139 if ld == r or r == a: # no change or local is newer
140 140 sm[s] = l
141 141 continue
142 142 elif ld == a: # other side changed
143 143 debug(s, "other changed, get", r)
144 144 wctx.sub(s).get(r, overwrite)
145 145 sm[s] = r
146 146 elif ld[0] != r[0]: # sources differ
147 147 if repo.ui.promptchoice(
148 148 _(' subrepository sources for %s differ\n'
149 149 'use (l)ocal source (%s) or (r)emote source (%s)?')
150 150 % (s, l[0], r[0]),
151 151 (_('&Local'), _('&Remote')), 0):
152 152 debug(s, "prompt changed, get", r)
153 153 wctx.sub(s).get(r, overwrite)
154 154 sm[s] = r
155 155 elif ld[1] == a[1]: # local side is unchanged
156 156 debug(s, "other side changed, get", r)
157 157 wctx.sub(s).get(r, overwrite)
158 158 sm[s] = r
159 159 else:
160 160 debug(s, "both sides changed, merge with", r)
161 161 wctx.sub(s).merge(r)
162 162 sm[s] = l
163 163 elif ld == a: # remote removed, local unchanged
164 164 debug(s, "remote removed, remove")
165 165 wctx.sub(s).remove()
166 166 elif a == nullstate: # not present in remote or ancestor
167 167 debug(s, "local added, keep")
168 168 sm[s] = l
169 169 continue
170 170 else:
171 171 if repo.ui.promptchoice(
172 172 _(' local changed subrepository %s which remote removed\n'
173 173 'use (c)hanged version or (d)elete?') % s,
174 174 (_('&Changed'), _('&Delete')), 0):
175 175 debug(s, "prompt remove")
176 176 wctx.sub(s).remove()
177 177
178 178 for s, r in sorted(s2.items()):
179 179 if s in s1:
180 180 continue
181 181 elif s not in sa:
182 182 debug(s, "remote added, get", r)
183 183 mctx.sub(s).get(r)
184 184 sm[s] = r
185 185 elif r != sa[s]:
186 186 if repo.ui.promptchoice(
187 187 _(' remote changed subrepository %s which local removed\n'
188 188 'use (c)hanged version or (d)elete?') % s,
189 189 (_('&Changed'), _('&Delete')), 0) == 0:
190 190 debug(s, "prompt recreate", r)
191 191 wctx.sub(s).get(r)
192 192 sm[s] = r
193 193
194 194 # record merged .hgsubstate
195 195 writestate(repo, sm)
196 196
197 197 def _updateprompt(ui, sub, dirty, local, remote):
198 198 if dirty:
199 199 msg = (_(' subrepository sources for %s differ\n'
200 200 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
201 201 % (subrelpath(sub), local, remote))
202 202 else:
203 203 msg = (_(' subrepository sources for %s differ (in checked out '
204 204 'version)\n'
205 205 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
206 206 % (subrelpath(sub), local, remote))
207 207 return ui.promptchoice(msg, (_('&Local'), _('&Remote')), 0)
208 208
209 209 def reporelpath(repo):
210 210 """return path to this (sub)repo as seen from outermost repo"""
211 211 parent = repo
212 212 while util.safehasattr(parent, '_subparent'):
213 213 parent = parent._subparent
214 214 p = parent.root.rstrip(os.sep)
215 215 return repo.root[len(p) + 1:]
216 216
217 217 def subrelpath(sub):
218 218 """return path to this subrepo as seen from outermost repo"""
219 219 if util.safehasattr(sub, '_relpath'):
220 220 return sub._relpath
221 221 if not util.safehasattr(sub, '_repo'):
222 222 return sub._path
223 223 return reporelpath(sub._repo)
224 224
225 225 def _abssource(repo, push=False, abort=True):
226 226 """return pull/push path of repo - either based on parent repo .hgsub info
227 227 or on the top repo config. Abort or return None if no source found."""
228 228 if util.safehasattr(repo, '_subparent'):
229 229 source = util.url(repo._subsource)
230 230 if source.isabs():
231 231 return str(source)
232 232 source.path = posixpath.normpath(source.path)
233 233 parent = _abssource(repo._subparent, push, abort=False)
234 234 if parent:
235 235 parent = util.url(util.pconvert(parent))
236 236 parent.path = posixpath.join(parent.path or '', source.path)
237 237 parent.path = posixpath.normpath(parent.path)
238 238 return str(parent)
239 239 else: # recursion reached top repo
240 240 if util.safehasattr(repo, '_subtoppath'):
241 241 return repo._subtoppath
242 242 if push and repo.ui.config('paths', 'default-push'):
243 243 return repo.ui.config('paths', 'default-push')
244 244 if repo.ui.config('paths', 'default'):
245 245 return repo.ui.config('paths', 'default')
246 246 if abort:
247 247 raise util.Abort(_("default path for subrepository %s not found") %
248 248 reporelpath(repo))
249 249
250 250 def itersubrepos(ctx1, ctx2):
251 251 """find subrepos in ctx1 or ctx2"""
252 252 # Create a (subpath, ctx) mapping where we prefer subpaths from
253 253 # ctx1. The subpaths from ctx2 are important when the .hgsub file
254 254 # has been modified (in ctx2) but not yet committed (in ctx1).
255 255 subpaths = dict.fromkeys(ctx2.substate, ctx2)
256 256 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
257 257 for subpath, ctx in sorted(subpaths.iteritems()):
258 258 yield subpath, ctx.sub(subpath)
259 259
260 260 def subrepo(ctx, path):
261 261 """return instance of the right subrepo class for subrepo in path"""
262 262 # subrepo inherently violates our import layering rules
263 263 # because it wants to make repo objects from deep inside the stack
264 264 # so we manually delay the circular imports to not break
265 265 # scripts that don't use our demand-loading
266 266 global hg
267 267 import hg as h
268 268 hg = h
269 269
270 270 scmutil.pathauditor(ctx._repo.root)(path)
271 271 state = ctx.substate[path]
272 272 if state[2] not in types:
273 273 raise util.Abort(_('unknown subrepo type %s') % state[2])
274 274 return types[state[2]](ctx, path, state[:2])
275 275
276 276 # subrepo classes need to implement the following abstract class:
277 277
278 278 class abstractsubrepo(object):
279 279
280 280 def dirty(self, ignoreupdate=False):
281 281 """returns true if the dirstate of the subrepo is dirty or does not
282 282 match current stored state. If ignoreupdate is true, only check
283 283 whether the subrepo has uncommitted changes in its dirstate.
284 284 """
285 285 raise NotImplementedError
286 286
287 287 def basestate(self):
288 288 """current working directory base state, disregarding .hgsubstate
289 289 state and working directory modifications"""
290 290 raise NotImplementedError
291 291
292 292 def checknested(self, path):
293 293 """check if path is a subrepository within this repository"""
294 294 return False
295 295
296 296 def commit(self, text, user, date):
297 297 """commit the current changes to the subrepo with the given
298 298 log message. Use given user and date if possible. Return the
299 299 new state of the subrepo.
300 300 """
301 301 raise NotImplementedError
302 302
303 303 def remove(self):
304 304 """remove the subrepo
305 305
306 306 (should verify the dirstate is not dirty first)
307 307 """
308 308 raise NotImplementedError
309 309
310 310 def get(self, state, overwrite=False):
311 311 """run whatever commands are needed to put the subrepo into
312 312 this state
313 313 """
314 314 raise NotImplementedError
315 315
316 316 def merge(self, state):
317 317 """merge currently-saved state with the new state."""
318 318 raise NotImplementedError
319 319
320 320 def push(self, opts):
321 321 """perform whatever action is analogous to 'hg push'
322 322
323 323 This may be a no-op on some systems.
324 324 """
325 325 raise NotImplementedError
326 326
327 327 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
328 328 return []
329 329
330 330 def status(self, rev2, **opts):
331 331 return [], [], [], [], [], [], []
332 332
333 333 def diff(self, diffopts, node2, match, prefix, **opts):
334 334 pass
335 335
336 336 def outgoing(self, ui, dest, opts):
337 337 return 1
338 338
339 339 def incoming(self, ui, source, opts):
340 340 return 1
341 341
342 342 def files(self):
343 343 """return filename iterator"""
344 344 raise NotImplementedError
345 345
346 346 def filedata(self, name):
347 347 """return file data"""
348 348 raise NotImplementedError
349 349
350 350 def fileflags(self, name):
351 351 """return file flags"""
352 352 return ''
353 353
354 354 def archive(self, ui, archiver, prefix, match=None):
355 355 if match is not None:
356 356 files = [f for f in self.files() if match(f)]
357 357 else:
358 358 files = self.files()
359 359 total = len(files)
360 360 relpath = subrelpath(self)
361 361 ui.progress(_('archiving (%s)') % relpath, 0,
362 362 unit=_('files'), total=total)
363 363 for i, name in enumerate(files):
364 364 flags = self.fileflags(name)
365 365 mode = 'x' in flags and 0755 or 0644
366 366 symlink = 'l' in flags
367 367 archiver.addfile(os.path.join(prefix, self._path, name),
368 368 mode, symlink, self.filedata(name))
369 369 ui.progress(_('archiving (%s)') % relpath, i + 1,
370 370 unit=_('files'), total=total)
371 371 ui.progress(_('archiving (%s)') % relpath, None)
372 372
373 373 def walk(self, match):
374 374 '''
375 375 walk recursively through the directory tree, finding all files
376 376 matched by the match function
377 377 '''
378 378 pass
379 379
380 380 def forget(self, ui, match, prefix):
381 381 return ([], [])
382 382
383 383 def revert(self, ui, substate, *pats, **opts):
384 384 ui.warn('%s: reverting %s subrepos is unsupported\n' \
385 385 % (substate[0], substate[2]))
386 386 return []
387 387
388 388 class hgsubrepo(abstractsubrepo):
389 389 def __init__(self, ctx, path, state):
390 390 self._path = path
391 391 self._state = state
392 392 r = ctx._repo
393 393 root = r.wjoin(path)
394 394 create = False
395 395 if not os.path.exists(os.path.join(root, '.hg')):
396 396 create = True
397 397 util.makedirs(root)
398 398 self._repo = hg.repository(r.ui, root, create=create)
399 399 self._initrepo(r, state[0], create)
400 400
401 401 def _initrepo(self, parentrepo, source, create):
402 402 self._repo._subparent = parentrepo
403 403 self._repo._subsource = source
404 404
405 405 if create:
406 406 fp = self._repo.opener("hgrc", "w", text=True)
407 407 fp.write('[paths]\n')
408 408
409 409 def addpathconfig(key, value):
410 410 if value:
411 411 fp.write('%s = %s\n' % (key, value))
412 412 self._repo.ui.setconfig('paths', key, value)
413 413
414 414 defpath = _abssource(self._repo, abort=False)
415 415 defpushpath = _abssource(self._repo, True, abort=False)
416 416 addpathconfig('default', defpath)
417 417 if defpath != defpushpath:
418 418 addpathconfig('default-push', defpushpath)
419 419 fp.close()
420 420
421 421 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
422 422 return cmdutil.add(ui, self._repo, match, dryrun, listsubrepos,
423 423 os.path.join(prefix, self._path), explicitonly)
424 424
425 425 def status(self, rev2, **opts):
426 426 try:
427 427 rev1 = self._state[1]
428 428 ctx1 = self._repo[rev1]
429 429 ctx2 = self._repo[rev2]
430 430 return self._repo.status(ctx1, ctx2, **opts)
431 431 except error.RepoLookupError, inst:
432 432 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
433 433 % (inst, subrelpath(self)))
434 434 return [], [], [], [], [], [], []
435 435
436 436 def diff(self, diffopts, node2, match, prefix, **opts):
437 437 try:
438 438 node1 = node.bin(self._state[1])
439 439 # We currently expect node2 to come from substate and be
440 440 # in hex format
441 441 if node2 is not None:
442 442 node2 = node.bin(node2)
443 443 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
444 444 node1, node2, match,
445 445 prefix=os.path.join(prefix, self._path),
446 446 listsubrepos=True, **opts)
447 447 except error.RepoLookupError, inst:
448 448 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
449 449 % (inst, subrelpath(self)))
450 450
451 451 def archive(self, ui, archiver, prefix, match=None):
452 452 self._get(self._state + ('hg',))
453 453 abstractsubrepo.archive(self, ui, archiver, prefix, match)
454 454
455 455 rev = self._state[1]
456 456 ctx = self._repo[rev]
457 457 for subpath in ctx.substate:
458 458 s = subrepo(ctx, subpath)
459 459 submatch = matchmod.narrowmatcher(subpath, match)
460 460 s.archive(ui, archiver, os.path.join(prefix, self._path), submatch)
461 461
462 462 def dirty(self, ignoreupdate=False):
463 463 r = self._state[1]
464 464 if r == '' and not ignoreupdate: # no state recorded
465 465 return True
466 466 w = self._repo[None]
467 467 if r != w.p1().hex() and not ignoreupdate:
468 468 # different version checked out
469 469 return True
470 470 return w.dirty() # working directory changed
471 471
472 472 def basestate(self):
473 473 return self._repo['.'].hex()
474 474
475 475 def checknested(self, path):
476 476 return self._repo._checknested(self._repo.wjoin(path))
477 477
478 478 def commit(self, text, user, date):
479 479 # don't bother committing in the subrepo if it's only been
480 480 # updated
481 481 if not self.dirty(True):
482 482 return self._repo['.'].hex()
483 483 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
484 484 n = self._repo.commit(text, user, date)
485 485 if not n:
486 486 return self._repo['.'].hex() # different version checked out
487 487 return node.hex(n)
488 488
489 489 def remove(self):
490 490 # we can't fully delete the repository as it may contain
491 491 # local-only history
492 492 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
493 493 hg.clean(self._repo, node.nullid, False)
494 494
495 495 def _get(self, state):
496 496 source, revision, kind = state
497 497 if revision not in self._repo:
498 498 self._repo._subsource = source
499 499 srcurl = _abssource(self._repo)
500 500 other = hg.peer(self._repo.ui, {}, srcurl)
501 501 if len(self._repo) == 0:
502 502 self._repo.ui.status(_('cloning subrepo %s from %s\n')
503 503 % (subrelpath(self), srcurl))
504 504 parentrepo = self._repo._subparent
505 505 shutil.rmtree(self._repo.path)
506 506 other, cloned = hg.clone(self._repo._subparent.ui, {},
507 507 other, self._repo.root,
508 508 update=False)
509 509 self._repo = cloned.local()
510 510 self._initrepo(parentrepo, source, create=True)
511 511 else:
512 512 self._repo.ui.status(_('pulling subrepo %s from %s\n')
513 513 % (subrelpath(self), srcurl))
514 514 self._repo.pull(other)
515 515 bookmarks.updatefromremote(self._repo.ui, self._repo, other,
516 516 srcurl)
517 517
518 518 def get(self, state, overwrite=False):
519 519 self._get(state)
520 520 source, revision, kind = state
521 521 self._repo.ui.debug("getting subrepo %s\n" % self._path)
522 522 hg.clean(self._repo, revision, False)
523 523
524 524 def merge(self, state):
525 525 self._get(state)
526 526 cur = self._repo['.']
527 527 dst = self._repo[state[1]]
528 528 anc = dst.ancestor(cur)
529 529
530 530 def mergefunc():
531 531 if anc == cur and dst.branch() == cur.branch():
532 532 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
533 533 hg.update(self._repo, state[1])
534 534 elif anc == dst:
535 535 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
536 536 else:
537 537 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
538 538 hg.merge(self._repo, state[1], remind=False)
539 539
540 540 wctx = self._repo[None]
541 541 if self.dirty():
542 542 if anc != dst:
543 543 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
544 544 mergefunc()
545 545 else:
546 546 mergefunc()
547 547 else:
548 548 mergefunc()
549 549
550 550 def push(self, opts):
551 551 force = opts.get('force')
552 552 newbranch = opts.get('new_branch')
553 553 ssh = opts.get('ssh')
554 554
555 555 # push subrepos depth-first for coherent ordering
556 556 c = self._repo['']
557 557 subs = c.substate # only repos that are committed
558 558 for s in sorted(subs):
559 559 if c.sub(s).push(opts) == 0:
560 560 return False
561 561
562 562 dsturl = _abssource(self._repo, True)
563 563 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
564 564 (subrelpath(self), dsturl))
565 565 other = hg.peer(self._repo.ui, {'ssh': ssh}, dsturl)
566 566 return self._repo.push(other, force, newbranch=newbranch)
567 567
568 568 def outgoing(self, ui, dest, opts):
569 569 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
570 570
571 571 def incoming(self, ui, source, opts):
572 572 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
573 573
574 574 def files(self):
575 575 rev = self._state[1]
576 576 ctx = self._repo[rev]
577 577 return ctx.manifest()
578 578
579 579 def filedata(self, name):
580 580 rev = self._state[1]
581 581 return self._repo[rev][name].data()
582 582
583 583 def fileflags(self, name):
584 584 rev = self._state[1]
585 585 ctx = self._repo[rev]
586 586 return ctx.flags(name)
587 587
588 588 def walk(self, match):
589 589 ctx = self._repo[None]
590 590 return ctx.walk(match)
591 591
592 592 def forget(self, ui, match, prefix):
593 593 return cmdutil.forget(ui, self._repo, match,
594 594 os.path.join(prefix, self._path), True)
595 595
596 596 def revert(self, ui, substate, *pats, **opts):
597 597 # reverting a subrepo is a 2 step process:
598 598 # 1. if the no_backup is not set, revert all modified
599 599 # files inside the subrepo
600 600 # 2. update the subrepo to the revision specified in
601 601 # the corresponding substate dictionary
602 602 ui.status(_('reverting subrepo %s\n') % substate[0])
603 603 if not opts.get('no_backup'):
604 604 # Revert all files on the subrepo, creating backups
605 605 # Note that this will not recursively revert subrepos
606 606 # We could do it if there was a set:subrepos() predicate
607 607 opts = opts.copy()
608 608 opts['date'] = None
609 609 opts['rev'] = substate[1]
610 610
611 611 pats = []
612 612 if not opts['all']:
613 613 pats = ['set:modified()']
614 614 self.filerevert(ui, *pats, **opts)
615 615
616 616 # Update the repo to the revision specified in the given substate
617 617 self.get(substate, overwrite=True)
618 618
619 619 def filerevert(self, ui, *pats, **opts):
620 620 ctx = self._repo[opts['rev']]
621 621 parents = self._repo.dirstate.parents()
622 622 if opts['all']:
623 623 pats = ['set:modified()']
624 624 else:
625 625 pats = []
626 626 cmdutil.revert(ui, self._repo, ctx, parents, *pats, **opts)
627 627
628 628 class svnsubrepo(abstractsubrepo):
629 629 def __init__(self, ctx, path, state):
630 630 self._path = path
631 631 self._state = state
632 632 self._ctx = ctx
633 633 self._ui = ctx._repo.ui
634 634 self._exe = util.findexe('svn')
635 635 if not self._exe:
636 636 raise util.Abort(_("'svn' executable not found for subrepo '%s'")
637 637 % self._path)
638 638
639 639 def _svncommand(self, commands, filename='', failok=False):
640 640 cmd = [self._exe]
641 641 extrakw = {}
642 642 if not self._ui.interactive():
643 643 # Making stdin be a pipe should prevent svn from behaving
644 644 # interactively even if we can't pass --non-interactive.
645 645 extrakw['stdin'] = subprocess.PIPE
646 646 # Starting in svn 1.5 --non-interactive is a global flag
647 647 # instead of being per-command, but we need to support 1.4 so
648 648 # we have to be intelligent about what commands take
649 649 # --non-interactive.
650 650 if commands[0] in ('update', 'checkout', 'commit'):
651 651 cmd.append('--non-interactive')
652 652 cmd.extend(commands)
653 653 if filename is not None:
654 654 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
655 655 cmd.append(path)
656 656 env = dict(os.environ)
657 657 # Avoid localized output, preserve current locale for everything else.
658 lc_all = env.get('LC_ALL')
659 if lc_all:
660 env['LANG'] = lc_all
661 del env['LC_ALL']
658 662 env['LC_MESSAGES'] = 'C'
659 663 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
660 664 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
661 665 universal_newlines=True, env=env, **extrakw)
662 666 stdout, stderr = p.communicate()
663 667 stderr = stderr.strip()
664 668 if not failok:
665 669 if p.returncode:
666 670 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
667 671 if stderr:
668 672 self._ui.warn(stderr + '\n')
669 673 return stdout, stderr
670 674
671 675 @propertycache
672 676 def _svnversion(self):
673 677 output, err = self._svncommand(['--version'], filename=None)
674 678 m = re.search(r'^svn,\s+version\s+(\d+)\.(\d+)', output)
675 679 if not m:
676 680 raise util.Abort(_('cannot retrieve svn tool version'))
677 681 return (int(m.group(1)), int(m.group(2)))
678 682
679 683 def _wcrevs(self):
680 684 # Get the working directory revision as well as the last
681 685 # commit revision so we can compare the subrepo state with
682 686 # both. We used to store the working directory one.
683 687 output, err = self._svncommand(['info', '--xml'])
684 688 doc = xml.dom.minidom.parseString(output)
685 689 entries = doc.getElementsByTagName('entry')
686 690 lastrev, rev = '0', '0'
687 691 if entries:
688 692 rev = str(entries[0].getAttribute('revision')) or '0'
689 693 commits = entries[0].getElementsByTagName('commit')
690 694 if commits:
691 695 lastrev = str(commits[0].getAttribute('revision')) or '0'
692 696 return (lastrev, rev)
693 697
694 698 def _wcrev(self):
695 699 return self._wcrevs()[0]
696 700
697 701 def _wcchanged(self):
698 702 """Return (changes, extchanges, missing) where changes is True
699 703 if the working directory was changed, extchanges is
700 704 True if any of these changes concern an external entry and missing
701 705 is True if any change is a missing entry.
702 706 """
703 707 output, err = self._svncommand(['status', '--xml'])
704 708 externals, changes, missing = [], [], []
705 709 doc = xml.dom.minidom.parseString(output)
706 710 for e in doc.getElementsByTagName('entry'):
707 711 s = e.getElementsByTagName('wc-status')
708 712 if not s:
709 713 continue
710 714 item = s[0].getAttribute('item')
711 715 props = s[0].getAttribute('props')
712 716 path = e.getAttribute('path')
713 717 if item == 'external':
714 718 externals.append(path)
715 719 elif item == 'missing':
716 720 missing.append(path)
717 721 if (item not in ('', 'normal', 'unversioned', 'external')
718 722 or props not in ('', 'none', 'normal')):
719 723 changes.append(path)
720 724 for path in changes:
721 725 for ext in externals:
722 726 if path == ext or path.startswith(ext + os.sep):
723 727 return True, True, bool(missing)
724 728 return bool(changes), False, bool(missing)
725 729
726 730 def dirty(self, ignoreupdate=False):
727 731 if not self._wcchanged()[0]:
728 732 if self._state[1] in self._wcrevs() or ignoreupdate:
729 733 return False
730 734 return True
731 735
732 736 def basestate(self):
733 737 lastrev, rev = self._wcrevs()
734 738 if lastrev != rev:
735 739 # Last committed rev is not the same than rev. We would
736 740 # like to take lastrev but we do not know if the subrepo
737 741 # URL exists at lastrev. Test it and fallback to rev it
738 742 # is not there.
739 743 try:
740 744 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
741 745 return lastrev
742 746 except error.Abort:
743 747 pass
744 748 return rev
745 749
746 750 def commit(self, text, user, date):
747 751 # user and date are out of our hands since svn is centralized
748 752 changed, extchanged, missing = self._wcchanged()
749 753 if not changed:
750 754 return self.basestate()
751 755 if extchanged:
752 756 # Do not try to commit externals
753 757 raise util.Abort(_('cannot commit svn externals'))
754 758 if missing:
755 759 # svn can commit with missing entries but aborting like hg
756 760 # seems a better approach.
757 761 raise util.Abort(_('cannot commit missing svn entries'))
758 762 commitinfo, err = self._svncommand(['commit', '-m', text])
759 763 self._ui.status(commitinfo)
760 764 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
761 765 if not newrev:
762 766 if not commitinfo.strip():
763 767 # Sometimes, our definition of "changed" differs from
764 768 # svn one. For instance, svn ignores missing files
765 769 # when committing. If there are only missing files, no
766 770 # commit is made, no output and no error code.
767 771 raise util.Abort(_('failed to commit svn changes'))
768 772 raise util.Abort(commitinfo.splitlines()[-1])
769 773 newrev = newrev.groups()[0]
770 774 self._ui.status(self._svncommand(['update', '-r', newrev])[0])
771 775 return newrev
772 776
773 777 def remove(self):
774 778 if self.dirty():
775 779 self._ui.warn(_('not removing repo %s because '
776 780 'it has changes.\n' % self._path))
777 781 return
778 782 self._ui.note(_('removing subrepo %s\n') % self._path)
779 783
780 784 def onerror(function, path, excinfo):
781 785 if function is not os.remove:
782 786 raise
783 787 # read-only files cannot be unlinked under Windows
784 788 s = os.stat(path)
785 789 if (s.st_mode & stat.S_IWRITE) != 0:
786 790 raise
787 791 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
788 792 os.remove(path)
789 793
790 794 path = self._ctx._repo.wjoin(self._path)
791 795 shutil.rmtree(path, onerror=onerror)
792 796 try:
793 797 os.removedirs(os.path.dirname(path))
794 798 except OSError:
795 799 pass
796 800
797 801 def get(self, state, overwrite=False):
798 802 if overwrite:
799 803 self._svncommand(['revert', '--recursive'])
800 804 args = ['checkout']
801 805 if self._svnversion >= (1, 5):
802 806 args.append('--force')
803 807 # The revision must be specified at the end of the URL to properly
804 808 # update to a directory which has since been deleted and recreated.
805 809 args.append('%s@%s' % (state[0], state[1]))
806 810 status, err = self._svncommand(args, failok=True)
807 811 if not re.search('Checked out revision [0-9]+.', status):
808 812 if ('is already a working copy for a different URL' in err
809 813 and (self._wcchanged()[:2] == (False, False))):
810 814 # obstructed but clean working copy, so just blow it away.
811 815 self.remove()
812 816 self.get(state, overwrite=False)
813 817 return
814 818 raise util.Abort((status or err).splitlines()[-1])
815 819 self._ui.status(status)
816 820
817 821 def merge(self, state):
818 822 old = self._state[1]
819 823 new = state[1]
820 824 wcrev = self._wcrev()
821 825 if new != wcrev:
822 826 dirty = old == wcrev or self._wcchanged()[0]
823 827 if _updateprompt(self._ui, self, dirty, wcrev, new):
824 828 self.get(state, False)
825 829
826 830 def push(self, opts):
827 831 # push is a no-op for SVN
828 832 return True
829 833
830 834 def files(self):
831 835 output = self._svncommand(['list', '--recursive', '--xml'])[0]
832 836 doc = xml.dom.minidom.parseString(output)
833 837 paths = []
834 838 for e in doc.getElementsByTagName('entry'):
835 839 kind = str(e.getAttribute('kind'))
836 840 if kind != 'file':
837 841 continue
838 842 name = ''.join(c.data for c
839 843 in e.getElementsByTagName('name')[0].childNodes
840 844 if c.nodeType == c.TEXT_NODE)
841 845 paths.append(name.encode('utf-8'))
842 846 return paths
843 847
844 848 def filedata(self, name):
845 849 return self._svncommand(['cat'], name)[0]
846 850
847 851
848 852 class gitsubrepo(abstractsubrepo):
849 853 def __init__(self, ctx, path, state):
850 854 self._state = state
851 855 self._ctx = ctx
852 856 self._path = path
853 857 self._relpath = os.path.join(reporelpath(ctx._repo), path)
854 858 self._abspath = ctx._repo.wjoin(path)
855 859 self._subparent = ctx._repo
856 860 self._ui = ctx._repo.ui
857 861 self._ensuregit()
858 862
859 863 def _ensuregit(self):
860 864 try:
861 865 self._gitexecutable = 'git'
862 866 out, err = self._gitnodir(['--version'])
863 867 except OSError, e:
864 868 if e.errno != 2 or os.name != 'nt':
865 869 raise
866 870 self._gitexecutable = 'git.cmd'
867 871 out, err = self._gitnodir(['--version'])
868 872 m = re.search(r'^git version (\d+)\.(\d+)\.(\d+)', out)
869 873 if not m:
870 874 self._ui.warn(_('cannot retrieve git version'))
871 875 return
872 876 version = (int(m.group(1)), m.group(2), m.group(3))
873 877 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
874 878 # despite the docstring comment. For now, error on 1.4.0, warn on
875 879 # 1.5.0 but attempt to continue.
876 880 if version < (1, 5, 0):
877 881 raise util.Abort(_('git subrepo requires at least 1.6.0 or later'))
878 882 elif version < (1, 6, 0):
879 883 self._ui.warn(_('git subrepo requires at least 1.6.0 or later'))
880 884
881 885 def _gitcommand(self, commands, env=None, stream=False):
882 886 return self._gitdir(commands, env=env, stream=stream)[0]
883 887
884 888 def _gitdir(self, commands, env=None, stream=False):
885 889 return self._gitnodir(commands, env=env, stream=stream,
886 890 cwd=self._abspath)
887 891
888 892 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
889 893 """Calls the git command
890 894
891 895 The methods tries to call the git command. versions previor to 1.6.0
892 896 are not supported and very probably fail.
893 897 """
894 898 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
895 899 # unless ui.quiet is set, print git's stderr,
896 900 # which is mostly progress and useful info
897 901 errpipe = None
898 902 if self._ui.quiet:
899 903 errpipe = open(os.devnull, 'w')
900 904 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
901 905 cwd=cwd, env=env, close_fds=util.closefds,
902 906 stdout=subprocess.PIPE, stderr=errpipe)
903 907 if stream:
904 908 return p.stdout, None
905 909
906 910 retdata = p.stdout.read().strip()
907 911 # wait for the child to exit to avoid race condition.
908 912 p.wait()
909 913
910 914 if p.returncode != 0 and p.returncode != 1:
911 915 # there are certain error codes that are ok
912 916 command = commands[0]
913 917 if command in ('cat-file', 'symbolic-ref'):
914 918 return retdata, p.returncode
915 919 # for all others, abort
916 920 raise util.Abort('git %s error %d in %s' %
917 921 (command, p.returncode, self._relpath))
918 922
919 923 return retdata, p.returncode
920 924
921 925 def _gitmissing(self):
922 926 return not os.path.exists(os.path.join(self._abspath, '.git'))
923 927
924 928 def _gitstate(self):
925 929 return self._gitcommand(['rev-parse', 'HEAD'])
926 930
927 931 def _gitcurrentbranch(self):
928 932 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
929 933 if err:
930 934 current = None
931 935 return current
932 936
933 937 def _gitremote(self, remote):
934 938 out = self._gitcommand(['remote', 'show', '-n', remote])
935 939 line = out.split('\n')[1]
936 940 i = line.index('URL: ') + len('URL: ')
937 941 return line[i:]
938 942
939 943 def _githavelocally(self, revision):
940 944 out, code = self._gitdir(['cat-file', '-e', revision])
941 945 return code == 0
942 946
943 947 def _gitisancestor(self, r1, r2):
944 948 base = self._gitcommand(['merge-base', r1, r2])
945 949 return base == r1
946 950
947 951 def _gitisbare(self):
948 952 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
949 953
950 954 def _gitupdatestat(self):
951 955 """This must be run before git diff-index.
952 956 diff-index only looks at changes to file stat;
953 957 this command looks at file contents and updates the stat."""
954 958 self._gitcommand(['update-index', '-q', '--refresh'])
955 959
956 960 def _gitbranchmap(self):
957 961 '''returns 2 things:
958 962 a map from git branch to revision
959 963 a map from revision to branches'''
960 964 branch2rev = {}
961 965 rev2branch = {}
962 966
963 967 out = self._gitcommand(['for-each-ref', '--format',
964 968 '%(objectname) %(refname)'])
965 969 for line in out.split('\n'):
966 970 revision, ref = line.split(' ')
967 971 if (not ref.startswith('refs/heads/') and
968 972 not ref.startswith('refs/remotes/')):
969 973 continue
970 974 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
971 975 continue # ignore remote/HEAD redirects
972 976 branch2rev[ref] = revision
973 977 rev2branch.setdefault(revision, []).append(ref)
974 978 return branch2rev, rev2branch
975 979
976 980 def _gittracking(self, branches):
977 981 'return map of remote branch to local tracking branch'
978 982 # assumes no more than one local tracking branch for each remote
979 983 tracking = {}
980 984 for b in branches:
981 985 if b.startswith('refs/remotes/'):
982 986 continue
983 987 bname = b.split('/', 2)[2]
984 988 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
985 989 if remote:
986 990 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
987 991 tracking['refs/remotes/%s/%s' %
988 992 (remote, ref.split('/', 2)[2])] = b
989 993 return tracking
990 994
991 995 def _abssource(self, source):
992 996 if '://' not in source:
993 997 # recognize the scp syntax as an absolute source
994 998 colon = source.find(':')
995 999 if colon != -1 and '/' not in source[:colon]:
996 1000 return source
997 1001 self._subsource = source
998 1002 return _abssource(self)
999 1003
1000 1004 def _fetch(self, source, revision):
1001 1005 if self._gitmissing():
1002 1006 source = self._abssource(source)
1003 1007 self._ui.status(_('cloning subrepo %s from %s\n') %
1004 1008 (self._relpath, source))
1005 1009 self._gitnodir(['clone', source, self._abspath])
1006 1010 if self._githavelocally(revision):
1007 1011 return
1008 1012 self._ui.status(_('pulling subrepo %s from %s\n') %
1009 1013 (self._relpath, self._gitremote('origin')))
1010 1014 # try only origin: the originally cloned repo
1011 1015 self._gitcommand(['fetch'])
1012 1016 if not self._githavelocally(revision):
1013 1017 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
1014 1018 (revision, self._relpath))
1015 1019
1016 1020 def dirty(self, ignoreupdate=False):
1017 1021 if self._gitmissing():
1018 1022 return self._state[1] != ''
1019 1023 if self._gitisbare():
1020 1024 return True
1021 1025 if not ignoreupdate and self._state[1] != self._gitstate():
1022 1026 # different version checked out
1023 1027 return True
1024 1028 # check for staged changes or modified files; ignore untracked files
1025 1029 self._gitupdatestat()
1026 1030 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1027 1031 return code == 1
1028 1032
1029 1033 def basestate(self):
1030 1034 return self._gitstate()
1031 1035
1032 1036 def get(self, state, overwrite=False):
1033 1037 source, revision, kind = state
1034 1038 if not revision:
1035 1039 self.remove()
1036 1040 return
1037 1041 self._fetch(source, revision)
1038 1042 # if the repo was set to be bare, unbare it
1039 1043 if self._gitisbare():
1040 1044 self._gitcommand(['config', 'core.bare', 'false'])
1041 1045 if self._gitstate() == revision:
1042 1046 self._gitcommand(['reset', '--hard', 'HEAD'])
1043 1047 return
1044 1048 elif self._gitstate() == revision:
1045 1049 if overwrite:
1046 1050 # first reset the index to unmark new files for commit, because
1047 1051 # reset --hard will otherwise throw away files added for commit,
1048 1052 # not just unmark them.
1049 1053 self._gitcommand(['reset', 'HEAD'])
1050 1054 self._gitcommand(['reset', '--hard', 'HEAD'])
1051 1055 return
1052 1056 branch2rev, rev2branch = self._gitbranchmap()
1053 1057
1054 1058 def checkout(args):
1055 1059 cmd = ['checkout']
1056 1060 if overwrite:
1057 1061 # first reset the index to unmark new files for commit, because
1058 1062 # the -f option will otherwise throw away files added for
1059 1063 # commit, not just unmark them.
1060 1064 self._gitcommand(['reset', 'HEAD'])
1061 1065 cmd.append('-f')
1062 1066 self._gitcommand(cmd + args)
1063 1067
1064 1068 def rawcheckout():
1065 1069 # no branch to checkout, check it out with no branch
1066 1070 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1067 1071 self._relpath)
1068 1072 self._ui.warn(_('check out a git branch if you intend '
1069 1073 'to make changes\n'))
1070 1074 checkout(['-q', revision])
1071 1075
1072 1076 if revision not in rev2branch:
1073 1077 rawcheckout()
1074 1078 return
1075 1079 branches = rev2branch[revision]
1076 1080 firstlocalbranch = None
1077 1081 for b in branches:
1078 1082 if b == 'refs/heads/master':
1079 1083 # master trumps all other branches
1080 1084 checkout(['refs/heads/master'])
1081 1085 return
1082 1086 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1083 1087 firstlocalbranch = b
1084 1088 if firstlocalbranch:
1085 1089 checkout([firstlocalbranch])
1086 1090 return
1087 1091
1088 1092 tracking = self._gittracking(branch2rev.keys())
1089 1093 # choose a remote branch already tracked if possible
1090 1094 remote = branches[0]
1091 1095 if remote not in tracking:
1092 1096 for b in branches:
1093 1097 if b in tracking:
1094 1098 remote = b
1095 1099 break
1096 1100
1097 1101 if remote not in tracking:
1098 1102 # create a new local tracking branch
1099 1103 local = remote.split('/', 2)[2]
1100 1104 checkout(['-b', local, remote])
1101 1105 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1102 1106 # When updating to a tracked remote branch,
1103 1107 # if the local tracking branch is downstream of it,
1104 1108 # a normal `git pull` would have performed a "fast-forward merge"
1105 1109 # which is equivalent to updating the local branch to the remote.
1106 1110 # Since we are only looking at branching at update, we need to
1107 1111 # detect this situation and perform this action lazily.
1108 1112 if tracking[remote] != self._gitcurrentbranch():
1109 1113 checkout([tracking[remote]])
1110 1114 self._gitcommand(['merge', '--ff', remote])
1111 1115 else:
1112 1116 # a real merge would be required, just checkout the revision
1113 1117 rawcheckout()
1114 1118
1115 1119 def commit(self, text, user, date):
1116 1120 if self._gitmissing():
1117 1121 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1118 1122 cmd = ['commit', '-a', '-m', text]
1119 1123 env = os.environ.copy()
1120 1124 if user:
1121 1125 cmd += ['--author', user]
1122 1126 if date:
1123 1127 # git's date parser silently ignores when seconds < 1e9
1124 1128 # convert to ISO8601
1125 1129 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1126 1130 '%Y-%m-%dT%H:%M:%S %1%2')
1127 1131 self._gitcommand(cmd, env=env)
1128 1132 # make sure commit works otherwise HEAD might not exist under certain
1129 1133 # circumstances
1130 1134 return self._gitstate()
1131 1135
1132 1136 def merge(self, state):
1133 1137 source, revision, kind = state
1134 1138 self._fetch(source, revision)
1135 1139 base = self._gitcommand(['merge-base', revision, self._state[1]])
1136 1140 self._gitupdatestat()
1137 1141 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1138 1142
1139 1143 def mergefunc():
1140 1144 if base == revision:
1141 1145 self.get(state) # fast forward merge
1142 1146 elif base != self._state[1]:
1143 1147 self._gitcommand(['merge', '--no-commit', revision])
1144 1148
1145 1149 if self.dirty():
1146 1150 if self._gitstate() != revision:
1147 1151 dirty = self._gitstate() == self._state[1] or code != 0
1148 1152 if _updateprompt(self._ui, self, dirty,
1149 1153 self._state[1][:7], revision[:7]):
1150 1154 mergefunc()
1151 1155 else:
1152 1156 mergefunc()
1153 1157
1154 1158 def push(self, opts):
1155 1159 force = opts.get('force')
1156 1160
1157 1161 if not self._state[1]:
1158 1162 return True
1159 1163 if self._gitmissing():
1160 1164 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1161 1165 # if a branch in origin contains the revision, nothing to do
1162 1166 branch2rev, rev2branch = self._gitbranchmap()
1163 1167 if self._state[1] in rev2branch:
1164 1168 for b in rev2branch[self._state[1]]:
1165 1169 if b.startswith('refs/remotes/origin/'):
1166 1170 return True
1167 1171 for b, revision in branch2rev.iteritems():
1168 1172 if b.startswith('refs/remotes/origin/'):
1169 1173 if self._gitisancestor(self._state[1], revision):
1170 1174 return True
1171 1175 # otherwise, try to push the currently checked out branch
1172 1176 cmd = ['push']
1173 1177 if force:
1174 1178 cmd.append('--force')
1175 1179
1176 1180 current = self._gitcurrentbranch()
1177 1181 if current:
1178 1182 # determine if the current branch is even useful
1179 1183 if not self._gitisancestor(self._state[1], current):
1180 1184 self._ui.warn(_('unrelated git branch checked out '
1181 1185 'in subrepo %s\n') % self._relpath)
1182 1186 return False
1183 1187 self._ui.status(_('pushing branch %s of subrepo %s\n') %
1184 1188 (current.split('/', 2)[2], self._relpath))
1185 1189 self._gitcommand(cmd + ['origin', current])
1186 1190 return True
1187 1191 else:
1188 1192 self._ui.warn(_('no branch checked out in subrepo %s\n'
1189 1193 'cannot push revision %s\n') %
1190 1194 (self._relpath, self._state[1]))
1191 1195 return False
1192 1196
1193 1197 def remove(self):
1194 1198 if self._gitmissing():
1195 1199 return
1196 1200 if self.dirty():
1197 1201 self._ui.warn(_('not removing repo %s because '
1198 1202 'it has changes.\n') % self._relpath)
1199 1203 return
1200 1204 # we can't fully delete the repository as it may contain
1201 1205 # local-only history
1202 1206 self._ui.note(_('removing subrepo %s\n') % self._relpath)
1203 1207 self._gitcommand(['config', 'core.bare', 'true'])
1204 1208 for f in os.listdir(self._abspath):
1205 1209 if f == '.git':
1206 1210 continue
1207 1211 path = os.path.join(self._abspath, f)
1208 1212 if os.path.isdir(path) and not os.path.islink(path):
1209 1213 shutil.rmtree(path)
1210 1214 else:
1211 1215 os.remove(path)
1212 1216
1213 1217 def archive(self, ui, archiver, prefix, match=None):
1214 1218 source, revision = self._state
1215 1219 if not revision:
1216 1220 return
1217 1221 self._fetch(source, revision)
1218 1222
1219 1223 # Parse git's native archive command.
1220 1224 # This should be much faster than manually traversing the trees
1221 1225 # and objects with many subprocess calls.
1222 1226 tarstream = self._gitcommand(['archive', revision], stream=True)
1223 1227 tar = tarfile.open(fileobj=tarstream, mode='r|')
1224 1228 relpath = subrelpath(self)
1225 1229 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1226 1230 for i, info in enumerate(tar):
1227 1231 if info.isdir():
1228 1232 continue
1229 1233 if match and not match(info.name):
1230 1234 continue
1231 1235 if info.issym():
1232 1236 data = info.linkname
1233 1237 else:
1234 1238 data = tar.extractfile(info).read()
1235 1239 archiver.addfile(os.path.join(prefix, self._path, info.name),
1236 1240 info.mode, info.issym(), data)
1237 1241 ui.progress(_('archiving (%s)') % relpath, i + 1,
1238 1242 unit=_('files'))
1239 1243 ui.progress(_('archiving (%s)') % relpath, None)
1240 1244
1241 1245
1242 1246 def status(self, rev2, **opts):
1243 1247 rev1 = self._state[1]
1244 1248 if self._gitmissing() or not rev1:
1245 1249 # if the repo is missing, return no results
1246 1250 return [], [], [], [], [], [], []
1247 1251 modified, added, removed = [], [], []
1248 1252 self._gitupdatestat()
1249 1253 if rev2:
1250 1254 command = ['diff-tree', rev1, rev2]
1251 1255 else:
1252 1256 command = ['diff-index', rev1]
1253 1257 out = self._gitcommand(command)
1254 1258 for line in out.split('\n'):
1255 1259 tab = line.find('\t')
1256 1260 if tab == -1:
1257 1261 continue
1258 1262 status, f = line[tab - 1], line[tab + 1:]
1259 1263 if status == 'M':
1260 1264 modified.append(f)
1261 1265 elif status == 'A':
1262 1266 added.append(f)
1263 1267 elif status == 'D':
1264 1268 removed.append(f)
1265 1269
1266 1270 deleted = unknown = ignored = clean = []
1267 1271 return modified, added, removed, deleted, unknown, ignored, clean
1268 1272
1269 1273 types = {
1270 1274 'hg': hgsubrepo,
1271 1275 'svn': svnsubrepo,
1272 1276 'git': gitsubrepo,
1273 1277 }
General Comments 0
You need to be logged in to leave comments. Login now