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