##// END OF EJS Templates
subrepo: support Git being named "git.cmd" on Windows (issue3173)...
Benjamin Pollack -
r17025:8ad08dca stable
parent child Browse files
Show More
@@ -1,1257 +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(['info', '%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 try:
854 self._gitexecutable = 'git'
855 out, err = self._gitnodir(['--version'])
856 except OSError, e:
857 if e.errno != 2 or os.name != 'nt':
858 raise
859 self._gitexecutable = 'git.cmd'
853 860 out, err = self._gitnodir(['--version'])
854 861 m = re.search(r'^git version (\d+)\.(\d+)\.(\d+)', out)
855 862 if not m:
856 863 self._ui.warn(_('cannot retrieve git version'))
857 864 return
858 865 version = (int(m.group(1)), m.group(2), m.group(3))
859 866 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
860 867 # despite the docstring comment. For now, error on 1.4.0, warn on
861 868 # 1.5.0 but attempt to continue.
862 869 if version < (1, 5, 0):
863 870 raise util.Abort(_('git subrepo requires at least 1.6.0 or later'))
864 871 elif version < (1, 6, 0):
865 872 self._ui.warn(_('git subrepo requires at least 1.6.0 or later'))
866 873
867 874 def _gitcommand(self, commands, env=None, stream=False):
868 875 return self._gitdir(commands, env=env, stream=stream)[0]
869 876
870 877 def _gitdir(self, commands, env=None, stream=False):
871 878 return self._gitnodir(commands, env=env, stream=stream,
872 879 cwd=self._abspath)
873 880
874 881 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
875 882 """Calls the git command
876 883
877 884 The methods tries to call the git command. versions previor to 1.6.0
878 885 are not supported and very probably fail.
879 886 """
880 887 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
881 888 # unless ui.quiet is set, print git's stderr,
882 889 # which is mostly progress and useful info
883 890 errpipe = None
884 891 if self._ui.quiet:
885 892 errpipe = open(os.devnull, 'w')
886 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
887 close_fds=util.closefds,
893 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
894 cwd=cwd, env=env, close_fds=util.closefds,
888 895 stdout=subprocess.PIPE, stderr=errpipe)
889 896 if stream:
890 897 return p.stdout, None
891 898
892 899 retdata = p.stdout.read().strip()
893 900 # wait for the child to exit to avoid race condition.
894 901 p.wait()
895 902
896 903 if p.returncode != 0 and p.returncode != 1:
897 904 # there are certain error codes that are ok
898 905 command = commands[0]
899 906 if command in ('cat-file', 'symbolic-ref'):
900 907 return retdata, p.returncode
901 908 # for all others, abort
902 909 raise util.Abort('git %s error %d in %s' %
903 910 (command, p.returncode, self._relpath))
904 911
905 912 return retdata, p.returncode
906 913
907 914 def _gitmissing(self):
908 915 return not os.path.exists(os.path.join(self._abspath, '.git'))
909 916
910 917 def _gitstate(self):
911 918 return self._gitcommand(['rev-parse', 'HEAD'])
912 919
913 920 def _gitcurrentbranch(self):
914 921 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
915 922 if err:
916 923 current = None
917 924 return current
918 925
919 926 def _gitremote(self, remote):
920 927 out = self._gitcommand(['remote', 'show', '-n', remote])
921 928 line = out.split('\n')[1]
922 929 i = line.index('URL: ') + len('URL: ')
923 930 return line[i:]
924 931
925 932 def _githavelocally(self, revision):
926 933 out, code = self._gitdir(['cat-file', '-e', revision])
927 934 return code == 0
928 935
929 936 def _gitisancestor(self, r1, r2):
930 937 base = self._gitcommand(['merge-base', r1, r2])
931 938 return base == r1
932 939
933 940 def _gitisbare(self):
934 941 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
935 942
936 943 def _gitupdatestat(self):
937 944 """This must be run before git diff-index.
938 945 diff-index only looks at changes to file stat;
939 946 this command looks at file contents and updates the stat."""
940 947 self._gitcommand(['update-index', '-q', '--refresh'])
941 948
942 949 def _gitbranchmap(self):
943 950 '''returns 2 things:
944 951 a map from git branch to revision
945 952 a map from revision to branches'''
946 953 branch2rev = {}
947 954 rev2branch = {}
948 955
949 956 out = self._gitcommand(['for-each-ref', '--format',
950 957 '%(objectname) %(refname)'])
951 958 for line in out.split('\n'):
952 959 revision, ref = line.split(' ')
953 960 if (not ref.startswith('refs/heads/') and
954 961 not ref.startswith('refs/remotes/')):
955 962 continue
956 963 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
957 964 continue # ignore remote/HEAD redirects
958 965 branch2rev[ref] = revision
959 966 rev2branch.setdefault(revision, []).append(ref)
960 967 return branch2rev, rev2branch
961 968
962 969 def _gittracking(self, branches):
963 970 'return map of remote branch to local tracking branch'
964 971 # assumes no more than one local tracking branch for each remote
965 972 tracking = {}
966 973 for b in branches:
967 974 if b.startswith('refs/remotes/'):
968 975 continue
969 976 bname = b.split('/', 2)[2]
970 977 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
971 978 if remote:
972 979 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
973 980 tracking['refs/remotes/%s/%s' %
974 981 (remote, ref.split('/', 2)[2])] = b
975 982 return tracking
976 983
977 984 def _abssource(self, source):
978 985 if '://' not in source:
979 986 # recognize the scp syntax as an absolute source
980 987 colon = source.find(':')
981 988 if colon != -1 and '/' not in source[:colon]:
982 989 return source
983 990 self._subsource = source
984 991 return _abssource(self)
985 992
986 993 def _fetch(self, source, revision):
987 994 if self._gitmissing():
988 995 source = self._abssource(source)
989 996 self._ui.status(_('cloning subrepo %s from %s\n') %
990 997 (self._relpath, source))
991 998 self._gitnodir(['clone', source, self._abspath])
992 999 if self._githavelocally(revision):
993 1000 return
994 1001 self._ui.status(_('pulling subrepo %s from %s\n') %
995 1002 (self._relpath, self._gitremote('origin')))
996 1003 # try only origin: the originally cloned repo
997 1004 self._gitcommand(['fetch'])
998 1005 if not self._githavelocally(revision):
999 1006 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
1000 1007 (revision, self._relpath))
1001 1008
1002 1009 def dirty(self, ignoreupdate=False):
1003 1010 if self._gitmissing():
1004 1011 return self._state[1] != ''
1005 1012 if self._gitisbare():
1006 1013 return True
1007 1014 if not ignoreupdate and self._state[1] != self._gitstate():
1008 1015 # different version checked out
1009 1016 return True
1010 1017 # check for staged changes or modified files; ignore untracked files
1011 1018 self._gitupdatestat()
1012 1019 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1013 1020 return code == 1
1014 1021
1015 1022 def basestate(self):
1016 1023 return self._gitstate()
1017 1024
1018 1025 def get(self, state, overwrite=False):
1019 1026 source, revision, kind = state
1020 1027 if not revision:
1021 1028 self.remove()
1022 1029 return
1023 1030 self._fetch(source, revision)
1024 1031 # if the repo was set to be bare, unbare it
1025 1032 if self._gitisbare():
1026 1033 self._gitcommand(['config', 'core.bare', 'false'])
1027 1034 if self._gitstate() == revision:
1028 1035 self._gitcommand(['reset', '--hard', 'HEAD'])
1029 1036 return
1030 1037 elif self._gitstate() == revision:
1031 1038 if overwrite:
1032 1039 # first reset the index to unmark new files for commit, because
1033 1040 # reset --hard will otherwise throw away files added for commit,
1034 1041 # not just unmark them.
1035 1042 self._gitcommand(['reset', 'HEAD'])
1036 1043 self._gitcommand(['reset', '--hard', 'HEAD'])
1037 1044 return
1038 1045 branch2rev, rev2branch = self._gitbranchmap()
1039 1046
1040 1047 def checkout(args):
1041 1048 cmd = ['checkout']
1042 1049 if overwrite:
1043 1050 # first reset the index to unmark new files for commit, because
1044 1051 # the -f option will otherwise throw away files added for
1045 1052 # commit, not just unmark them.
1046 1053 self._gitcommand(['reset', 'HEAD'])
1047 1054 cmd.append('-f')
1048 1055 self._gitcommand(cmd + args)
1049 1056
1050 1057 def rawcheckout():
1051 1058 # no branch to checkout, check it out with no branch
1052 1059 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1053 1060 self._relpath)
1054 1061 self._ui.warn(_('check out a git branch if you intend '
1055 1062 'to make changes\n'))
1056 1063 checkout(['-q', revision])
1057 1064
1058 1065 if revision not in rev2branch:
1059 1066 rawcheckout()
1060 1067 return
1061 1068 branches = rev2branch[revision]
1062 1069 firstlocalbranch = None
1063 1070 for b in branches:
1064 1071 if b == 'refs/heads/master':
1065 1072 # master trumps all other branches
1066 1073 checkout(['refs/heads/master'])
1067 1074 return
1068 1075 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1069 1076 firstlocalbranch = b
1070 1077 if firstlocalbranch:
1071 1078 checkout([firstlocalbranch])
1072 1079 return
1073 1080
1074 1081 tracking = self._gittracking(branch2rev.keys())
1075 1082 # choose a remote branch already tracked if possible
1076 1083 remote = branches[0]
1077 1084 if remote not in tracking:
1078 1085 for b in branches:
1079 1086 if b in tracking:
1080 1087 remote = b
1081 1088 break
1082 1089
1083 1090 if remote not in tracking:
1084 1091 # create a new local tracking branch
1085 1092 local = remote.split('/', 2)[2]
1086 1093 checkout(['-b', local, remote])
1087 1094 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1088 1095 # When updating to a tracked remote branch,
1089 1096 # if the local tracking branch is downstream of it,
1090 1097 # a normal `git pull` would have performed a "fast-forward merge"
1091 1098 # which is equivalent to updating the local branch to the remote.
1092 1099 # Since we are only looking at branching at update, we need to
1093 1100 # detect this situation and perform this action lazily.
1094 1101 if tracking[remote] != self._gitcurrentbranch():
1095 1102 checkout([tracking[remote]])
1096 1103 self._gitcommand(['merge', '--ff', remote])
1097 1104 else:
1098 1105 # a real merge would be required, just checkout the revision
1099 1106 rawcheckout()
1100 1107
1101 1108 def commit(self, text, user, date):
1102 1109 if self._gitmissing():
1103 1110 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1104 1111 cmd = ['commit', '-a', '-m', text]
1105 1112 env = os.environ.copy()
1106 1113 if user:
1107 1114 cmd += ['--author', user]
1108 1115 if date:
1109 1116 # git's date parser silently ignores when seconds < 1e9
1110 1117 # convert to ISO8601
1111 1118 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1112 1119 '%Y-%m-%dT%H:%M:%S %1%2')
1113 1120 self._gitcommand(cmd, env=env)
1114 1121 # make sure commit works otherwise HEAD might not exist under certain
1115 1122 # circumstances
1116 1123 return self._gitstate()
1117 1124
1118 1125 def merge(self, state):
1119 1126 source, revision, kind = state
1120 1127 self._fetch(source, revision)
1121 1128 base = self._gitcommand(['merge-base', revision, self._state[1]])
1122 1129 self._gitupdatestat()
1123 1130 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1124 1131
1125 1132 def mergefunc():
1126 1133 if base == revision:
1127 1134 self.get(state) # fast forward merge
1128 1135 elif base != self._state[1]:
1129 1136 self._gitcommand(['merge', '--no-commit', revision])
1130 1137
1131 1138 if self.dirty():
1132 1139 if self._gitstate() != revision:
1133 1140 dirty = self._gitstate() == self._state[1] or code != 0
1134 1141 if _updateprompt(self._ui, self, dirty,
1135 1142 self._state[1][:7], revision[:7]):
1136 1143 mergefunc()
1137 1144 else:
1138 1145 mergefunc()
1139 1146
1140 1147 def push(self, opts):
1141 1148 force = opts.get('force')
1142 1149
1143 1150 if not self._state[1]:
1144 1151 return True
1145 1152 if self._gitmissing():
1146 1153 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1147 1154 # if a branch in origin contains the revision, nothing to do
1148 1155 branch2rev, rev2branch = self._gitbranchmap()
1149 1156 if self._state[1] in rev2branch:
1150 1157 for b in rev2branch[self._state[1]]:
1151 1158 if b.startswith('refs/remotes/origin/'):
1152 1159 return True
1153 1160 for b, revision in branch2rev.iteritems():
1154 1161 if b.startswith('refs/remotes/origin/'):
1155 1162 if self._gitisancestor(self._state[1], revision):
1156 1163 return True
1157 1164 # otherwise, try to push the currently checked out branch
1158 1165 cmd = ['push']
1159 1166 if force:
1160 1167 cmd.append('--force')
1161 1168
1162 1169 current = self._gitcurrentbranch()
1163 1170 if current:
1164 1171 # determine if the current branch is even useful
1165 1172 if not self._gitisancestor(self._state[1], current):
1166 1173 self._ui.warn(_('unrelated git branch checked out '
1167 1174 'in subrepo %s\n') % self._relpath)
1168 1175 return False
1169 1176 self._ui.status(_('pushing branch %s of subrepo %s\n') %
1170 1177 (current.split('/', 2)[2], self._relpath))
1171 1178 self._gitcommand(cmd + ['origin', current])
1172 1179 return True
1173 1180 else:
1174 1181 self._ui.warn(_('no branch checked out in subrepo %s\n'
1175 1182 'cannot push revision %s') %
1176 1183 (self._relpath, self._state[1]))
1177 1184 return False
1178 1185
1179 1186 def remove(self):
1180 1187 if self._gitmissing():
1181 1188 return
1182 1189 if self.dirty():
1183 1190 self._ui.warn(_('not removing repo %s because '
1184 1191 'it has changes.\n') % self._relpath)
1185 1192 return
1186 1193 # we can't fully delete the repository as it may contain
1187 1194 # local-only history
1188 1195 self._ui.note(_('removing subrepo %s\n') % self._relpath)
1189 1196 self._gitcommand(['config', 'core.bare', 'true'])
1190 1197 for f in os.listdir(self._abspath):
1191 1198 if f == '.git':
1192 1199 continue
1193 1200 path = os.path.join(self._abspath, f)
1194 1201 if os.path.isdir(path) and not os.path.islink(path):
1195 1202 shutil.rmtree(path)
1196 1203 else:
1197 1204 os.remove(path)
1198 1205
1199 1206 def archive(self, ui, archiver, prefix):
1200 1207 source, revision = self._state
1201 1208 if not revision:
1202 1209 return
1203 1210 self._fetch(source, revision)
1204 1211
1205 1212 # Parse git's native archive command.
1206 1213 # This should be much faster than manually traversing the trees
1207 1214 # and objects with many subprocess calls.
1208 1215 tarstream = self._gitcommand(['archive', revision], stream=True)
1209 1216 tar = tarfile.open(fileobj=tarstream, mode='r|')
1210 1217 relpath = subrelpath(self)
1211 1218 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1212 1219 for i, info in enumerate(tar):
1213 1220 if info.isdir():
1214 1221 continue
1215 1222 if info.issym():
1216 1223 data = info.linkname
1217 1224 else:
1218 1225 data = tar.extractfile(info).read()
1219 1226 archiver.addfile(os.path.join(prefix, self._path, info.name),
1220 1227 info.mode, info.issym(), data)
1221 1228 ui.progress(_('archiving (%s)') % relpath, i + 1,
1222 1229 unit=_('files'))
1223 1230 ui.progress(_('archiving (%s)') % relpath, None)
1224 1231
1225 1232
1226 1233 def status(self, rev2, **opts):
1227 1234 rev1 = self._state[1]
1228 1235 if self._gitmissing() or not rev1:
1229 1236 # if the repo is missing, return no results
1230 1237 return [], [], [], [], [], [], []
1231 1238 modified, added, removed = [], [], []
1232 1239 self._gitupdatestat()
1233 1240 if rev2:
1234 1241 command = ['diff-tree', rev1, rev2]
1235 1242 else:
1236 1243 command = ['diff-index', rev1]
1237 1244 out = self._gitcommand(command)
1238 1245 for line in out.split('\n'):
1239 1246 tab = line.find('\t')
1240 1247 if tab == -1:
1241 1248 continue
1242 1249 status, f = line[tab - 1], line[tab + 1:]
1243 1250 if status == 'M':
1244 1251 modified.append(f)
1245 1252 elif status == 'A':
1246 1253 added.append(f)
1247 1254 elif status == 'D':
1248 1255 removed.append(f)
1249 1256
1250 1257 deleted = unknown = ignored = clean = []
1251 1258 return modified, added, removed, deleted, unknown, ignored, clean
1252 1259
1253 1260 types = {
1254 1261 'hg': hgsubrepo,
1255 1262 'svn': svnsubrepo,
1256 1263 'git': gitsubrepo,
1257 1264 }
General Comments 0
You need to be logged in to leave comments. Login now