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