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